闭包的文章非常多,为了面试也前前后后看了挺多,但是总是感觉理解的模模糊糊,今天找的时候发现MDN关于闭包的文档教程非常详细,能理解的比较彻底,所以总结一下写下来,MDN文档链接放在最后,需要的同学直接滑到最后就能看到传送门。
我的疑惑有三个:
1.什么是闭包?
2.闭包能做些什么?
3.为什么要有闭包?
首先我们了解一下词法作用域的概念,词法就是特定文体内语词的构成和使用法则,词法作用域就是定义在词法阶段的作用域,简单来说我们按照JavaScript给定的语法写出代码,这叫词法,词法作用域就是由我们在代码里将变量和块作用域写在哪里来决定的,看下面一段代码:
function init() {
var name = 'Mozilla'; //name是一个被init创建的局部变量
function displayName() { //displayName()是内部函数,一个闭包
console.log(name); //使用了父函数里面声明的变量
}
displayName();
}
init(); //输出"Mozilla"
init()创建了一个局部变量name和一个名为displayName()的函数,displayName()是定义在init()里面的,仅在函数里面能用,displayName()内没有自己的局部变量,然而它可以访问到外部函数的变量,所以在displayName()可以使用init()中声明的变量name,如果在displayName()里声明了同名变量name,则会使用displayName()里声明的name。
运行init()会输出name的值“Mozilla”,由此可以确定,嵌套函数可以访问其外部声明的变量。
下面我们来玩一个找不同的游戏,请从下面代码中找出三处与上面代码不同的地方,游戏开始
function makeFunc() {
var name = 'Mozilla'; //name是一个被init创建的局部变量
function displayName() { //displayName()是内部函数,一个闭包
console.log(name); //使用了父函数里面声明的变量
}
return displayName; //返回内部函数
}
var myFunc = makeFunc();
myFunc(); //输出"Mozilla"
揭晓答案的时候到了
- 函数名字不同
- myFunc()内没有执行displayName(),而是返回了它
- 定义了一个变量来接收返回的displayName(),在别的地方执行
通过运行代码,我们发现其实两段代码执行效果是相同的,不同之处是第二段代码的displayName()在执行前被外部函数返回。第一眼看上去,也许不能直观地看出这段代码能够正常运行,在一些编程语言中,函数中的局部变量仅在函数的执行期间可用,一旦makeFunc()执行完毕,我们会认为name变量将不能被访问,然而,因为代码运行的没问题,所以很显然在JavaScript中并不是这样的,
这个谜题的答案就是闭包,JavaScript中的函数会形成闭包。闭包是由函数及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量,在我们的例子中,myFunc是执行makeFunc时创建的displayName函数实例的引用,而displayName实例仍可访问到其词法作用域中的变量,即可以访问到name,因此,当myFunc被调用时,name仍可被访问,其值Mozilla就被传递到console中。
下面我们看一个更有趣的示例–makeAdder函数:
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.这样我们就解决了第一个问题,什么是闭包,闭包就是函数和声明该函数的词法环境的组合,闭包能访问其词法环境中的所有变量
下一个问题,闭包能做些什么?
闭包很有用,因为它允许将函数及其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。
因此,通常我们使用只有一个方法的对象时,可以使用闭包。
在web中,大部分我们所写的JavaScript代码都是基于事件的–定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。
假想,我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定body元素的font-size,然后通过相对的em单位设置页面中其它元素(例如header)的字号:
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
我们的文本尺寸调整按钮可以修改body元素的font-size属性,由于我们使用相对单位,页面中的其它元素也会相应的调整。
以下是JavaScript:
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px'
}
}
var size12 = makeSizer(12)
var size14 = makeSizer(14)
var size16 = makeSizer(16)
size12, size14和size16三个函数分别把body文本调整为12,14,16像素。我们可以将他们分别添加到按钮的点击事件上。如下所示:
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
用闭包模拟私有方法
编程语言中,比如Java或者C++,是支持私有方法的,即它们只能同时被同一个类中的其它方法所调用。
在JavaScript中没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问,还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口。
下面的实例展示了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方法也成为模块模式。
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());
Counter.increment();
console.log(Counter.value());
Counter.decrement();
console.log(Counter.value());
之前的示例中,每个闭包都有自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:Counter.increase, Counter.decrease,和Counter.value。
该共享环境创建于一个立即执行的匿名函数体内。这个环境包含两个私有项:名为privateCounter的变量和名为changeBy的函数。这两项都无法再匿名函数外部直接访问,必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。多亏了JavaScript的词法作用域,它们都可以访问privateCounter变量和changeBy函数。
你应该注意到我们定义了一个匿名函数,用来创建一个计数器。我们立即执行了这个匿名函数,并把他的值赋给了变量counter。我们可以把这个函数存储在另一个变量makeCounter中,并用他来创建多个计数器。
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());//0
Counter1.increment();
console.log(Counter1.value());//1
console.log(Counter2.value());//0
Counter2.decrement();
console.log(Counter2.value()); //-1
请注意两个计数器counter1和counter2是如何维护它们各自的独立性的,每个闭包都是引用自己词法作用域内的变量privateCounter。
每次调用其中一个计数器时,通过改变这个变量,会改变这个闭包的词法环境。然而在一个闭包内对变量进行的修改,不会影响另一个闭包中的变量。以这种方式使用闭包,提供了许多面向对象编程的相关的好处–特别是数据的隐藏和封装。
在循环中创建闭包:一个常见错误
在ECMAscript2015引入let关键字之前,在循环中有一个常见的闭包创建问题。参考下面的示例:
HTML
<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>
JavaScript
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).onclick = function () {
showHelp(item.help);
}
}
}
setupHelp();
数组helpText中定义了三个有用的提示信息,每一个都关联对应的文档中的input的ID,通过循环着三项定义,依次为相应的input添加了一个onfocus事件处理函数,以便显示帮助信息。
运行这段后,我们会发现它没有达到要的效果。无论焦点在哪个input上,显示的都是关于年龄的信息。
原因是赋值给onfocus的是闭包。这些闭包是由他们的函数定义和在setupHelp作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。当onfocus的回调执行时,item.help的值被决定。由于循环在触发之前早已执行完毕,对象item(被三个闭包所共享)已经指向了helpText的最后一项。
解决这个问题的一种方案是使用更多的闭包:特别是使用前面所述的函数工厂:
JavaScript:
function makeHelpCallback(help) {
return function() {
return showHelp(help)
}
}
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).onclick = makeHelpCallback(item.help);
}
}
setupHelp();
另一个方法是使用匿名函数,原理和也是使用更多闭包,语法相对于上面的示例更简洁了一点,代码如下:
JavaScript:
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).onclick = function () {
showHelp(item.help)
}
})();
}
}
setupHelp();
为避免使用过多的闭包,可以用let关键字:
JavaScript:
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,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。
第二个问题解决了,第三个问题,未完待续。。。。
性能考量
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本有负面影响。