概念

函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内

Javascript允许嵌套内部函数—即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。

实例:

1
2
3
4
5
6
7
8
9
10
var scope = 'global';
function checkScope() {
var scope = 'local';
function f() {
console.log(scope);
}
return f;
}
var func = checkScope();
func(); // => local

JavaScript中的函数会形成闭包。这个环境包含了这个闭包创建时所能访问的所有局部变量。在我们的例子中,checkScope返回的是内部函数f本身,funcf函数的引用,而f仍可访问其词法作用域中的变量,即可以访问到checkScope作用域内定义的scope。由此,当func被调用时,checkScope中的scope仍可被访问

闭包可能带来的问题

在循环中创建闭包

在实际开发中可能会遇到一个问题,如何对相似的元素绑定事件,比如:

1
2
3
4
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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元素绑定onfoucs时,会发现所有的提示均为"Your age (you must be over 16)",这就是闭包带来的问题——闭包只能取得包含函数中任何变量的最后一个值

将该问题做一个抽象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function f() {
var result = [];
for (var i = 0; i < 5; i++) {
result[i] = function () {
console.log(i);
};
}
return result;
}

f()[0](); // => 5
f()[1](); // => 5
f()[2](); // => 5
f()[3](); // => 5
f()[4](); // => 5

事实上,我们想要的结果应该是f()[i]() == i。出现全部是5的原因是:当f()返回后,变量i的值是5,此时通过循环创建的每个函数result[i]()都引用保存着同一个变量i,所有每个函数内部的i都是5

想要实现f()[i]() == i有以下几个方法:

  • 使用更多闭包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function print(i) {
    console.log(i);
    }

    function f() {
    var result = [];
    for (var i = 0; i < 5; i++) {
    result[i] = print(i);
    }
    return result;
    }

    f();
  • 使用匿名闭包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function f() {
    var result = [];
    for (var i = 0; i < 5; i++) {
    result[i] = (function () {
    console.log(i);
    })(i)
    }
    return result;
    }

    f();
  • ES6中可以使用let声明i

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function f() {
    var result = [];
    for (let i = 0; i < 5; i++) {
    result[i] = function () {
    console.log(i);
    };
    }
    return result;
    }

    f()[2](); // = 2

性能问题

由于闭包会携带包含它的函数的作用域,会比其他函数占用更多内容,过度使用闭包,会导致内存占用过多,在处理速度和内存消耗方面对脚本性能具有负面影响。因此
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的

this指向问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var scope = 'global';

var obj = {
scope: 'local',

getScope1: function () {
return this.scope;
},

getScope2: function () {
return function () {
return this.scope;
};
}
}

console.log(obj.getScope1()) // => local
console.log(obj.getScope2()()) // => global

匿名函数的执行环境具有全局性,因此其this通常指向window,可以通过把外部作用域中的this保存在一个闭包能访问的变量中

1
2
3
4
5
6
7
8
9
10
11
12
13
var scope = 'global';

var obj = {
scope: 'local',

getScope2: function () {
var self = this;
return function () {
return self.scope;
};
}
}
console.log(obj.getScope2()()) // => local

内存泄漏

IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程,故闭包在IE的这些版本中会导致一些特殊的问题

具体来说,如果闭包的作用域链中保存着一个HTML元素,那么该元素无法销毁

1
2
3
4
5
6
function assignHandler() {
var element = document.getElementById('elementId');
element.onclick = function () {
alert(element.id);
};
}

可以改成:

1
2
3
4
5
6
7
8
9
10
function assignHandler() {
var element = document.getElementById('elementId');
var id = element.id;

element.onclick = function () {
alert(id);
};

element = null;
}

闭包的用途

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

用闭包模拟私有方法

可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问,还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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(),
Counter2 = makeCounter();
console.log(Counter1.value()); // => 0
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); // => 2
Counter1.decrement();
console.log(Counter1.value()); // => 1
console.log(Counter2.value()); // => 0
console.log(makeCounter.privateCounter) // => undefined

两个计数器counter1counter2各自独立性,每个闭包都是引用自己词法作用域内的变量privateCounte,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量

以这种方式使用闭包,提供了许多与面向对象编程相关的好处——特别是数据隐藏和封装

实现类和继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Person() {
var name;

return {
getName: function () {
return name;
},
setName: function (newName) {
name = newName;
}
}
};

var p1 = new Person();
p1.setName("Tom");
console.log(p1.getName()); // => Tom

var Person1 = function () { };
Person1.prototype = new Person();
Person1.prototype.Say = function () {
console.log('Hello,my name is ' + this.getName());
};
var p2 = new Person1();
p2.setName('Eddy');
p2.Say(); // => Hello,my name is Eddy
console.log(p2.getName()); // => Eddy