什么是闭包
函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。
也就是说,闭包可以让你从内部函数访问外部函数作用域。
在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。
闭包实例
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();
init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。
displayName() 是定义在 init() 里的内部函数,并且仅在 init() 函数体内可用。
请注意,displayName() 没有自己的局部变量。
然而,因为它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name 。
闭包原理详解
外层函数的局部变量若被闭包函数调用,就不会在外层函数执行完毕之后立即被回收
不管什么语言,操作系统都会存在一个垃圾回收机制,将多余的分配空间回收以便减小内存。
而一个函数的生命周期的是从调用开始的,在函数调用完毕的时候,函数内部的局部变量等都会被回收机制回收。
但是当外层函数内部的闭包函数引用到某个变量的时候,闭包函数内部就会形成一个指向该变量的地址,所以即使外层函数执行完成,但是被引用的变量还在继续被闭包函数引用,不符合垃圾回收机制的标准(当一个变量在整个环境中都没有被引用的时候就会被清除),所以不会被清除。
但同时也可能会造成内存泄漏,所以需要我们在闭包函数引用完成后将引用的变量设置成null.
同一个闭包函数多次执行,同一作用域中的状态改变是连续的。
每调用一次外层函数都会生成一个新的闭包函数,每个闭包函数之间的作用域不会互相影响。
function people() {
var age = 1;
return function() {
console.log(age++);
};
}
var lily = people();
var zw = people();
lily(); //1
lily(); //2
zw(); //1
上面的例子中,lily和zw是两个不同的闭包函数。
在lily执行两次后,引用的变量age已经变成2了。
因为lily在多次执行之后,同一闭包函数作用域中的引用变量也在连续发生改变。
但是当新的闭包函数zw生成后,所引用的变量age还是初始的1。
所以我们可以看出不同的闭包函数的作用域是互不影响的。
闭包定义模块模式
使用闭包来定义公共函数,并令其可以访问私有函数和变量
var num = (function () {
var number = 500;
function see() {console.log(number)};
return {
lily: () => {number +=2;},
zw: () => {number +=1;},
seeNum: () => {see()}
};
})();
num.lily(); //502
num.lily(); //504
num.seeNum();
num.zw(); //505
num.seeNum();
这次我们只创建了一个词法环境,为三个函数所共享:num.lily,num.zw 和 num.seeNum。
该共享环境创建于一个立即执行的匿名函数体内。
这个环境中包含两个私有项:名为 number的变量和名为 see的函数。
这两项都无法在这个匿名函数外部直接访问,必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。
多亏 JavaScript 的词法作用域,它们都可以访问 number的变量和名为 see的函数。
在循环中创建闭包:一个常见错误
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
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();
数组 helpText 中定义了三个有用的提示信息,每一个都关联于对应的文档中的input 的 ID。
通过循环这三项定义,依次为相应input添加了一个 onfocus 事件处理函数,以便显示帮助信息。
运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个input上,显示的都是关于年龄的信息。
原因是赋值给 onfocus 的是闭包。
这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。
这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。
这是因为变量item使用var进行声明,由于变量提升,所以具有函数作用域。
当onfocus的回调执行时,item.help的值被决定。
由于循环在事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。
解决这个问题的一种方案是使用更多的闭包:特别是使用前面所述的函数工厂:
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();
这段代码可以如我们所期望的那样工作。
所有的回调不再共享同一个环境, makeHelpCallback 函数为每一个回调创建一个新的词法环境。
在这些环境中,help 指向 helpText 数组中对应的字符串。
另一种方法使用了匿名闭包:
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++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})(); // 马上把当前循环项的item与事件回调相关联起来
}
}
setupHelp();
如果不想使用过多的闭包,你可以用ES2015引入的let关键词:
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++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
这个例子使用let而不是var,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。
性能考量
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。