10 JavaScript函数

ECMAScript函数实际上是对象。每个函数都是Function类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。函数通常以函数声明的方式定义,比如:

function sum (num1, num2) { 
 	return num1 + num2; 
} 

注意函数定义最后没有加分号。

另一种定义函数的语法是函数表达式。函数表达式与函数声明几乎是等价的:

let sum = function(num1, num2) { 
 	return num1 + num2; 
}; 

这里,代码定义了一个变量 sum 并将其初始化为一个函数。注意 function 关键字后面没有名称,因为不需要。这个函数可以通过变量 sum 来引用。

注意这里的函数末尾是有分号的,与任何变量初始化语句一样。

还有一种定义函数的方式与函数表达式很像,叫作“箭头函数”(arrow function),如下所示:

let sum = (num1, num2) => { 
 	return num1 + num2; 
}; 

最后一种定义函数的方式是使用 Function 构造函数。这个构造函数接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的参数都是新函数的参数。来看下面的例子:

let sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐

不推荐使用这种语法来定义函数,因为这段代码会被解释两次:第一次是将它当作常规ECMAScript 代码,第二次是解释传给构造函数的字符串。这显然会影响性能。

1 箭头函数

ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数:

let arrowSum = (a, b) => { 
 	return a + b; 
}; 

let functionExpressionSum = function(a, b) { 
 	return a + b; 
}; 

console.log(arrowSum(5, 8)); // 13 
console.log(functionExpressionSum(5, 8)); // 13

箭头函数简洁的语法非常适合嵌入函数的场景:

let ints = [1, 2, 3]; 

console.log(ints.map(function(i) { return i + 1; })); // [2, 3, 4] 
console.log(ints.map((i) => { return i + 1 })); // [2, 3, 4] 

如果只有一个参数,那也可以不用括号。只有没有参数,或者多个参数的情况下,才需要使用括号:

// 以下两种写法都有效
let double = (x) => { return 2 * x; }; 
let triple = x => { return 3 * x; }; 

// 没有参数需要括号
let getRandom = () => { return Math.random(); }; 

// 多个参数需要括号
let sum = (a, b) => { return a + b; }; 

// 无效的写法:
let multiply = a, b => { return a * b; }; 

箭头函数也可以不用大括号,但这样会改变函数的行为。使用大括号就说明包含“函数体”,可以在一个函数中包含多条语句,跟常规的函数一样。如果不使用大括号,那么箭头后面就只能有一行代码,比如一个赋值操作,或者一个表达式。而且,省略大括号会隐式返回这行代码的值:

// 以下两种写法都有效,而且返回相应的值
let double = (x) => { return 2 * x; }; 
let triple = (x) => 3 * x; 

// 可以赋值
let value = {}; 
let setName = (x) => x.name = "Matt"; 
setName(value); 
console.log(value.name); // "Matt" 

// 无效的写法:
let multiply = (a, b) => return a * b; 

待补充 下面不能用的都是啥

箭头函数虽然语法简洁,但也有很多场合不适用。箭头函数不能使用 arguments、super 和 new.target,也不能用作构造函数。此外,箭头函数也没有 prototype 属性。

2 函数名

因为函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。这意味着一个函数可以有多个名称,如下所示:

function sum(num1, num2) { 
 	return num1 + num2; 
} 

console.log(sum(10, 10)); // 20 

let anotherSum = sum; 
console.log(anotherSum(10, 10)); // 20 

sum = null; 
console.log(anotherSum(10, 10)); // 20 

以上代码定义了一个名为 sum()的函数,用于求两个数之和。然后又声明了一个变量 anotherSum,并将它的值设置为等于 sum。注意,使用不带括号的函数名会访问函数指针,而不会执行函数。此时,anotherSumsum 都指向同一个函数。调用 anotherSum()也可以返回结果。把 sum 设置为 null 之后,就切断了它与函数之间的关联。而 anotherSum()还是可以照常调用,没有问题。

ECMAScript 6 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称,也会如实显示成空字符串。如果它是使用 Function 构造函数创建的,则会标识成"anonymous":

function foo() {} 
let bar = function() {}; 
let baz = () => {}; 

console.log(foo.name); // foo 
console.log(bar.name); // bar 
console.log(baz.name); // baz 
console.log((() => {}).name); //(空字符串)
console.log((new Function()).name); // anonymous

如果函数是一个获取函数、设置函数,或者使用 bind()实例化,那么标识符前面会加上一个前缀:

function foo() {} 

console.log(foo.bind(null).name); 	// bound foo 

let dog = { 
 	years: 1, 
 	get age() { 
 		return this.years; 
 	}, 
 	set age(newAge) { 
 		this.years = newAge; 
 	} 
} 

let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age'); 
console.log(propertyDescriptor.get.name); 			// get age 
console.log(propertyDescriptor.set.name); 			// set age

待补充,上面的getownpropertydescriptor,get,set函数

3 理解参数

ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一个、三个,甚至一个也不传,解释器都不会报错。

