闭包
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。[MDN]
示例:
function outerFunction() {
let outerVariable = 'I am from outer function';
// 闭包: innerFunction
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
let closureFunction = outerFunction();
closureFunction();
在上述示例中,innerFunction
就是一个闭包。当 outerFunction
执行完毕后,innerFunction
仍然能够访问 outerFunction
中的 outerVariable
变量,这是因为闭包保留了创建它时的词法环境。
闭包由函数以及创建该函数的词法环境组合而成。即使创建闭包的外部函数已经执行完毕,闭包仍能访问外部函数中的变量(依然可以保留对这些外部变量的引用)。
我们来看一个例子:
下面的代码创建了一个 shooters 数组,需求是每个shooter输出自己的编号。
function makeArmy() {
let shooters = [];
let i = 0;
while (i < 10) {
let shooter = function() { // 创建一个 shooter 函数,
console.log( "输出编号:", i ); // 应该显示其编号
};
shooters.push(shooter); // 将此 shooter 函数添加到数组中
i++;
}
// ……返回 shooters 数组
return shooters;
}
let army = makeArmy();
// ……所有的 shooter 显示的都是 10,而不是它们的编号 0, 1, 2, 3...
army[0](); // 编号为 0 的 shooter 显示的是 10
army[1](); // 编号为 1 的 shooter 显示的是 10
army[2](); // 10,其他的也是这样。
每个函数输出的编号都是10,为什么?
在上述代码中,所有的 shooter
函数显示的都是 10
,原因是 JavaScript 的闭包特性和变量提升。
在 while
循环中创建的 shooter
函数都引用了同一个外部变量 i
。当while
循环结束时,i
的值已经变成了 10
,此时 shooter
函数还保留着对外部变量i
的引用。当调用 shooter
函数时,它们获取的都是此时 i
的最终值 10
。
正确实现:
function makeArmy() {
let shooters = [];
let i = 0;
while (i < 10) {
let shooter = (function(j) { // 使用立即执行函数表达式创建一个新的作用域
return function() {
console.log( "输出编号:", i ); // 应该显示其编号
};
})(i);
shooters.push(shooter); // 将此 shooter 函数添加到数组中
i++;
}
// ……返回 shooters 数组
return shooters;
}
let army = makeArmy();
army[0](); // 0
army[1](); // 1
army[2](); // 2
通过立即执行函数表达式 (function(j) {...})(i)
,将当前的 i
值作为参数传递给这个函数,并在其内部创建了一个新的变量 j
来保存这个值。这样,每个 shooter
函数都有了自己独立的 j
值,从而能够正确显示各自的编号。
例如,如果 i
的值是 1
,那么传递给立即执行函数表达式的就是 1
,内部的 j
就被赋值为 1
,对应的 shooter
函数就会显示 1
。
闭包用途和特点:
- 实现私有变量:通过闭包,可以创建只能在特定函数内部访问和修改的变量,模拟私有变量的效果。
例如:
function createPerson() {
let name = '张三';
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
}
};
}
let person = createPerson();
console.log(person.getName()); // 输出 张三
person.setName('李四');
console.log(person.getName()); // 输出 李四
- 数据封装和隐藏:可以将相关的操作和数据封装在一个闭包中,避免外部直接访问和修改关键数据。
- 函数柯里化:通过闭包逐步构建函数的参数,实现更灵活的函数调用方式。
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func(...args);
} else {
return function(...moreArgs) {
return curried(...args,...moreArgs);
};
}
};
}
function add(a, b, c) {
return a + b + c;
}
let curriedAdd = curry(add);
let add5 = curriedAdd(5);
let add5And6 = add5(6);
console.log(add5And6(7)); // 输出 18
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,滥用可能会导致内存泄漏、网页性能差 等问题。
在退出函数之前,将不使用的局部变量全部删除。