JavaScript 闭包

一般而言,因为存在作用域链的关系,JavaScript 函数内部可以访问函数外部的变量,而函数外部往往无法访问到函数内部的变量。

而 JavaScript 闭包的作用,就是通过改变作用域链的方式,使得函数外部可以访问到函数内部的作用域。详见下方的例子:

1
2
3
4
5
6
7
8
9
10
var f = function () {
var x = "x in f"
var getX = function () {
return x
}
return getX
}

var get = f()
console.log(get()) // 'x in f'

JavaScript 闭包,指的是一个函数内部的函数,通过将这个内部函数暴露给外界(如作为返回值返回、立即执行函数等),外界即可访问到这个函数内部的作用域。关于作用域的解释,可以详见我的另一篇博客:JavaScript 执行环境和作用域。

上图中:

  • 灰色部分表示了作用域链的构成,从最内部的getX到最外层的全局作用域Global。只能由作用域链顶端访问作用域链底端。
  • 蓝色部分表示了一次赋值,此时将f内部的函数赋值给了作用域链底端的全局作用域,于是全局作用域链又能访问到f内部的变量了。

变量的生命周期

全局变量,会被当做 Global 变量的一个属性一直保存下来,于是它的生命周期是永久的。

局部变量,指的是函数内部的变量,一般情况下,当函数调用结束后,该变量就会被垃圾回收机制回收,再次调用该函数的时候,会再创建一个变量。

闭包的一个关键作用,就是能延长一个变量的生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
var f = function () {
var count = 0
return function () {
count++
return count
}
}

var count = f()
console.log(count()) // 1
console.log(count()) // 2
console.log(count()) // 3

事实上,变量依附于它存在的作用域,一旦该作用域从作用域链中弹出,这个作用域中的变量也随即销毁。

闭包经典问题

问题一:

需要给多个按钮绑定不同的事件:

1
2
3
<button id="email"></button>
<button id="name"></button>
<button id="age"></button>
1
2
3
4
5
6
7
8
9
10
11
var text = [
{ id: "email", text: "jackie@mail.com" },
{ id: "name", text: "Jackie" },
{ id: "age", text: "18" },
]
for (var i = 0; i < text.length; i++) {
var item = text[i]
document.getElementById(item.id).onclick = function () {
alert(item.text)
}
}

这时你会发现,点击任何一个 button,输出的都是 18,这和 JavaScript 的作用域机制有关,那么要怎么达到我们想要的效果,使得这几个回调函数不共享一个作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var text = [
{ id: "email", text: "jackie@mail.com" },
{ id: "name", text: "Jackie" },
{ id: "age", text: "18" },
]
var click = function (item) {
return function () {
alert(item.text)
}
}
for (var i = 0; i < text.length; i++) {
var item = text[i]
document.getElementById(item.id).onclick = click(item)
}

当然,es6 标准中提出了更便捷、规范的解决方案,就是用let关键字:

1
2
3
4
5
6
7
8
9
10
11
var text = [
{ id: "email", text: "jackie@mail.com" },
{ id: "name", text: "Jackie" },
{ id: "age", text: "18" },
]
for (var i = 0; i < text.length; i++) {
let item = text[i]
document.getElementById(item.id).onclick = function () {
alert(item.text)
}
}

问题二:

循环内的异步调用(本质和前者无区别)

1
2
3
4
5
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}

对应的正确解决方法,利用匿名函数存储变量 i

1
2
3
4
5
6
7
for (var i = 0; i < 3; i++) {
;(function (i) {
setTimeout(function () {
console.log(i)
}, i * 1000)
})(i)
}

闭包的负面影响

闭包对脚本性能具有负面影响,包括处理速度和内存消耗。故而,在没有必要创建闭包的时候,就最好不要创建闭包。例子来自MDN 闭包:

1
2
3
4
5
6
7
8
9
10
11
function MyObject(name, message) {
this.name = name.toString()
this.message = message.toString()
this.getName = function () {
return this.name
}

this.getMessage = function () {
return this.message
}
}

改写如下:

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