这是因为 ECMAScript 函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么。如果数组中什么也没有,那没问题;如果数组的元素超出了要求,那也没问题。事实上,在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问 arguments 对象,从中取得传进来的每个参数值。

arguments 对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的元素(第一个参数是 arguments[0],第二个参数是 arguments[1])。而要确定传进来多少个参数,可以访问 arguments.length 属性。

在下面的例子中,sayHi()函数的第一个参数叫 name:

function sayHi(name, message) { 
 	console.log("Hello " + name + ", " + message); 
} 

可以通过 arguments[0]取得相同的参数值。因此,把函数重写成不声明参数也可以:

function sayHi() { 
 	console.log("Hello " + arguments[0] + ", " + arguments[1]); 
} 

在重写后的代码中,没有命名参数。name 和 message 参数都不见了,但函数照样可以调用。这就表明,ECMAScript 函数的参数只是为了方便才写出来的,并不是必须写出来的。与其他语言不同,在ECMAScript 中的命名参数不会创建让之后的调用必须匹配的函数签名。这是因为根本不存在验证命名参数的机制。

也可以通过 arguments 对象的 length 属性检查传入的参数个数。下面的例子展示了在每调用一个函数时,都会打印出传入的参数个数:

function howManyArgs() { 
 	console.log(arguments.length); 
} 

howManyArgs("string", 45); // 2 
howManyArgs(); // 0 
howManyArgs(12); // 1 

这个例子分别打印出 2、0 和 1(按顺序)。既然如此,那么开发者可以想传多少参数就传多少参数。

还有一个必须理解的重要方面,那就是 arguments 对象可以跟命名参数一起使用,比如:

function doAdd(num1, num2) { 
 	if (arguments.length === 1) { 
 		console.log(num1 + 10); 
 	} else if (arguments.length === 2) { 
 		console.log(arguments[0] + num2); 
 	} 
} 

