闭包
原文链接:http://wwsun.github.io/posts/javascript-closure.html
在JavaScript中,闭包是个常令新手困惑的术语,并且很容易和匿名函数相混淆。一句话来讲, 闭包是指有权访问另一个函数(嵌套函数)作用域中的变量的函数。本文将着重解释JavaScript中的闭包概念, 及其用法。
语法作用域
处于种种原因,有时候我们需要得到函数内部的局部变量。通常情况下,这是无法做到的(函数内部变量属于局部变量), 只有通过变通方法才能实现。我们需要在函数内部再定义一个函数。比如有下面这个例子:
function init() {
var name = "Mozilla"; // name是一个局部变量
// 内部函数,它是一个闭包
function displayName() {
console.log(name); // displayName()使用了外部函数中定义的变量
}
displayName();
}
init();
上面的代码中,函数displayName是函数init的内函数,我们发现,可以在displayName中访问init的局部变量,但反过来却不可以。 也就是displayName内部的局部变量对init是不可见的。这就是Javascript语言特有的”链式作用域”结构(chain scope), 子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
简单的说,内函数displayName()之所以能访问外函数中的name变量是因为JavaScript中的词法作用域的作用: 在avaScript中,变量的作用域是由它在源代码中所处位置决定的(词法),并且嵌套的函数可以访问到其外层作用域中声明的变量。
闭包
既然displayName可以访问init的局部变量,那么只要把displayName返回, 我们不就可以在init的外部读取到它的内部变量了吗!
现在考虑下面这个例子:
function makeFunc() {
var name = "Mozilla";
// 内函数可以访问外部函数的局部变量
function displayName() {
console.log(name);
}
return displayName; // 返回内部函数
}
var myFunc = makeFunc(); // myFunc成为了闭包
myFunc(); // Mozilla
代码的运行结果并没有变,所不同的是,内函数displayName()在执行之前被外函数返回了。
这段代码看起来别扭却能正常运行。通常,函数中的局部变量仅在函数的执行期间可用。 一旦 makeFunc() 执行过后,我们会很合理的认为 name 变量将不再可用。虽然代码运行的没问题,但实际并非如此。
对这一问题的解释就是,myFunc变成成一个闭包了。闭包是一种特殊的对象,它由两部分构成:
函数:displayName()
创建该函数的环境:环境由闭包创建时在作用域中的任何局部变量组成,即局部变量name
你可以简单的理解为:闭包就是能够读取其他函数内部变量的函数。而闭包通常是“定义在函数内部的函数”。 我们可以认为,闭包充当了函数内部和函数外部连接起来的桥梁。
创建闭包的一个常见方式就是在一个函数内部创建另一个函数。
闭包用途
- 读取函数内部的变量
- 让这些变量的值始终保存在内存中
注意,因为闭包会使得函数中变量都保存在内存中,因此不能滥用闭包,否则会造成内存溢出。
下面是一个更有趣的例子,一个makeAddr函数
function makeAddr(x) {
return function(y) {
return x+y; // 在内函数中访问外函数中的变量x
};
}
// add5和add10都是闭包
var add5 = makeAddr(5);
var add10 = makeAddr(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
使用闭包来仿造私有方法
在Java中你可以声明私有方法,私有方法表示只能在当前类中使调用该方法。 但是JavaScript并没有提供一个直接的方法去声明私有方法,但是可以使用闭包来仿造私有方法。 私有方法的作用不仅仅在于可以有效的限制代码的访问, 也为管理你的全局命名空间提供了一种强有力的方式,使得无关的方法不被公开的暴露出来。
下面来看如何定义可以访问私有函数和变量的公共函数,我们使用闭包来达到这一目的, 这也被称为模块模式:
// 所定义的匿名函数表达式会立即执行,并将返回对象(含有三个方法)赋值给counter
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()); // 0
counter.increment();
counter.increment();
console.log(counter.value()); // 2
counter.decrement();
console.log(counter.value()); // 1
这里有好多细节。在以往的示例中,每个闭包都有它自己的环境;而这次我们只创建了一个环境,为三个函数所共享, 分别是:counter.increment, counter.decrement, counter.value。
被共享的环境是在一个匿名函数的函数体内被创建的,这将会在其被定义后立即执行(立即执行函数 IIFE)。 这个共享环境包含两个私有项:变量privateCounter和函数changeBy, 外界无法直接访问匿名函数内部的这两个私有项。取而代之的是,只能通过访问三个公开的接口函数,也就是被匿名函数返回的三个函数。
这三个公共函数是共享同一个环境(共享相同的privateCounter和changeBy())的闭包, 而它们之所以都可以访问变量privateCounter和函数changeBy,是因为JavaScript词法作用域的作用。
您应该注意到了,我们定义了一个匿名函数用于创建计数器,然后直接调用该函数,并将返回值赋给 counter 变量。 也可以将这个函数保存到另一个变量中,以便创建多个计数器。
// 此时我们没有使用立即执行函数表达式,而是直接定义了一个匿名函数
var counter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
};
};
// 我们可以借助counter创建两个不同的计数器
var counter1 = counter();
var counter2 = counter();
// 每个计数器都有自己独立的环境
counter1.decrement();
counter1.decrement();
counter2.increment();
console.log(counter1.value()); // -2
console.log(counter2.value()); // 1
可以发现,这两个计数器是彼此独立的,它们的环境在函数counter()在调用期间是彼此不同的。 闭包变量privateCounter在每一次包含不同的实例。
利用这种方式使用闭包可以向OOP编程一样提供非常多的好处,尤其是数据隐藏和封装。
立即执行函数 IIFE
通过IIFE这种方式我们可以构造块作用域,通常的模式为:
// 这是个立即执行的函数表达式,相当于Java中的一个普通的块作用域(里面的变量为局部变量)
(function () {
var tmp = ...; // 这里的tmp就是个局部变量
}());