先来说下概念:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
说到闭包,必须要说下作用域,看下面代码👇:
function init() {
var name = "Davie"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();
init()
创建了一个局部变量 name
和一个名为 displayName()
的函数。displayName()
是定义在 init()
里的内部函数,并且仅在 init()
函数体内可用。displayName()
没有自己的局部变量。然而,因为它可以访问到外部函数的变量,所以 displayName()
可以使用父函数 init()
中声明的变量 name
。
运行该代码后发现, displayName()
函数内的 alert()
语句成功显示出了变量 name
的值(该变量在其父函数中声明)。词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。
重要的话说三遍:
词法作用域根据源代码中声明变量的位置来确定该变量在何处可用
词法作用域根据源代码中声明变量的位置来确定该变量在何处可用
词法作用域根据源代码中声明变量的位置来确定该变量在何处可用
一定是声明变量的位置,而不是调用的位置。
再来看一段代码:
function makeFunc() {
var name = "Davie";
function displayName() {
alert(name);
}
return displayName;
}
var name = "大卫"
var myFunc = makeFunc();
myFunc();
代码运行结果是 “Davie”,displayName
函数形成一个闭包,如果没有displayName
函数,makeFunc
函数执行完之后,name属性就不能再被访问。而由于displayName
的存在,维持了对name
的引用,因此,当 myFunc
被调用时,变量 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
仍然存在,保留在内存中,当在此执行内部的闭包函数时,就可以被内部函数访问到。
在一些编程语言,比如 Java中,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。
而 JavaScript 没有这种原生支持(TypeScript已经支持),但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为模块模式:
var Counter = (function() {
var _privateCounter = 0;
function changeBy(val) {
_privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return _privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
通过立即执行函数创建了一个模块。模块内部的两个变量privateCounter
和changeBy
都不能再外部访问到,于是提供了三个函数用于在外部访问。这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter
变量和 changeBy
函数。
把上面的代码稍加改造,变成有名函数,通过执行这个函数得到多个计数器:
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
两个计数器 Counter1
和 Counter2
是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter
。
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。
通过这种方式可以实现很多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。
我们在开发中,经常会遇到一个问题就是通过循环的方式给元素添加事件:
<p id="html">HTML</p>
<p id="css">CSS</p>
<p id="js">JavaScript</p>
需求是给每一个p
标添加点击事件,当点击p
标签时,使用alert
弹出里面的文字内容:
通过for循环添加:
var list = document.getElementsByTagName('p')
for(var i = 0;i < list.length;i++){
var item = list[i]
item.onclick = function(){
alert(item.innerText)
}
}
如果这样写的话,执行代码会发现不管点击那个元素,弹出的始终是”JavaScript”。
原因是赋值给 onclick 的是闭包。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。变量item使用var进行声明,由于变量提升,所以具有函数作用域。由于循环在事件触发之前早已执行完毕,变量对象item
(被三个闭包所共享)的值已经变成了最后一个p
。
解决这个问题的办法是使用工厂模式:
function showText(item){
return function(){
alert(item.innerText)
}
}
var list = document.getElementsByTagName('p')
for(var i = 0;i < list.length;i++){
var item = list[i]
item.onclick = showText(item)
}
所有的回调不再共享同一个环境, showText 函数为每一个回调创建一个新的词法环境。三个闭包中,三个item会被单独保存下来。
当然使用匿名函数也可以:
var list = document.getElementsByTagName('p')
for(var i = 0; i < list.length;i++){
(function(){
var item = list[i]
item.onclick = function(){
alert(item.innerText)
}
})()
}
当然,使用ES6的let关键字就单间多了:
for(var i = 0;i < list.length;i++){
let item = list[i]
item.onclick = function(){
alert(item.innerText)
}
}
使用let定义item,每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。
另外还有一种方法是使用forEach函数:
// 使用forEach方法
new Array().forEach.call(list, item => {
item.onclick = function () {
alert(item.innerText)
}
})
闭包虽然有很多好处,然而也要谨慎使用,由于闭包会保存变量,不会立即被垃圾回收机制处理,所以创建过多的闭包可能会造成内存泄漏。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
function Person(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:
function Person(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
闭包是能够读取另一个函数作用域的变量的函数。
闭包具有:封闭性、持久性的有点。
同时又由于持久性,处理不当易造成内存泄漏。