在这个 doAdd()函数中,同时使用了两个命名参数和 arguments 对象。命名参数 num1 保存着与arugments[0]一样的值,因此使用谁都无所谓。(同样,num2 也保存着跟 arguments[1]一样的值。

arguments 对象的另一个有意思的地方就是,它的值始终会与对应的命名参数同步。来看下面的例子:

function doAdd(num1, num2) { 
 	arguments[1] = 10; 
 	console.log(arguments[0] + num2); 
} 

待补充,可以看下QuickJS里面是怎么实现这个同步的。

这个 doAdd()函数把第二个参数的值重写为 10。因为 arguments 对象的值会自动同步到对应的命名参数,所以修改 arguments[1]也会修改 num2 的值,因此两者的值都是 10。但这并不意味着它们都访问同一个内存地址,它们在内存中还是分开的,只不过会保持同步而已。另外还要记住一点:如果只传了一个参数,然后把 arguments[1]设置为某个值,那么这个值并不会反映到第二个命名参数。这是因为 arguments 对象的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数确定的。

对于命名参数而言,如果调用函数时没有传这个参数,那么它的值就是 undefined。这就类似于定义了变量而没有初始化。比如,如果只给 doAdd()传了一个参数,那么 num2 的值就是 undefined。

严格模式下,arguments 会有一些变化。首先,像前面那样给 arguments[1]赋值不会再影响 num2的值。就算把 arguments[1]设置为 10,num2 的值仍然还是传入的值。其次,在函数中尝试重写arguments 对象会导致语法错误。(代码也不会执行。)

3.1 箭头函数中的参数

如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用 arguments 关键字访问,而只能通过定义的命名参数访问。

function foo() { 
 	console.log(arguments[0]); 
} 
foo(5); // 5 

let bar = () => { 
 	console.log(arguments[0]); 
}; 
bar(5); // ReferenceError: arguments is not defined 

待补充 包装函数是什么

虽然箭头函数中没有 arguments 对象,但可以在包装函数中把它提供给箭头函数:

function foo() { 
 	let bar = () => { 
 		console.log(arguments[0]); // 5 
 	}; 
 	bar(); 
} 

foo(5);

注意 ECMAScript 中的所有参数都按值传递的。不可能按引用传递参数。如果把对象作为参数传递,那么传递的值就是这个对象的引用。

4 没有重载

ECMAScript 函数不能像传统编程那样重载。在其他语言比如 Java 中,一个函数可以有两个定义,只要签名(接收参数的类型和数量)不同就行。如前所述,ECMAScript 函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载。

如果在 ECMAScript 中定义了两个同名函数,则后定义的会覆盖先定义的。来看下面的例子:

function addSomeNumber(num) { 
 	return num + 100; 
} 

function addSomeNumber(num) { 
 	return num + 200; 
} 

let result = addSomeNumber(100); // 300

这里,函数 addSomeNumber()被定义了两次。第一个版本给参数加 100,第二个版本加 200。最后一行调用这个函数时,返回了 300,因为第二个定义覆盖了第一个定义。

前面也提到过,可以通过检查参数的类型和数量,然后分别执行不同的逻辑来模拟函数重载。

把函数名当成指针也有助于理解为什么 ECMAScript 没有函数重载。在前面的例子中,定义两个同名的函数显然会导致后定义的重写先定义的。而那个例子几乎跟下面这个是一样的:

let addSomeNumber = function(num) { 
 	return num + 100; 
}; 

addSomeNumber = function(num) { 
 	return num + 200; 
}; 

let result = addSomeNumber(100); // 300 

看这段代码应该更容易理解发生了什么。在创建第二个函数时,变量 addSomeNumber 被重写成保存第二个函数对象了。

5 默认参数值

在 ECMAScript5.1 及以前,实现默认参数的一种常用方式就是检测某个参数是否等于 undefined,如果是则意味着没有传这个参数,那就给它赋一个值:

待补充 typeof是什么,以及反引号又代表什么

function makeKing(name) { 
 	name = (typeof name !== 'undefined') ? name : 'Henry'; 
 	return `King ${name} VIII`; 
} 

console.log(makeKing()); // 'King Henry VIII' 
console.log(makeKing('Louis')); // 'King Louis VIII' 

ECMAScript 6 之后就不用这么麻烦了,因为它支持显式定义默认参数了。下面就是与前面代码等价的 ES6 写法,只要在函数定义中的参数后面用=就可以为参数赋一个默认值:

function makeKing(name = 'Henry') { 
 	return `King ${name} VIII`; 
} 

console.log(makeKing('Louis')); // 'King Louis VIII' 
console.log(makeKing()); // 'King Henry VIII'

给参数传 undefined 相当于没有传值,不过这样可以利用多个独立的默认值:

function makeKing(name = 'Henry', numerals = 'VIII') { 
 	return `King ${name} ${numerals}`; 
} 

console.log(makeKing()); // 'King Henry VIII' 
console.log(makeKing('Louis')); // 'King Louis VIII' 
console.log(makeKing(undefined, 'VI')); // 'King Henry VI' 

在使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数。当然,跟 ES5 严格模式一样,修改命名参数也不会影响 arguments 对象,它始终以调用函数时传入的值为准:

function makeKing(name = 'Henry') { 
 	name = 'Louis'; 
 	return `King ${arguments[0]}`; 
} 

console.log(makeKing()); // 'King undefined' 
console.log(makeKing('Louis')); // 'King Louis'

默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值:

let romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI']; 
let ordinality = 0; 
function getNumerals() { 
 	// 每次调用后递增
 	return romanNumerals[ordinality++]; 
} 

function makeKing(name = 'Henry', numerals = getNumerals()) { 
 	return `King ${name} ${numerals}`; 
} 

console.log(makeKing()); // 'King Henry I'
console.log(makeKing('Louis', 'XVI')); // 'King Louis XVI' 
console.log(makeKing()); // 'King Henry II' 
console.log(makeKing()); // 'King Henry III'

函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。而且,计算默认值的函数只有在调用函数但未传相应参数时才会被调用。

箭头函数同样也可以这样使用默认参数,只不过在只有一个参数时,就必须使用括号而不能省略了:

let makeKing = (name = 'Henry') => `King ${name}`; 

console.log(makeKing()); // King Henry

待补充 319

6 参数扩展与收集

ECMAScript 6 新增了扩展操作符,使用它可以非常简洁地操作和组合集合数据。扩展操作符最有用的场景就是函数定义中的参数列表,在这里它可以充分利用这门语言的弱类型及参数长度可变的特点。扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数。

6.1 扩展参数

在给函数传参时,有时候可能不需要传一个数组,而是要分别传入数组的元素。

假设有如下函数定义,它会将所有传入的参数累加起来:

let values = [1, 2, 3, 4]; 

function getSum() { 
 	let sum = 0; 
 	for (let i = 0; i < arguments.length; ++i) { 
 		sum += arguments[i]; 
 	} 
 	return sum; 
} 

这个函数希望将所有加数逐个传进来,然后通过迭代 arguments 对象来实现累加。如果不使用扩展操作符,想把定义在这个函数这面的数组拆分,那么就得求助于 apply()方法:

待补充 这个apply又是什么

console.log(getSum.apply(null, values)); // 10

但在 ECMAScript 6 中,可以通过扩展操作符极为简洁地实现这种操作。对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。

比如,使用扩展操作符可以将前面例子中的数组像这样直接传给函数:

console.log(getSum(...values)); // 10

因为数组的长度已知,所以在使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值,包括使用扩展操作符传其他参数:

console.log(getSum(-1, ...values)); // 9 
console.log(getSum(...values, 5)); // 15 
console.log(getSum(-1, ...values, 5)); // 14 
console.log(getSum(...values, ...[5,6,7])); // 28

对函数中的 arguments 对象而言,它并不知道扩展操作符的存在,而是按照调用函数时传入的参数接收每一个值:

let values = [1,2,3,4] 

function countArguments() { 
 	console.log(arguments.length); 
} 

countArguments(-1, ...values); // 5 
countArguments(...values, 5); // 5 
countArguments(-1, ...values, 5); // 6 
countArguments(...values, ...[5,6,7]); // 7 

arguments 对象只是消费扩展操作符的一种方式。在普通函数和箭头函数中,也可以将扩展操作符用于命名参数,当然同时也可以使用默认参数:

function getProduct(a, b, c = 1) { 
 	return a * b * c; 
} 

let getSum = (a, b, c = 0) => { 
 	return a + b + c; 
} 

console.log(getProduct(...[1,2])); // 2 
console.log(getProduct(...[1,2,3])); // 6 
console.log(getProduct(...[1,2,3,4])); // 6
 
console.log(getSum(...[0,1])); // 1 
console.log(getSum(...[0,1,2])); // 3 
console.log(getSum(...[0,1,2,3])); // 3 
6.2 收集参数

在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组。这有点类似arguments 对象的构造机制,只不过收集参数的结果会得到一个 Array 实例。

待补充 321

7 函数声明与函数表达式

JavaScript 引擎在加载数据时对函数声明和函数表达式是区别对待的。JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。下面是一个例子:

// 没问题 
console.log(sum(10, 10)); 
function sum(num1, num2) { 
 	return num1 + num2; 
} 

以上代码可以正常运行,因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)。在执行代码时,JavaScript 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。如果把前面代码中的函数声明改为等价的函数表达式,那么执行的时候就会出错:

// 会出错
console.log(sum(10, 10)); 
let sum = function(num1, num2) { 
 	return num1 + num2; 
}; 

上面的代码之所以会出错,是因为这个函数定义包含在一个变量初始化语句中,而不是函数声明中。这意味着代码如果没有执行到加粗的那一行,那么执行上下文中就没有函数的定义,所以上面的代码会出错。这并不是因为使用 let 而导致的,使用 var 关键字也会碰到同样的问题:

console.log(sum(10, 10)); 
var sum = function(num1, num2) { 
 	return num1 + num2; 
};

除了函数什么时候真正有定义这个区别之外,这两种语法是等价的。

注意: 在使用函数表达式初始化变量时,也可以给函数一个名称,比如 let sum = function sum() {}。

8 函数作为值

因为函数名在 ECMAScript 中就是变量,所以函数可以用在任何可以使用变量的地方。这意味着不仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数。来看下面的例子:

9 函数内部

在 ECMAScript 5 中,函数内部存在两个特殊的对象:arguments 和 this。ECMAScript 6 又新增了 new.target 属性。

9.1 arguments

arguments 是一个类数组对象,包含调用函数时传入的所有参数。这个对象只有以 function 关键字定义函数(相对于使用箭头语法创建函数)时才会有。虽然主要用于包含函数参数,但 arguments 对象还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。以如下阶乘函数为例:

function factorial(num) { 
 	if (num <= 1) { 
 		return 1; 
 	} else { 
 		return num * factorial(num - 1); 
 	} 
} 

阶乘函数一般定义成递归调用的,只要给函数一个名称,而且这个名称不会变,这样定义就没有问题。但是,这个函数要正确执行就必须保证函数名是 factorial,从而导致了紧密耦合。使用 arguments.callee 就可以让函数逻辑与函数名解耦:

function factorial(num) { 
 	if (num <= 1) { 
 		return 1; 
 	} else { 
 		return num * arguments.callee(num - 1); 
 	} 
} 

这个重写之后的 factorial()函数已经用 arguments.callee 代替了之前硬编码的 factorial。这意味着无论函数叫什么名称,都可以引用正确的函数。考虑下面的情况:

function factorial(num){
  if(num<=1){
    return 1;
  }
  else{
    return num*arguments.callee(num-1);
  }
}

let trueFactorial=factorial;

factorial=function(){
  return 0;
};

console.log(trueFactorial(5));
console.log(factorial(5));

这里,trueFactorial 变量被赋值为 factorial,实际上把同一个函数的指针又保存到了另一个位置。然后,factorial 函数又被重写为一个返回 0 的函数。如果像 factorial()最初的版本那样不使用 arguments.callee,那么像上面这样调用 trueFactorial()就会返回 0。不过,通过将函数与名称解耦,trueFactorial()就可以正确计算阶乘,而 factorial()则只能返回 0。

9.2 this

this在标准函数和箭头函数中有不同的行为。

在标准函数中,this 引用的是把函数当成方法调用的上下文对象(例如在网页的全局上下文中调用函数时,this 指向 windows)。一个例子如下:

window.color = 'red'; 

let o = { 
 	color: 'blue' 
}; 

function sayColor() { 
 	console.log(this.color); 
} 

sayColor(); // 'red' 

o.sayColor = sayColor; 
o.sayColor(); // 'blue' 

如果在全局上下文中调用sayColor(),这结果会输出"red",因为 this 指向 window,此时 this.color 相当于 window.color。而在把 sayColor()赋值给 o 之后再调用 o.sayColor(),this 会指向 o,此时 this.color 相当于 o.color,所以会显示"blue"。

在箭头函数中,this 引用的是定义箭头函数的上下文。下面的例子演示了这一点,在对sayColor()的两次调用中,this 引用的都是 window 对象,因为这个箭头函数是在 window 上下文中定义的:

