闭包的定义
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
上面两段代码运行结果是完全一样的。不同的是:makeFunc
函数中,内部函数 displayName()
在执行前,被外部函数返回。在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 makeFunc()
执行完毕,我们会认为 name
变量将不能被访问。然而,因为代码运行得没问题,所以很显然在 JavaScript 中并不是这样的。
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 求和。
闭包的应用
应用于面向对象编程
闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。
应用于Web开发
在 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;
<p>Some paragraph text</p>
<h1>some heading 1 text</h1>
<h2>some heading 2 text</h2>
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
用闭包模拟私有方法
编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern),下面创建一个计数器可以实现数字加减和查看数字:
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()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
上面计数器代码只创建了一个词法环境,为三个函数所共享Counter.increment,Counter.decrement
和 Counter.value
。该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为privateCounter
的变量和名为 changeBy
的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。这三个公共函数是共享同一个环境的闭包。因为JavaScript 的词法作用域,它们都可以访问 privateCounter
变量和 changeBy
函数。
下面改进一下,定义一个不立即执行的非匿名函数,用于创建计数器。这样可以创建多个计数器并且互相不影响:
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()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
上面两个计数器,counter1
和 counter2
是相互独立的,每个闭包都是引用自己词法作用域内的变量 privateCounter
。每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。
闭包的问题
问题来源
问题例子引用自廖雪峰JavaScript教程之闭包,在应用闭包时,需要注意一个问题,返回的函数并没有立刻执行,而是直到调用了f()
才执行。我们来看一个例子:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都添加到一个Array
中返回了。你可能认为调用f1()
,f2()
和f3()
结果应该是1
,4
,9
,但实际结果是:
f1(); // 16
f2(); // 16
f3(); // 16
全部都是16
!原因就在于返回的函数引用了变量i
,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i
已经变成了4
,因此最终结果为16
。
问题解析
首先我们弄懂上面代码的运行流程:首先var results = count();
之后,函数count
已经被调用了,所以一次执行函数内的各段代码:var arr = [];
,for (var i=1; i<=3; i++)
,这个for循环尤其值得注意。因为此时循环体执行了push方法,将一个个函数function () { return i * i;}
添加到数组内,但是这个函数并没有被调用,还只是一个变量,所以for循环依次执行,直到i = 4
。因为闭包,内部函数function () { return i * i;}
引用的i
就是外部变量,for循环中的i = 4
。所以,之后数组arr
内的函数的i
都是4。
调用函数count
后,变量results
已经是数组arr
了。数组里面元素依次是function f1() { return i * i;} function f2() { return i * i;} function f3() { return i * i;}
。但是三个函数都没有被调用,直到var f1 = results[0];
,此时function f1() { return i * i;}
开始执行,如上段所写,此时的i = 4
,所以,返回值就是16了。后面两个调用也是类似情况。
问题启示
返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。如果一定要引用循环变量,方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 1
f2(); // 4
f3(); // 9
上述代码中,避免在arr.push
方法中,实现了每次循环都立即将当前的参数i
传送给函数,然后立即塞进数组。这样就避免了i
最后才传给函数。注意,这里用了一个“创建一个匿名函数并立刻执行”的语法,才能及时绑定参数i
。
(function (x) {
return x * x;
})(3); // 9