剩余参数
对于变长参数的函数,在 JavaScript 中可以这么定义:
function sum(numbers) {
let sum = 0;
for (let number of numbers) {
sum += number;
}
return sum;
}
console.log(sum(1, 2, 3));
console.log(sum());
// 6
// 0
可以在变长参数前添加若干确定的参数:
function display(message, names) {
console.log(message, names);
}
display("Hello", "Alice", "Bob", "Charlie");
// Hello [ 'Alice', 'Bob', 'Charlie' ]
需要注意的是,变长参数只能有一个,且必须要在结尾处,否则会报错。
在旧版本的 JavaScript 中,可以使用一个特殊参数arguments获取所有的函数参数:
function sum2() {
let sum = 0;
for (let number of arguments) {
sum += number;
}
return sum;
}
console.log(sum2(1, 2, 3));
// 6
比较特殊的是,箭头函数没有自己的arguments:
function test() {
console.log("test", arguments);
let test2 = () => {
console.log("test2", arguments);
}
test2();
}
test(1, 2, 3);
// test [Arguments] { '0': 1, '1': 2, '2': 3 }
// test2 [Arguments] { '0': 1, '1': 2, '2': 3 }
在这个示例中,箭头函数的 arguments 所在的外部函数的 arguments。这与箭头函数没有自己的this是类似的。
展开语法
对于变长参数,可以用数组结合展开语法进行调用:
let numbers = [1, 2, 3];
console.log(sum(numbers));
展开语法还可以用于合并数组:
let arr1 = [1, 2, 3];
let arr2 = [arr1, 4, 5];
console.log(arr2);
// [ 1, 2, 3, 4, 5 ]
或者拷贝数组:
let oldArr = [1, 2, 3];
let newArr = [oldArr];
oldArr.push(4);
console.log(oldArr);
console.log(newArr);
// [ 1, 2, 3, 4 ]
// [ 1, 2, 3 ]
拷贝对象:
let person1 = { name: "Alice", age: 20 };
let person2 = { person1};
person1.name = "Bob";
console.log(person1);
console.log(person2);
// { name: 'Bob', age: 20 }
// { name: 'Alice', age: 20 }
展开语法可以用于任何可迭代对象,相当于使用for...of进行遍历,比如:
let chars = "abcdefg";
console.log([chars]);
// [
// 'a', 'b', 'c',
// 'd', 'e', 'f',
// 'g'
// ]
这相当于:
let char2 = "abcdefg";
console.log(Array.from(char2))
只不过Array.from更通用,它接收的参数可以是类数组或可迭代对象。
函数
在 JavaScript 中,函数也是对象,因此它有一些属性:
name
name属性是函数名:
function func1(){
}
console.log(func1.name);
// func1
匿名函数也可以获取到正确的函数名:
let func2 = function(){
}
console.log(func2.name);
// func2
对象中的函数(方法)也可以获取到函数名:
let person = {
func1: ()=>{},
func2: function(){},
}
console.log(person.func1.name);
console.log(person.func2.name);
// func1
// func2
特殊情况下也可能获取不到函数名,比如:
let arr = [function(){}];
console.log(arr[0].name);
//
length
函数的length属性表示函数的参数个数:
function func3(){}
function func4(num1,num2){}
function func5(num1, num2, rest){}
console.log(func3.length);
console.log(func4.length);
console.log(func5.length);
// 0
// 2
// 2
自定义属性
可以为函数添加自定义属性:
function countFunc(){
console.log(countFunc.count);
countFunc.count++;
}
countFunc.count = 0;
countFunc();
countFunc();
countFunc();
// 0
// 1
// 2
命名函数表达式
有时候,会在使用函数表达式时涉及函数的自调用问题:
let func6 = function (param) {
if (param) {
console.log(param);
}
else {
func6("Tom");
}
};
func6();
// Tom
一般而言这样做没有什么问题,但有时候会因为外部的函数变量的改变而出错:
let func7 = function (param) {
if (param) {
console.log(param);
}
else {
func7("Tom");
}
};
let func8 = func7;
func7 = null;
func8();
// TypeError: func7 is not a function
在这里匿名函数内部使用外部变量名func7,而该变量在之后被重新赋值为null,因此这里会报错。
为了解决这个问题,我们需要为匿名函数提供一个可靠的名字:
let func7 = function funcSelf(param) {
if (param) {
console.log(param);
}
else {
funcSelf("Tom");
}
};
let func8 = func7;
func7 = null;
func8();
这里为函数表达式添加了一个名字funcSelf,这个名字仅能在函数表达式内部使用。现在函数表达式的自调用不再依赖于外部变量,因此外部变量被重新赋值后也不会影响函数表达式本身的调用。
new Function
有一种很少使用的创建函数的方式:
let sum = new Function('a', 'b', 'return a + b');
console.log(sum(1, 2));
// 3
还可以使用变长参数:
let funcContent = `
if(!arr){
return 0;
}
let total = 0;
for(num of arr){
total += num;
}
return total;
`;
let sumArr = new Function('...arr', funcContent);
console.log(sumArr(1, 2, 3, 4, 5));
// 15
利用这个功能,我们可以从其它地方获取 JS 代码并用这些代码创建函数。
通过这种方式创建的函数不能访问外部变量:
function test(){
let a = 1;
let codeFunc = new Function('return a + 1');
return codeFunc();
}
console.log(test());
// 报错
普通定义的函数不存在这个问题:
function test(){
let a = 1;
let codeFunc = function(){
return a + 1;
};
return codeFunc();
}
console.log(test());
// 2
延迟执行
setTimeout
使用setTimeout可以在指定一段时间后执行某个函数:
function showTime(){
console.log(Date.now());
}
setTimeout(showTime, 1000);
如果函数需要参数:
function sum2(a, b) {
console.log(a + b);
}
setTimeout(sum2, 1000, 1, 2);
// 3
更常见的是使用箭头函数:
setTimeout(() => {
console.log(Date.now());
}, 1000);
clearTimeout
setTimeout会返回一个标识,使用这个标识可以取消:
let timeout = setTimeout(() => console.log("hello"), 1000);
clearTimeout(timeout);
这里代码不会被执行。
setInterval
setInterval可以周期性执行代码:
let timeShowSchedule = setInterval(() => {
console.log(Date.now());
}, 1000);
setTimeout(() => clearInterval(timeShowSchedule), 5000);
这里每1秒会打印一次时间,在5秒后停止。
嵌套 setTimeout
使用嵌套 setTimeout 可以起到和 setInterval 相同的效果:
setTimeout(function showTime(){
console.log(formatTime(Date.now()));
setTimeout(showTime, 1000);
}, 1000);
这里在内部的setTimeout中调用了外部setTimeout使用的命名函数showTime,因此形成了一种循环调用,最终效果和setInterval类似。
不同的是,嵌套setTimeout有更高的灵活性,可以按需要动态调整之后调用的延迟时间,比如:
function pingServer(){
// 模拟测试服务器是否存活
return Math.random() > 0.5;
}
setTimeout(function ping(delayN = 1){
if(pingServer()){
console.log('服务器正常');
delayN = 1;
}else{
console.log('服务器异常');
delayN *= 2;
console.log(`延迟${delayN}秒重试`);
}
setTimeout(ping, 1000*delayN, delayN);
}, 1000)
如果服务端正常,每1秒检查一次服务器状态,如果服务器异常,每次检查时间延长2倍时间。
装饰器和转发
装饰器
// 计算斐波那契数列
function fib(n) {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
console.log('fib(10)', fib(10));
console.log('fib(40)', fib(40));
console.log('fib(40)', fib(40));
console.log('fib(40)', fib(40));
示例展示的是一个计算斐波那契数列的函数,这个函数计算时间较长,即使连续调用的n相同,也需要消耗较长时间。
我们可以为这个函数添加缓存以优化执行性能:
let cache = new Map();
function cachedFib(n) {
if (cache.has(n)) {
return cache.get(n);
}
let result = fib(n);
cache.set(n, result);
return result;
}
console.log('cachedFib(40)', cachedFib(40));
console.log('cachedFib(40)', cachedFib(40));
console.log('cachedFib(40)', cachedFib(40));
可以看到,连续调用同一个n时,执行效率被提高了。
但这样做有两个缺陷:
-
缓存逻辑与函数本身的逻辑混杂在一起,代码结构不清晰,不符合设计模式中的单一职责原则。
-
代码无法复用,需要为每个业务函数编写缓存逻辑。
可以使用装饰器模式:
function cacheFunc(fn) {
let cache = new Map();
return function (args) {
let key = args.join(',');
if (cache.has(key)) {
return cache.get(key);
}
let result = fn(args);
cache.set(key, result);
return result;
}
}
let cachedFib2 = cacheFunc(fib);
console.log('cachedFib2(40)', cachedFib2(40));
console.log('cachedFib2(40)', cachedFib2(40));
console.log('cachedFib2(40)', cachedFib2(40));
Func.call
对于对象方法,之前的做法可能出错,比如:
let myMath = {
tag: 'myMath',
fib: function (n) {
console.log(`${this.tag} fib(${n})`);
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
}
console.log(myMath.fib(10));
let cachedFib3 = cacheFunc(myMath.fib);
// myMath fib(10)
// 55
// undefined fib(10)
// 55
第二次输出undefined,因为被缓存的方法被执行时,JS 找不到关联的对象(this 指针为 undefined)。JS 提供一个func.call方法用于在执行对象方法时,显式指定方法关联的对象,比如:
function cacheObjFunc(obj, fn) {
let cache = new Map();
return function (args) {
let key = args.join(',');
if (cache.has(key)) {
return cache.get(key);
}
let result = fn.call(obj, args);
cache.set(key, result);
return result;
}
}
let cachedFib4 = cacheObjFunc(myMath, myMath.fib);
console.log(cachedFib4(10))
// myMath fib(10)
// 55
函数绑定
之前有提到过,如果传递对象方法,可能出现this丢失的问题:
let user2 = {
name: 'Jack',
hello() {
console.log(`hello ${this.name}`);
}
};
user2.hello();
setTimeout(user2.hello, 1000);
// hello undefined
第一种解决方式是使用包装函数:
setTimeout(function(){
user2.hello();
},1000);
// hello Jack
第二种解决方式是使用绑定函数:
setTimeout(user2.hello.bind(user2), 1000);
// hello Jack
func.bind返回的函数是将对象与函数绑定后的函数,因此现在的this不再是 undefined。
绑定参数
除了常见的绑定this以外,还可以绑定参数,比如:
function multiply(a, b) {
return a * b;
}
let double = multiply.bind(null, 2);
console.log(double(1));
console.log(double(2));
console.log(double(3));
// 2
// 4
// 6
就像示例中展示的,使用bind可以利用已有函数快速构建新的方法。需要注意的是,虽然这里绑定的是普通函数,没有关联的对象,但依然需要为bind函数指定一个对象参数作为第一个参数,所以这里使用null。
不指定上下文
如果在函数调用时,需要固定一部分参数,但不固定上下文的话,可以:
function partial(fn, args) {
return function (rest) {
return fn.call(this, args, rest);
}
}
let user3 = {
name: 'Jack',
hello(prefix, suffix) {
console.log(`${prefix} ${this.name} ${suffix}`);
}
}
user3.hello2 = partial(user3.hello, 'hello');
user3.hello2('!');
// hello Jack !
partial返回的函数中调用时使用的是this,因此user3.hello2调用时关联的上下文(this)就是user3。
箭头函数
this
let names = {
title: 'Mr',
names: ['Tom', 'Jerry'],
hello() {
this.names.forEach(function (name) {
console.log(`hello, ${this.title} ${name}`);
})
}
}
names.hello();
// hello, undefined Tom
// hello, undefined Jerry
在这个示例中,this是匿名函数的this,并没有指向外部的对象names,因此这里输出是undefined。
当然可以直接使用:
console.log(`hello, ${names.title} ${name}`);
如果你习惯于使用this,可以:
let names = {
title: 'Mr',
names: ['Tom', 'Jerry'],
hello() {
this.names.forEach((name) => {
console.log(`hello, ${this.title} ${name}`);
})
}
}
names.hello();
// hello, Mr Tom
// hello, Mr Jerry
这里用箭头函数代替普通匿名函数,区别在于箭头函数没有自己的this,因此这里箭头函数内使用this时,程序将在外部查找this,也就是names对象。
arguments
function delayCall(fn, delay){
return function (){
console.log('call function after delay time');
setTimeout(()=>{
fn.apply(this, arguments);
}, delay);
}
}
function sayHello(name) {
console.log(`hello ${name}`);
}
delayCall(sayHello, 1000)('Tom');
在这个示例中,delayCall返回一个包装函数,它将目标函数进行包装,以在指定延时后调用。示例中的包装函数中setTimeout接收的是箭头函数,并使用特殊的arguments变量获取包装函数实际执行时接收的参数。之所以可以这样,是因为箭头函数没有arguments这个特殊变量,因此arguments是从外部(包装函数)获取的。
如果不是箭头函数而是普通函数:
function delayCall2(fn, delay){
return function (args){
console.log('call function after delay time');
let ctx = this;
setTimeout(function () {
fn.apply(ctx, args);
}, delay);
}
}
delayCall2(sayHello, 1000)('Tom');
这里就必须让包装函数显式接收参数(...args),并使用这个参数进行调用。
The End.
本文完整的示例代码可以从
参考资料

文章评论