window.color = 'red'; 

let o = { 
 	color: 'blue' 
}; 

let sayColor = () => console.log(this.color); 

sayColor(); // 'red' 

o.sayColor = sayColor; 
o.sayColor(); // 'red' 

待补充 回调

注意,在事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题。这是因为箭头函数中的 this 会保留定义该函数时的上下文:

function King() { 
 this.royaltyName = 'Henry'; 
 // this 引用 King 的实例
 setTimeout(() => console.log(this.royaltyName), 1000); 
} 

function Queen() { 
 this.royaltyName = 'Elizabeth'; 
 // this 引用 window 对象
 setTimeout(function() { console.log(this.royaltyName); }, 1000); 
} 

new King(); // Henry 
new Queen(); // undefined

注意:函数名只是保存指针的变量。因此全局定义的 sayColor()函数和 o.sayColor()是同一个函数,只不过执行的上下文不同。

10 FUNCTION PROPERTIES AND METHODS

Functions are objects in ECMAScript and, as mentioned previously, therefore have properties and methods. Each function has two properties: length and prototype. The length property indicates the number of named arguments that the function expects, as in this example:

function sayName(name) { 
 	console.log(name); 
} 

function sum(num1, num2) { 
 	return num1 + num2; 
} 

function sayHi() { 
 	console.log("hi"); 
} 

console.log(sayName.length); // 1 
console.log(sum.length); // 2 
console.log(sayHi.length); // 0 

This code defines three functions, each with a different number of named arguments. The sayName() function specifies one argument, so its length property is set to 1. Similarly, the sum() function specifies two arguments, so its length property is 2, and sayHi() has no named arguments, so its length is 0.

The prototype property is perhaps the most interesting part of the ECMAScript core. The prototype is the actual location of all instance methods for reference types, meaning methods such as toString() and valueOf() actually exist on the prototype and are then accessed from the object instances. This property is very important in terms of defining your own reference types and inheritance. (These topics are covered in the chapter “Objects, Classes, and Object-Oriented Programming.”) In ECMAScript 5, the prototype property is not enumerable and so will not be found using for-in.

There are two additional methods for functions: apply() and call(). These methods both call the function with a specific this value, effectively setting the value of the this object inside the function body. The apply() method accepts two arguments: the value of this inside the function and an array of arguments. This second argument may be an instance of Array, but it can also be the arguments object. Consider the following:

function sum(num1, num2) { 
 	return num1 + num2; 
} 

function callSum1(num1, num2) { 
 	return sum.apply(this, arguments); // 传入 arguments 对象
} 

function callSum2(num1, num2) { 
 	return sum.apply(this, [num1, num2]); // 传入数组
} 

console.log(callSum1(10, 10)); // 20 
console.log(callSum2(10, 10)); // 20 

In this example, callSum1() executes the sum() method, passing in this as the this value (which is equal to window because it’s being called in the global scope) and also passing in the arguments object. The callSum2() method also calls sum(), but it passes in an array of the arguments instead. Both functions will execute and return the correct result.

NOTE In strict mode, the this value of a function called without a context object is not coerced to window. Instead, this becomes undefined unless explicitly set by either attaching the function to an object or using apply() or call().

The call() method exhibits the same behavior as apply(), but arguments are passed to it differently. The first argument is the this value, but the remaining arguments are passed directly into the function. Using call() arguments must be enumerated specifically, as in this example:

function sum(num1, num2) { 
 	return num1 + num2; 
} 

function callSum(num1, num2) { 
 	return sum.call(this, num1, num2); 
} 

console.log(callSum(10, 10)); // 20

The callSum() method must pass in each of its arguments explicitly into the call() method. The result is the same as using apply(). The decision to use either apply() or call() depends solely on the easiest way for you to pass arguments into the function. If you intend to pass in the arguments object directly or if you already have an array of data to pass in, then apply() is the better choice; otherwise, call() may be a more appropriate choice. (If there are no arguments to pass in, these
methods are identical.)

The true power of apply() and call() lies not in their ability to pass arguments but rather in their ability to augment the this value inside of the function. Consider the following example:

window.color = 'red'; 

let o = { 
 	color: 'blue' 
}; 

function sayColor() { 
 	console.log(this.color); 
} 

sayColor(); // red 
sayColor.call(this); // red 
sayColor.call(window); // red 
sayColor.call(o); // blue 

This example is a modified version of the one used to illustrate the this object. The original example is as followed:

window.color = 'red';
let o = { 
 	color: 'blue'
};
 
function sayColor() {
 	console.log(this.color);
}
 
sayColor(); // 'red'
 
o.sayColor = sayColor;
o.sayColor(); // 'blue'

Once again, sayColor() is defined as a global function, and when it’s called in the global scope, it displays "red" because this.color evaluates to window.color. You can then call the function explicitly in the global scope by using sayColor.call(this) and sayColor.call(window), which both display "red". Running sayColor.call(o) switches the context of the function such that this points to o, resulting in a display of "blue".

