基本概念
闭包是函数和声明该函数的词法环境的组合。这个环境包含了这个闭包创建时所能访问的所有局部变量。并且它无视js的垃圾回收机制,在外层函数执行完毕后并不会被销毁。
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应当是不能够访问的。但由于无名函数的存在形成了闭包,导致函数变量add5和add10仍然可以访问x。(分别为5和10)
作用
闭包的作用是能将函数与某些数据(环境)联系起来。这显然类似于一个对象用法。
比如我们想将某些HTML DOM树上的元素绑定显示特定的内容。显然绑定的方法都是类似的,只是要绑定的数据有所区别。这里我们就能使用闭包将数据和固定的函数联系起来。
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
setupHelp();
这里的闭包我们就将item数组中的内容分别于showHelp()绑定。不过这个方法创建了三个闭包,相当消耗内存。
除此之外,闭包还能够模拟私有成员。考虑以下代码:
function privateCounter() {
let privateVal = 0;
function add(num) {
return privateVal + num;
}
return {
plus: () => {
add(1);
},
minus: () => {
add(-1);
},
getVal: () => {
return privateVal
}
}
}
let Counter = new privateCounter()
privateCounter()就相当于构造函数。通过它实例化了一个Counter对象。这个对象不能直接获取privateVal值,因为它没有这个属性;但是可以通过getVal函数获得,因为其形成了闭包,哪怕构造函数执行完毕也依然能够访问privateVal。
循环闭包陷阱
尽管与上面代码同样的思想,下面的代码就不能正常运行:
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
会发现三个都显示的是age的信息。为什么呢?这关系到var的作用域问题。众所周知var声明的变量是函数作用域。这就意味着在整个函数运行过程中它声明的变量都是有效的。而在上述代码中item的作用域显然就是setupHelp()函数的作用域。
在for循环中我们定义了函数showHelp的闭包。这个闭包包括的就是showHelp()函数和它的执行环境setupHelp()函数的作用域。
我们知道给DOM元素绑定事件是异步操作。只有当条件达到时才会调用绑定事件函数。那么当showHelp()执行的时候,需要给document.getElementById('help').innerHTML = help;
找到help的值。既然本函数中没有,那就只能去它的上一级—setupHelp()函数的作用域中寻找了。此时for循环早已完成,item的值就是{'id': 'age', 'help': 'Your age (you must be over 16)'}
。而且三个showHelp()的闭包都是包含的setupHelp()函数的作用域,它们都指向内存中的同一位置。于是三个showHelp()函数都得到的是age的数据。
想要解决这个问题,有这几种办法:
- 使用let声明item。let 声明的变量具有块级作用域,这样其作用域就被限制在了for循环内部。那么在寻找item的时候找到for循环块这一级作用域就能找到了。找到了之后就不会深入寻找了。
- 如“作用”部分中第一段代码所示,在showHelp()外再套一层函数。这样makeHelpCallback函数为每个回调函数创造了一个新的闭包环境(因为函数立即执行,作用域进链)。这个环境里的item.help分别是三个不同的值。
性能问题
闭包中的数据一直都处于内存之中且不会被垃圾回收机制回收。滥用闭包就可能导致内存消耗过多甚至是内存泄露。因此在使用闭包之前一定要充分考虑其他方法,控制闭包数量。
代码来源及参考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures