理解闭包

什么是闭包

函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内。 – 摘自《JavaScript权威指南》

  • 闭包就是能够读取其他函数内部变量的函数。
  • 闭包可以简单理解成“定义在一个函数内部的函数“。
  • 在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

    闭包的用途/优点

  • 可以读取函数内部的变量

  • 让这些变量的值始终保持在内存中,不会在原函数调用后被自动清除

使用闭包的注意点/缺点

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
  • 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

了解JavaScript中的作用域

函数作用域
1
2
3
4
5
6
7
8
9
10
11
12
13
function test(o){
var i = 0;
if(typeof o == "object"){
var j = o;
for(var k = 0; k < 10; k++){
console.log(k)
}
console.log(k)
}
console.log(j)
}
test({a:2})

JavaScript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。JavaScript函数里声明的所有变量(但不涉及赋值)都被“提前”至函数体的顶部。

1
2
3
4
5
6
var scope = "global";
function f(){
console.log(scope); // 输出 “undefined”
var scope = "local";
console.log(scope);
}

以上代码中第一个输出不是 global,是因为变量提前声明了,它等价于以下代码:

1
2
3
4
5
6
7
var scope = "global";
function f(){
var scope;
console.log(scope); // 输出 “undefined”
scope = "local";
console.log(scope);
}

由于JavaScript没有块级作用域,因此将变量声明放在函数体顶部,而不是将声明靠近放在使用变量之处。这种做法似的他们的源代码非常清晰地反映了真实的变量作用域。

实例一

1
2
3
4
5
6
7
8
9
10
11
12
 function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

类比:

1
2
3
4
5
6
7
8
var object = {
a:1,
b:2,
c:3,
D:function(){ this.c = 4}
}
var newObject = object; // 这时newObject有了一个作用域,内容和object一样,但指向不一样。
newObject.D() // 此时使用一个函数去改变newObject的值,再将newObject打印出来,里面的值已经是改变之后的

以上object好比第一个例子中的f1,当赋值给result的时候,已经有了一个新的作用域,因为result是全局变量,因此不会执行函数之后就被销毁。

实例二

利用闭包实现的私有属性存取器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function addPrivateProperty(o,name,predicate){
var value ;
o['get'+name]=function(){
return value;
}
o['set'+name]=function(v){
if(predicate && !predicate(v)){
throw Error('这是一个错误值');
} else {
return value = v;
}
}
}
var o = {};
function predicate (v) {
return typeof(v) === 'string';
}
addPrivateProperty(o,'Name',predicate);
o.setName("ABIGALE");
console.log(o.getName());

实例三

关联到闭包的作用域链都是”活动的”,嵌套的函数不会将作用域内的私有成员复制一份,也不会对所绑定的变量生成静态快照。

1
2
3
4
5
6
7
8
9
function constfuncs() {
var funcs = [];
for(var i = 0; i < 10; i++){
funcs[i] = function() { return i; }
}
return funcs;
}
var funcs = constfuncs();
funcs[5](); // 10

实例四

1
2
3
4
5
6
7
8
9
10
11
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

实例五

1
2
3
4
5
6
7
8
function sayAlice() {
var sayAlert = function() {
console.log(alice);
}
alice = 'Hello Alice';
return sayAlert;
}
sayAlice()(); // Hello Alice

如果一个变量没有声明,那它就为全局变量。

1
2
3
4
5
6
7
8
9
var alice = 'Hi Alice';
function sayAlice() {
var sayAlert = function() {
console.log(alice);
}
alice = 'Hello Alice';
return sayAlert;
}
sayAlice()(); // Hello Alice

即使在最外层定义了 alice ,但函数内又给它赋值了。

1
2
3
4
5
6
7
8
9
10
var alice = 'Hi Alice';
function sayAlice() {
var sayAlert = function() {
console.log(alice);
}
alice = 'Hello Alice';
return sayAlert;
}
alice = 'Good Moroning
sayAlice()(); // Hello Alice

虽然最后又给 alice 赋值了,但它的执行是在函数 sayAlice 之前,所以我们这里看到它的输出仍然为 Hello Alice

参考