The advantage of using call() (or apply()) to augment the scope is that the object doesn’t need to know anything about the method. In the first version of this example, the sayColor() function was placed directly on the object o before it was called; in the updated example, that step is no longer necessary.

ECMAScript 5 defines an additional method called bind(). The bind() method creates a new function instance whose this value is bound to the value that was passed into bind(). For example:

window.color = 'red'; 
var o = { 
 	color: 'blue' 
}; 

function sayColor() { 
 	console.log(this.color); 
} 
let objectSayColor = sayColor.bind(o); 
objectSayColor(); 		// blue 

Here, a new function called objectSayColor() is created from sayColor() by calling bind() and passing in the object o. The objectSayColor() function has a this value equivalent to o, so calling the function, even as a global call, results in the string "blue" being displayed.

For functions, the inherited methods toLocaleString() and toString() always return the function’s code. The exact format of this code varies from browser to browser—some return your code exactly as it appeared in the source code, including comments, whereas others return the internal representation of your code, which has comments removed and possibly some code changes that the interpreter made. Because of these differences, you can’t rely on what is returned for any important functionality, though this information may be useful for debugging purposes. The inherited method valueOf() simply returns the function itself.

11 函数表达式

定义函数有两种方式:函数声明和函数表达式。如下是函数声明:

function functionName(arg0, arg1, arg2) { 
 	// 函数体 
} 

函数声明的关键特点是函数声明提升,即函数声明会在代码执行之前获得定义。这意味着函数声明可以出现在调用它的代码之后:

sayHi(); 
function sayHi() { 
 	console.log("Hi!"); 
} 

这个例子不会抛出错误,因为 JavaScript 引擎会先读取函数声明,然后再执行代码。

第二种创建函数的方式就是函数表达式。函数表达式有几种不同的形式,最常见的是这样的:

let functionName = function(arg0, arg1, arg2) { 
 	// 函数体 
}; 

函数表达式看起来就像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量functionName。这样创建的函数叫作匿名函数(anonymous funtion),因为 function 关键字后面没有标识符。(匿名函数有也时候也被称为lambda函数)。未赋值给其他变量的匿名函数的 name 属性是空字符串。

函数表达式跟 JavaScript 中的其他表达式一样,需要先赋值再使用。下面的例子会导致错误:

sayHi(); // Error! function doesn't exist yet 
let sayHi = function() { 
 	console.log("Hi!"); 
};

函数声明与函数表达式的主要区别是提升。比如,以下代码的执行结果可能会出乎意料:

if (condition) { 
 	function sayHi() { 
 		console.log('Hi!'); 
 	} 
} else { 
 	function sayHi() { 
 		console.log('Yo!'); 
 	} 
} 

上述代码看起来很正常,但这不是ECMAScript中的有效语法。JavaScript 引擎会尝试将其纠正为适当的声明,但不同引擎纠正这个问题的方式并不一致。多数引擎会忽略 condition 直接返回第二个声明。Firefox 会在 condition 为 true 时返回第一个声明。这种写法很危险,不要使用。不过,如果把上面的函数声明换成函数表达式就没问题了:

let sayHi; 
if (condition) { 
 	sayHi = function() { 
 		console.log("Hi!"); 
 	}; 
} else { 
 	sayHi = function() { 
 		console.log("Yo!"); 
 	}; 
}

创建函数并赋值给变量的能力也可以用于在一个函数中把另一个函数当作值返回:

function createComparisonFunction(propertyName) { 
 	return function(object1, object2) { 
 		let value1 = object1[propertyName]; 
 		let value2 = object2[propertyName];
 		 
 		if (value1 < value2) { 
 			return -1; 
 		} else if (value1 > value2) { 
 			return 1; 
 		} else { 
 			return 0; 
 		} 
 	}; 
} 

这里的 createComparisonFunction() 函数返回一个匿名函数,这个匿名函数要么被赋值给一个变量,要么可以直接调用。但在 createComparisonFunction() 内部,那个函数是匿名的。任何时候,只要函数被当作值来使用,它就是一个函数表达式。

14 闭包

Closures are functions that have access to variables from another function’s scope. This is often accomplished by creating a function inside a function, as in the following highlighted lines from the previous
createComparisonFunction() example:

function createComparisonFunction(propertyName) { 
 	return function(object1, object2) { 
 		//注意观察下面的语句
 		let value1 = object1[propertyName]; 
 		let value2 = object2[propertyName]; 
 		
 		if (value1 < value2) { 
 			return -1; 
 		} else if (value1 > value2) { 
 			return 1; 
 		} else { 
 			return 0; 
 		} 
 	}; 
} 

The two let in this example are part of the inner function (an anonymous function) that is accessing a variable (propertyName) from the outer function. Even after the inner function has been
returned and is being used elsewhere, it has access to that variable. This occurs because the inner function’s scope chain includes the scope of createComparisonFunction(). To understand why this
is possible, consider what happens when a function is first called.

