闭包和原型应该是js里对初学者来说比较难以掌握的点了,今天我们就来好好聊一下什么是闭包。
先看看mdn的定义:
闭包是函数和声明该函数的词法环境的组合。
什么意思呢?先看一段代码吧
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
console.log(name); // 使用了父函数中声明的变量
}
displayName();
}
init();
在函数init()里还有一个函数,displayName()函数。这个函数没有局部变量,但是他可以访问到外部函数中的变量,但是,如果
有同名变量name在displayName中被定义,则会使用displayName()中定义的name。
这就是词法作用域的意思。
而什么又是闭包呢?
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
来看代码,
运行这段代码和之前的 init() 示例的效果完全一样。其中的不同 — 也是有意思的地方 — 在于内部函数 displayName() 在执行前,被外部函数返回。
第一眼看上去,也许不能直观的看出这段代码能够正常运行。在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 makeFunc() 执行完毕,我们会认为 name 变量将不能被访问。然而,因为代码运行得没问题,所以很显然在 JavaScript 中并不是这样的。
这个谜题的答案是,JavaScript中的函数会形成闭包。 闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。在我们的例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用,而 displayName 实例仍可访问其词法作用域中的变量,即可以访问到 name 。由此,当 myFunc 被调用时,name 仍可被访问,其值 Mozilla 就被传递到alert中。
再来看一段代码,
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。
闭包可以这样理解,好比一个餐厅,盘子总是有限的,所以服务员会去巡台回收空盘子,但还装着菜的盘子他怎么敢收? 当然,你自己手动倒掉了盘子里面的菜(=null),那盘子就会被收走了,这就是所谓的内存回收机制。
来看看常见的问题。
1.概念很抽象,我们到底什么时候使用闭包呢?
通常你使用只有一个方法的对象的地方,都可以使用闭包
举个例子
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(22);
var size14 = makeSizer(24);
var size16 = makeSizer(26);
document.getElementById('size-22').onclick = size12;
document.getElementById('size-24').onclick = size14;
document.getElementById('size-26').onclick = size16;
这就是一个闭包的使用案例,
22
24
26
当点击a标签的时候切换字体的大小。
2.有哪些常见的问题?
在循环中使用闭包,容易出错
来看代码
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();
运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个input
上,显示的都是关于年龄的信息。
原因是赋值给 onfocus
的是闭包。这些闭包是由他们的函数定义和在 setupHelp
作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。当onfocus的回调执行时,item.help
的值被决定。由于循环在事件触发之前早已执行完毕,变量对象item
(被三个闭包所共享)已经指向了helpText
的最后一项。
解决这个问题的一种方案是使用更多的闭包或者使用let关键字。
3.闭包有什么缺点?
因为闭包一直存在与内存中,所以会造成性能的降低,需要手动将不需要的闭包解除掉,可以用(=null)来处理
参考文献:
MDN闭包
深入理解javascript原型和闭包(15)——闭包
带你一分钟了解闭包