闭包的定义:
闭包是函数和声明该函数的词法作用域的组合。
先看如下例子:
function makeFn(){
var name = "Mirror";
function showName(){
alert(name)
}
return showName;
}
var myFn = makeFn();
myFn(); // "Mirror"
javascript 中的函数会形成闭包。闭包是由函数以及创建该函数的词法环境组合组成。这个环境包含了这个闭包创建时所能访问的所有局部变量。在以上的例子中,myFn
是执行makeFn
时创建的showName
函数实例的引用,而showName
实例仍可访问其词法作用域中的变量,既可以访问到name
。 由此,当myFn
被调用时,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)
函数,它接收一个参数x
,并返回一个新的函数。返回的函数接受一个参数y
,并返回 x+y
的值。
本质上讲,makeAdder
是一个工厂函数 -- 它创建了将指定的值和它的参数相加求和的函数。上面的add5
和add10
都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在add5
的环境中,x
为5,而在add10
中,x
则是10。
实用的闭包:
闭包允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。
通常,使用只有一个方法的对象的地方都可以使用闭包。
假如,我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定 body 元素的 font-size,然后通过相对的 em 单位设置页面中其它元素(例如header)的字号:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body { font-size: 12px;}
h1 {font-size: 1.5em;}
h2 {font-size: 1.2em;}
</style>
<script>
function SetFs( size ){
return function(){
document.body.style.fontSize = size + 'px'
}
}
</script>
</head>
<body>
<h1> h1 标签</h1>
<h2> h2 标签</h2>
<button id="size_22" onclick="SetFs(22)()" >22px</button>
<button id="size_32" onclick="SetFs(32)()" >32px</button>
</body>
</html>
用闭包模拟私有方法:
私有方法不仅仅有利于限制对代码的访问,还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
var Counter = ( function(){
var privateCounter = 0;
function changeBy(val){
privateCounter += val
};
return {
add:function(){
changeBy(1);
return privateCounter;
},
decrease:function(){
changeBy(-1);
return privateCounter;
},
value:function(){
return privateCounter;
}
}
})()
以上示例中我们只创建了一个词法环境,为三个函数所共享:Counter.add
、 Counter.decrease
和Counter.value
该共享环境创建一个立即执行的匿名函数体内。这个环境中包含两个私有项:privateCounter
的变量和changeBy
的函数。这两项都无法在这个匿名函数外直接访问。必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。多亏javascript的词法作用域,它们都可以访问privateCounter
变量和changeBy
函数。
应该注意到我们定义了一个匿名函数,用来创建计算器。立即执行函数将他的值赋给了变量Counter
。我们也可以将这个函数存储在另一个变量makeCounter
中,并用它来创建多个计数器。
var makeCounter = function(){
var privateCounter = 0;
var changeBy = function(val){
privateCounter += val
};
return {
add:function(){
changeBy(1)
return privateCounter
},
decrease:function(){
changeBy(-1)
return privateCounter
},
value:function(){
return privateCounter
}
}
}
var Counter1 = makeCounter();
var Counter2 = makeCounter();
Counter1.add() // 1
Counter2.decrease() // -1
两个计数器Counter1
和Counter2
都引用自己词法作用域内的变量privateCounter
。每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另一个闭包中的变量。
以这种方式使用闭包,提供了许多面向对象编程相关的好处——特别是数据隐藏和封装。
在循环中创建闭包:一个常见的错误
在ECMAScript2015引入let
关键字以前,在循环中有一个常见的闭包创建问题。如下:
<button>1</button>
<button>2</button>
<button>3</button>
<script>
var els = document.querySelectorAll('button');
for(var i=0; i<els.length ; i++){
els[i].onclick = function(){
alert( '第'+ i +`按钮` )
}
}
</script>
运行以上代码,会发现没有达到想要的结果。无论点击哪个按钮,弹窗提示的都是第3个按钮
。
原因是赋值给onclick
的是闭包。三个闭包在循环中被创建,但是他们共享同一个词法作用域,在这个作用域中存在一个变量i
。当onclick
的回调执行时,i
的值被决定。由于循环在事件触发之前早已执行完毕,变量i
(被三个闭包共享)已经变成了'3'。
解决这个问题的一种方案是使用跟多的闭包:特别是使用前面所述的函数工厂:
<button>1</button>
<button>2</button>
<button>3</button>
<script>
var els = document.querySelectorAll('button');
function clickCallback(x){
return function(){
alert(x)
}
}
for(var i=0; i<els.length ; i++){
els[i].onclick = clickCallback(i)
}
</script>
这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境,clickCallback
函数为每一个回调创建一个新的词法环境。在这些环境中。x
指向循环中的i
。
另一种方法实现了匿名闭包:
<button>1</button>
<button>2</button>
<button>3</button>
<script>
var els = document.querySelectorAll('button');
for(var i=0; i<els.length ; i++){
els[i].onclick = (function (i) {
return function(){alert(i)}
})(i)
}
</script>
避免使用过多的闭包,可以用let关键词:
<button>1</button>
<button>2</button>
<button>3</button>
<script>
var els = document.querySelectorAll('button');
for(let i=0; i<els.length ; i++){
els[i].onclick = function(){
alert( i );
}
}
</script>
这个例子使用的是let
而不是var
,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。
性能考量
如果不是某些特定的任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是,每个对象的创建)。
考虑一下示例:
function MyObject( name , message ){
this.name = name;
this.message = message.toString();
this.getName = function(){
return this.name;
};
this.getMessage = function(){
return this.message
};
}
上面的代码并未利用到闭包的好处,可以修改成如下:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};
不建议重新定义原型。可改成如下例子:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。