Variables, Scope, and Memory introduced the concept of a scope chain. The details of
how scope chains are created and used are important for a good understanding of closures. When a
function is called, an execution context is created, and its scope chain is created. The activation object
for the function is initialized with values for arguments and any named arguments. The outer function’s activation object is the second object in the scope chain. This process continues for all containing functions until the scope chain terminates with the global execution context.

函数执行时从作用域链中查找变量,以便读、写值。来看下面的代码:

function compare(value1, value2) { 
 	if (value1 < value2) { 
 		return -1; 
 	} else if (value1 > value2) { 
 		return 1; 
 	} else { 
 		return 0; 
 	} 
} 

let result = compare(5, 10); 

这里定义的 compare()函数是在全局上下文中调用的。第一次调用 compare()时,会为它创建一个包含 arguments、value1 和 value2 的活动对象,这个对象是其作用域链上的第一个对象。而全局上下文的变量对象则是 compare()作用域链上的第二个对象,其中包含 this、result 和 compare。图 10-1 展示了以上关系。

在这里插入图片描述
函数执行时,每个执行上下文中都会有一个包含其中变量的对象:

  • 全局上下文中的叫变量对象,它会在代码执行期间始终存在
  • 局部上下文中的叫活动对象,只在函数执行期间存在。

在定义compare()函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。在这个例子中,这意味着 compare()函数执行上下文的作用域链中有两个变量对象:局部变量对象和全局变量对象。作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。

函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域。不过,闭包就不一样了。

在一个函数内部定义的函数会把其外部函数的活动对象添加到自己的作用域链中。因此,在createComparisonFunction()函数中,匿名函数的作用域链中实际上包含 createComparisonFunction()的活动对象。图 10-2 展示了以下代码执行后的结果:

let compare = createComparisonFunction('name'); 
let result = compare({ name: 'Nicholas' }, { name: 'Matt' });

在这里插入图片描述
在 createComparisonFunction()返回匿名函数后,它的作用域链被初始化为包含 createComparisonFunction()的活动对象和全局变量对象。这样,匿名函数就可以访问到 createComparisonFunction()可以访问的所有变量。一个有意思的副作用是,createComparisonFunction()的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。在 createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁:

// 创建比较函数
let compareNames = createComparisonFunction('name'); 

// 调用函数
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
 
// 解除对函数的引用,这样就可以释放内存了
compareNames = null; 

这里,创建的比较函数被保存在变量 compareNames 中。把 compareNames 设置为等于 null 会解除对函数的引用,从而让垃圾回收程序可以将内存释放掉。作用域链也会被销毁,其他作用域(除全局作用域之外)也可以销毁。图 10-2 展示了调用 compareNames()之后作用域链之间的关系。

注意 因为闭包会保留它们包含函数的作用域,所以比其他函数更占用内存。过度使用闭包可能导致内存过度占用,因此建议仅在十分必要时使用。V8 等优化的 JavaScript 引擎会努力回收被闭包困住的内存,不过我们还是建议在使用闭包时要谨慎。

14.1 this 对象

在闭包中使用 this 会让代码变复杂。如果内部函数没有使用箭头函数定义,则 this 对象会在运行时绑定到执行函数的上下文。如果在全局函数中调用,则 this 在非严格模式下等于 window,在严格模式下等于 undefined。如果作为某个对象的方法调用,则 this 等于这个对象。匿名函数在这种情况下不会绑定到某个对象,这就意味着 this 会指向 window,除非在严格模式下 this 是 undefined。不过,由于闭包的写法所致,这个事实有时候没有那么容易看出来。来看下面的例子:

window.identity = 'The Window'; 

let object = { 
 	identity: 'My Object', 
 	getIdentityFunc() { 
 		return function() { 
 			return this.identity; 
 		}; 
 	} 
}; 

console.log(object.getIdentityFunc()()); // 'The Window'

这里先创建了一个全局变量 identity,之后又创建一个包含 identity 属性的对象。这个对象还包含一个 getIdentityFunc()方法,返回一个匿名函数。这个匿名函数返回 this.identity。因为getIdentityFunc()返回函数,所以 object.getIdentityFunc()()会立即调用这个返回的函数,从而得到一个字符串。可是,此时返回的字符串是"The Winodw",即全局变量 identity 的值。为什么匿名函数没有使用其包含作用域(getIdentityFunc())的 this 对象呢?

前面介绍过,每个函数在被调用时都会自动创建两个特殊变量:this 和 arguments。内部函数永远不可能直接访问外部函数的这两个变量。但是,如果把 this 保存到闭包可以访问的另一个变量中,则是行得通的。比如:

window.identity = 'The Window'; 

let object = { 
 	identity: 'My Object', 
 	getIdentityFunc() { 
 		let that = this; 
 		return function() { 
 			return that.identity; 
 		}; 
 	} 
}; 

console.log(object.getIdentityFunc()()); // 'My Object' 

此时在定义匿名函数之前,先把外部函数的 this 保存到变量 that 中。然后在定义闭包时,就可以让它访问 that,因为这是包含函数中名称没有任何冲突的一个变量。即使在外部函数返回之后,that 仍然指向 object,所以调用 object.getIdentityFunc()()就会返回"My Object"。

