The Final Closure

什么是闭包

闭包是指有权访问另一个函数作用域中的变量的函数。嗯,闭包是个函数,能够访问另外函数中的变量,就这么简单。

闭包和作用域

1
2
3
4
5
6
7
8
function say(name) {
return function() {
return name
}
}

var a = say('hello')
console.log(a())

为什么当内部匿名函数被返回后,而且在其他地方被调用,仍能访问变量 name 呢?这里得从作用域链说起。

当某个函数第一次被调用时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([[scope]]),然后使用 this,arguments 和其他命名参数来初始化函数的活动对象。

每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在。而像 say() 函数这样的局部环境的变量对象,则只在函数的执行过程中存在。在创建 say() 时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[scope]]属性中。当调用 say() 时,会为函数创建一个执行环境,然后通过复制函数的[[scope]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象被创建并被推入执行环境的作用域链的前端。对于 say() 函数的执行环境而言,其作用域链中包含两个变量对象,本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

在函数中访问一个变量时,会从作用域链中搜索具有相应名字的变量。一般来讲,函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象),但闭包不同。

在一个函数内部定义的函数将包含其外部函数的活动对象添加到它的作用域链中。在 say() 函数内部定义的匿名函数的作用域中,实际会包含 say() 的活动对象。在匿名函数从 say() 中被返回后,它的作用域链被初始化为包含 say() 函数的活动对象和全局变量对象。say() 函数执行完毕后,其活动对象不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。当 say() 函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然留在内存中,直到匿名函数被销毁,say() 的活动对象才会被销毁。

用自己的话总结一下:
当一个函数被调用时,会创建一个执行环境和作用域链,作用域链前端是当前执行环境中的变量对象。作用域链中的下一个变量对象来自外部包含环境,最后一个变量对象来自全局执行环境。函数中的变量如果在自己的执行环境中没找到,则沿着作用域链一路往上直到全局。

闭包的问题

1
2
3
4
5
6
7
8
9
10
function bindClick() {
var buttons = document.getElementsByTagName('button')
for(var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log(i)
})
}
}

bindClick()

每个 button 打印值都是3,why?就是因为 click 方法的回调函数中 i 的值都是引用自上级作用域链,即 bindClick 函数中变量对象中的 i ,当 click 方法被触发时,i 活动变量的值早就变成 3 了

解决
给每个绑定事件自己的作用域:

1
2
3
4
5
(function(i) {
buttons[i].addEventListener('click', function() {
console.log(i)
})
})(i)

使用块作用域:

1
2
3
4
5
6
var buttons = document.getElementsByTagName('button')
for(let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log(i)
})
}

闭包的使用场景

  • Function factories
1
2
3
4
5
6
7
8
9
10
11
12
function makeAdder(x) {
return function(y) {
return x + y
}
}

const add5 = makeAdder(5)
const add10 = makeAdder(10)

console.log(add5(2)) // 7
console.log(add10(2)) // 12
`
  • Module pattern private method
1
2
3
4
5
6
7
8
9
const salary = (function() {
let salary = 5000

return {
getSalary: function () {
return salary
}
}
})()

闭包的弊端

过度使用闭包,显然内存中会存储过多的外部作用域变量,影响性能