每个函数都是Function类型的实例,而 Function 也有属性和方法,函数名就是指向函数对象的指针函数的定义方式:
- 函数声明
- 函数表达式
- 箭头函数
- 使用 Function 构造函数
// 使用 Function 构造函数
let sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐
// 因为这段代码会被解释两次:第一次是将它当作常规ECMAScript 代码,
//第二次是解释传给构造函数的字符串。这显然会影响性能。
1 箭头函数
箭头函数简洁的语法非常适合嵌入函数的场景:
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]
有很多场合不适用。箭头函数不能使用 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
注意:使用不带括号的函数名会访问函数指针,而不会执行函数。此时,anotherSum 和 sum 都指向同一个函数。把 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
3 理解参数
ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。你可以传一个、三个,甚至一个也不传,主要是因为 ECMAScript 函数的参数在内部表现为一个数组。在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问 arguments 类数组对象(但不是 Array 的实例),可以访问其中元素(第一个参数是 arguments[0])。
arguments 对象的值始终会与对应的命名参数同步。来看下面的例子:
function doAdd(num1, num2) {
arguments[1] = 10;
console.log(arguments[0] + num2);
}
这个 doAdd()函数把第二个参数的值重写为 10。因为 arguments 对象的值会自动同步到对应的命名参数,所以修改 arguments[1]也会修改 num2 的值,因此两者的值都是 10。但这并不意味着它们都访问同一个内存地址,它们在内存中还是分开的,只不过会保持同步而已。
箭头函数中的参数
如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用 arguments 关键字访问,而只能通过定义的命名参数访问。
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);
4 没有重载
ECMAScript 函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载。
把函数名当成指针也有助于理解为什么 ECMAScript 没有函数重载。定义两个同名的函数显然会导致后定义的重写先定义的:
let addSomeNumber = function(num) {
return num + 100;
};
addSomeNumber = function(num) {
return num + 200;
};
let result = addSomeNumber(100); // 300
在创建第二个函数时,变量 addSomeNumber 被重写成保存第二个函数对象了。
5 默认参数值
ECMAScript 6 之后支持显式定义默认参数:
function makeKing(name = 'Henry') {
return `King ${name} VIII`;
}
console.log(makeKing('Louis')); // 'King Louis VIII'
console.log(makeKing()); // 'King Henry VIII'
在使用默认参数时,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 关键字顺序声明变量一样.
// 因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数。看下面这个例子:
function makeKing(name = 'Henry', numerals = name) {
return `King ${name} ${numerals}`;
}
console.log(makeKing()); // King Henry Henry
参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。像这样就会抛出错误:
// 调用时不传第一个参数会报错
function makeKing(name = numerals, numerals = 'VIII') {
return `King ${name} ${numerals}`;
}
参数也存在于自己的作用域中,它们不能引用函数体的作用域:
// 调用时不传第二个参数会报错
function makeKing(name = 'Henry', numerals = defaultNumeral) {
let defaultNumeral = 'VIII';
return `King ${name} ${numerals}`;
}
6 参数扩展与收集
1 扩展参数
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
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
2 收集参数
// 可以
function ignoreFirst(firstValue, ...values) {
console.log(values);
}
ignoreFirst(); // []
ignoreFirst(1); // []
ignoreFirst(1,2); // [2]
ignoreFirst(1,2,3); // [2, 3]
// 箭头函数使用
let getSum = (...values) => {
return values.reduce((x, y) => x + y, 0);
}
console.log(getSum(1,2,3)); // 6
7 函数声明与函数表达式
函数声明和函数表达式差异:JavaScript 引擎在加载数据时对它们是区别对待的。JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
// 没问题 ,函数声明提升
console.log(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}
// 会出错, var定义也会错
console.log(sum(10, 10));
let sum = function(num1, num2) {
return num1 + num2;
};
8 函数作为值
可以在一个函数中返回另一个函数:
function callSomeFunction(someFunction, someArgument) {
return someFunction(someArgument);
}
function add10(num) {
return num + 10;
}
let result1 = callSomeFunction(add10, 10);
console.log(result1); // 20
9 函数内部
在 ECMAScript 5 中,函数内部存在两个特殊的对象:arguments 和 this。ECMAScript 6 又新增了 new.target 属性。
1 arguments
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);
}
}
let trueFactorial = factorial;
factorial = function() {
return 0;
};
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0
这里,trueFactorial 变量被赋值为 factorial,实际上把同一个函数的指针又保存到了另一个位置。然后,factorial 函数又被重写为一个返回 0 的函数。如果像 factorial()最初的版本那样不使用 arguments.callee,那么像上面这样调用 trueFactorial()就会返回 0。不过,通过将函数与名称解耦,trueFactorial()就可以正确计算阶乘,而 factorial()则只能返回 0。
2 this
另一个特殊的对象是 this,它在标准函数和箭头函数中有不同的行为。
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'blue'
在把 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 会保留定义该函数时的上下文:
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
3 caller
ECMAScript 5 也会给函数对象上添加一个属性:caller。这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为 null。比如:
function outer() {
inner();
}
function inner() {
console.log(inner.caller);
}
outer();
// 会显示 outer()函数的源代码。这是因为 ourter()调用了 inner(),inner.caller
// 指向 outer()。
4 new.target
ECMAScript 6 新增了检测函数是否使用 new 关键字调用的。如果函数是正常调用的,则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数:
function King() {
if (!new.target) {
throw 'King must be instantiated using "new"'
}
console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // Error: King must be instantiated using "new"
10 函数属性与方法
ECMAScript 中的函数是对象,因此有属性和方法。每个函数都有两个属性:length和 prototype。
function sum(num1, num2) {
return num1 + num2;
}
console.log(sum.length); // 2
prototype 是保存引用类型所有实例方法的地方,这意味着 toString()、valueOf()等方法实际上都保存在 prototype 上,进而由所有实例共享。
注意 在严格模式下,调用函数时如果没有指定上下文对象,则 this 值不会指向 window。除非使用 apply()或 call()把函数指定给一个对象,否则 this 的值会变成 undefined。
ECMAScript 5 出于同样的目的定义了一个新方法:bind()。bind()方法会创建一个新的函数实例,其 this 值会被绑定到传给 bind()的对象。比如:
window.color = 'red';
var o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
let objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
这里,在 sayColor()上调用 bind()并传入对象 o 创建了一个新函数 objectSayColor()。objectSayColor()中的 this 值被设置为 o,因此直接调用这个函数,即使是在全局作用域中调用,也会返回字符串"blue"。
注意 对函数而言,继承的方法 toLocaleString()和 toString()始终返回函数的代码。返回代码的
具体格式因浏览器而异。不能在重要功能中依赖这些方法返回的值,而只应在调试中使用它们。
11 函数表达式
let functionName = function(arg0, arg1, arg2) {
// 函数体
};
这样创建的函数叫作匿名函数(anonymous funtion),因为 function 关键字后面没有标识符。(匿名函数有也时候也被称为兰姆达函数)。
12 递归
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
arguments.callee,可以确保无论通过什么变量调用这个函数都不会出问题。因此在编写递归函数时,arguments.callee 是引用当前函数的首选。
不过,在严格模式下运行的代码是不能访问 arguments.callee 的,因为访问会出错。此时,可以使用命名函数表达式(named function expression)达到目的。比如:
const factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
});
13 尾调用优化
ECMAScript 6 规范新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。具体来说,这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值。比如:
function outerFunction() {
return innerFunction(); // 尾调用
}
在 ES6 优化之前,执行这个例子会在内存中发生如下操作。
(1) 执行到 outerFunction 函数体,第一个栈帧被推到栈上。
(2) 执行 outerFunction 函数体,到 return 语句。计算返回值必须先计算 innerFunction。
(3) 执行到 innerFunction 函数体,第二个栈帧被推到栈上。
(4) 执行 innerFunction 函数体,计算其返回值。
(5) 将返回值传回 outerFunction,然后 outerFunction 再返回值。
(6) 将栈帧弹出栈外。
在 ES6 优化之后,执行这个例子会在内存中发生如下操作。
(1) 执行到 outerFunction 函数体,第一个栈帧被推到栈上。
(2) 执行 outerFunction 函数体,到达 return 语句。为求值返回语句,必须先求值 innerFunction。
(3) 引擎发现把第一个栈帧弹出栈外也没问题,因为 innerFunction 的返回值也是 outerFunction的返回值。
(4) 弹出 outerFunction 的栈帧。
(5) 执行到 innerFunction 函数体,栈帧被推到栈上。
(6) 执行 innerFunction 函数体,计算其返回值。
(7) 将 innerFunction 的栈帧弹出栈外。
很明显,第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。而第二种情况下无论调用多少次嵌套函数,都只有一个栈帧。这就是 ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。
注意 现在还没有办法测试尾调用优化是否起作用。不过,因为这是 ES6 规范所规定的,
兼容的浏览器实现都能保证在代码满足条件的情况下应用这个优化。
1.尾调用优化的条件
尾调用优化的条件就是确定外部栈帧真的没有必要存在了。涉及的条件如下:
代码在严格模式下执行;
外部函数的返回值是对尾调用函数的调用;
尾调用函数返回后不需要执行额外的逻辑;
尾调用函数不是引用外部函数作用域中自由变量的闭包。
14 闭包
匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。如图例子1:
//闭包
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;
}
};
}
理解作用域链创建和使用的细节:
在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。然后用 arguments和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。
在函数执行时,要从作用域链中查找变量,以便读、写值。来看下面的代码:
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let result = compare(5, 10);
第一次调用 compare()时,会为它创建一个包含 arguments、value1 和 value2 的活动对象,这个对象是其作用域链上的第一个对象。而全局上下文的变量对象则是 compare()作用域链上的第二个对象,其中包含 this、result 和 compare。关系如下图:
函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域。不过,闭包就不一样了。
闭包的不同:
在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。
上面例子1代码中,匿名函数的作用域链中实际上包含 createComparisonFunction()的活动对象。
let compare = createComparisonFunction('name');
let result = compare({ name: 'Nicholas' }, { name: 'Matt' });
下图展示了关系:
createComparisonFunction()的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。在 createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁:
// 创建比较函数
let compareNames = createComparisonFunction('name');
// 调用函数
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
// 解除对函数的引用,这样就可以释放内存了
compareNames = null;
这里,创建的比较函数被保存在变量 compareNames 中。把 compareNames 设置为等于 null 会解除对函数的引用,从而让垃圾回收程序可以将内存释放掉。作用域链也会被销毁,其他作用域(除全局作用域之外)也可以销毁。
注意 因为闭包会保留它们包含函数的作用域,所以比其他函数更占用内存。过度使用闭包可能导致内存过度占用,因此建议仅在十分必要时使用。V8 等优化的 JavaScript 引擎会努力回收被闭包困住的内存,不过我们还是建议在使用闭包时要谨慎。
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'
输出’The Window’是因为:每个函数在被调用时都会自动创建两个特殊变量: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 对象,则同样需要将其引用先保存到闭包能访问的另一个变量中。
2 内存泄漏
由于 IE 在 IE9 之前对 JScript 对象和 COM 对象使用了不同的垃圾回收机制,所以闭包在这些旧版本 IE 中可能会导致问题。在这些版本的 IE 中,把 HTML 元素保存在某个闭包的作用域中,就相当于宣布该元素不能被销毁。来看下面的例子:
function assignHandler() {
let element = document.getElementById('someElement');
element.onclick = () => console.log(element.id); // 创建了一个循环引用,引用计数至少等于 1,内存不会被回收
}
可以这样修改:
function assignHandler() {
let element = document.getElementById('someElement');
let id = element.id;
element.onclick = () => console.log(id); // 引用一个保存着 element.id 的变量 id,从而消除了循环引用
element = null;
}
15 立即调用的函数表达式
立即调用的匿名函数又被称作立即调用的函数表达式。
(function() {
// 块级作用域
})();
ECMAScript 5 尚未支持块级作用域,使用 IIFE模拟块级作用域是相当普遍的。比如下面的例子:
// IIFE
(function () {
for (var i = 0; i < count; i++) {
console.log(i);
}
})();
console.log(i); // 抛出错误
为了防止变量定义外泄,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); // 抛出错误
16 私有变量
私有变量包括函数参数、局部变量,以及函数内部定义的其他函数。来看下面的例子:
function add(num1, num2) {
let sum = num1 + num2;
return sum;
}
如果这个函数中创建了一个闭包,则这个闭包能通过其作用域链访问其外部的这 3 个变量。基于这一点,就可以创建出能够访问私有变量的公有方法。
特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。在对象上有两种方式创建特权方法。第一种是在构造函数中实现,比如:
function MyObject() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权方法
this.publicMethod = function() {
privateVariable++;
return privateFunction();
};
}
1 静态私有变量
特权方法也可以通过使用私有作用域定义私有变量和函数来实现。这个模式如下所示:
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 构造函数
MyObject = function() {};
// 公有和特权方法
MyObject.prototype.publicMethod = function() {
privateVariable++;
return privateFunction();
};
})();
模块模式
let singleton = function() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权/公有方法和属性
return {
publicProperty: true,
publicMethod() {
privateVariable++;
return privateFunction();
}
};
}();
模块模式使用了匿名函数返回一个对象。在匿名函数内部,首先定义私有变量和私有函数。之后,
创建一个要通过匿名函数返回的对象字面量。这个对象字面量中只包含可以公开访问的属性和方法。因为这个对象定义在匿名函数内部,所以它的所有公有方法都可以访问同一个作用域的私有变量和私有函数。
3 模块增强模式
let singleton = function() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 创建对象
let object = new CustomType();
// 添加特权/公有属性和方法
object.publicProperty = true;
object.publicMethod = function() {
privateVariable++;
return privateFunction();
};
// 返回对象
return object;
}();
17 小结
函数是 JavaScript 编程中最有用也最通用的工具。ECMAScript 6 新增了更加强大的语法特性,从而
让开发者可以更有效地使用函数。
函数表达式与函数声明是不一样的。函数声明要求写出函数名称,而函数表达式并不需要。没
有名称的函数表达式也被称为匿名函数。
ES6 新增了类似于函数表达式的箭头函数语法,但两者也有一些重要区别。
JavaScript 中函数定义与调用时的参数极其灵活。arguments 对象,以及 ES6 新增的扩展操作符,
可以实现函数定义和调用的完全动态化。
函数内部也暴露了很多对象和引用,涵盖了函数被谁调用、使用什么调用,以及调用时传入了
什么参数等信息。
JavaScript 引擎可以优化符合尾调用条件的函数,以节省栈空间。
闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的
变量对象。
通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁。
闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。
函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用。
立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量,则其包含的所有变量都
会被销毁。
虽然 JavaScript 没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域
中定义的变量。
可以访问私有变量的公共方法叫作特权方法。
特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增
强模式在单例对象上实现。