注意 this 和 arguments 都是不能直接在内部函数中访问的。如果想访问包含作用域中的 arguments 对象,则同样需要将其引用先保存到闭包能访问的另一个变量中。

在一些特殊情况下,this 值可能并不是我们所期待的值。比如下面这个修改后的例子:

window.identity = 'The Window'; 
let object = { 
 	identity: 'My Object', 
 	getIdentity () { 
 		return this.identity; 
 	} 
}; 

getIdentity()方法就是返回 this.identity 的值。以下是几种调用 object.getIdentity()的方式及返回值:

object.getIdentity(); // 'My Object' 
(object.getIdentity)(); // 'My Object' 
(object.getIdentity = object.getIdentity)(); // 'The Window'

第一行调用 object.getIdentity()是正常调用,会返回"My Object",因为 this.identity就是 object.identity。第二行在调用时把 object.getIdentity 放在了括号里。虽然加了括号之后看起来是对一个函数的引用,但 this 值并没有变。这是因为按照规范,object.getIdentity 和(object.getIdentity)是相等的。第三行执行了一次赋值,然后再调用赋值后的结果。因为赋值表达式的值是函数本身,this 值不再与任何对象绑定,所以返回的是"The Window"。

一般情况下,不大可能像第二行和第三行这样调用对象上的方法。但通过这个例子,我们可以知道,即使语法稍有不同,也可能影响 this 的值。

14.2 内存泄漏

待补充 339

15 立即调用的函数表达式

立即调用的匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式。下面是一个简单的例子:

(function() { 
 	// 块级作用域 
})(); 

使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。ECMAScript 5 尚未支持块级作用域,使用 IIFE模拟块级作用域是相当普遍的。比如下面的例子:

// IIFE 
(function () { 
 	for (var i = 0; i < count; i++) { 
 		console.log(i); 
 	} 
})(); 

console.log(i); // 抛出错误

在 ECMAScript 5.1 及以前,为了防止变量定义外泄,IIFE 是个非常有效的方式。这样也不会导致闭包相关的内存问题,因为不存在对这个匿名函数的引用。为此,只要函数执行完毕,其作用域链就可以被销毁。

在 ECMAScript 6 以后,IIFE 就没有那么必要了,因为块级作用域中的变量无须 IIFE 就可以实现同样的隔离。下面展示了两种不同的块级作用域形式:

// 内嵌块级作用域 
{ 
 	let i; 
 	for (i = 0; i < count; i++) { 
 		console.log(i); 
 	} 
} 
console.log(i); // 抛出错误

// 循环的块级作用域
for (let i = 0; i < count; i++) { 
 console.log(i); 
} 
console.log(i); // 抛出错误

说明 IIFE 用途的一个实际的例子,就是可以用它锁定参数值。比如:

待补充 这都是啥东西

let divs = document.querySelectorAll('div');
 
// 达不到目的! 
for (var i = 0; i < divs.length; ++i) { 
 	divs[i].addEventListener('click', function() { 
 		console.log(i); 
 	}); 
} 

这里使用 var 关键字声明了循环迭代变量 i,但这个变量并不会被限制在 for 循环的块级作用域内。因此,渲染到页面上之后,点击每个<div>都会弹出元素总数。这是因为在执行单击处理程序时,迭代变量的值是循环结束时的最终值,即元素的个数。而且,这个变量 i 存在于循环体外部,随时可以访问。

以前,为了实现点击第几个<div>就显示相应的索引值,需要借助 IIFE 来执行一个函数表达式,传入每次循环的当前索引,从而“锁定”点击时应该显示的索引值:

let divs = document.querySelectorAll('div'); 

for (var i = 0; i < divs.length; ++i) { 
 	divs[i].addEventListener('click', (function(frozenCounter) {
 		return function() { 
 			console.log(frozenCounter); 
 		}; 
 	})(i)); 
} 

而使用 ECMAScript 块级作用域变量,就不用这么大动干戈了:

let divs = document.querySelectorAll('div'); 

for (let i = 0; i < divs.length; ++i) { 
 	divs[i].addEventListener('click', function() {
  		console.log(i); 
 	}); 
} 

这样就可以让每次点击都显示正确的索引了。这里,事件处理程序执行时就会引用 for 循环块级作用域中的索引值。这是因为在 ECMAScript 6 中,如果对 for 循环使用块级作用域变量关键字,在这里就是 let,那么循环就会为每个循环创建独立的变量,从而让每个单击处理程序都能引用特定的索引。

但要注意,如果把变量声明拿到 for 循环外部,那就不行了。下面这种写法会碰到跟在循环中使用var i = 0 同样的问题:

let divs = document.querySelectorAll('div'); 

// 达不到目的!
let i; 
for (i = 0; i < divs.length; ++i) { 
 	divs[i].addEventListener('click', function() { 
 		console.log(i); 
 	}); 
} 

16 私有变量

待补充

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值