一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
闭包其实就是一个可以访问其他函数内部变量的函数。创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以 访问到当前函数的局部变量。
简单的例子
function bibao() {
var name = '张三'
function displayName() {
console.log(name, 'name');
}
displayName()
}
bibao()
// 张三 name
bibao() 创建了一个局部变量 name 和一个名为 displayName() 的函数。displayName() 是定义在 bibao() 里的内部函数,并且仅在 bibao() 函数体内可用。请注意,displayName() 没有自己的局部变量。然而,因为它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 bibao() 中声明的变量 name 。
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
在这个示例中,我们定义了 makeAdder(x) 函数,它接受一个参数 x ,并返回一个新的函数。返回的函数接受一个参数 y,并返回x+y的值。
从本质上讲,makeAdder 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。
add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。
闭包的用处
- 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
例子
let a = 1
// fn 是闭包
function fn() {
console.log(a);
}
function fn1() {
let a = 1
// 这里也是闭包
return () => {
console.log(a);
}
}
const fn2 = fn1()
fn2()
闭包其中一个作用是访问私有变量,就比如上述代码中的 fn2 访问到了 fn1 函数中的变量 a。但是此时 fn1 早已销毁,我们是如何访问到变量 a 的呢?不是都说原始类型是存放在栈上的么,为什么此时却没有被销毁掉?
上图中画红框的位置我们能看到一个内部的对象 [[Scopes]],其中存放着变量 a,该对象是被存放在堆上的,其中包含了闭包、全局对象等等内容,因此我们能通过闭包访问到本该销毁的变量。
用闭包模拟私有方法
编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。
而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分
function makeAdd() {
var privateCounter = 0
function addNum(val) {
privateCounter += val
}
return {
add1: function () {
addNum(1)
},
add2: function () {
addNum(-1)
},
value: function () {
return privateCounter
}
}
}
var makeAdd1 = makeAdd()
var makeAdd2 = makeAdd()
makeAdd1.add1()
makeAdd1.add1()
console.log(makeAdd1.value(), 'value1');
makeAdd2.add2()
makeAdd2.add2()
console.log(makeAdd2.value(), 'value2');
请注意两个计数器 makeAdd1 和 makeAdd2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。
以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。
性能考量
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};
闭包的表现形式
- 返回一个函数
- 在定时器、事件监听、Ajax 请求、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。请看下面这段代码,这些都是平常开发中用到的形式
// 定时器
setTimeout(function handler(){
console.log('1');
},1000);
// 事件监听
$('#app').click(function(){
console.log('Event Listener');
});
// 立即执行函数
var a = 2;
(function IIFE(){
console.log(a); // 输出2
})();
经典面试题
for (var i = 0; i < 6; i++) {
setTimeout(() => {
console.log(i)
})
}
解决方法:
for(var i = 1;i <= 6;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}
还有其他方法自行百度吧。
参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures