简介
这是《你不知道的JavaScript》系列的第二个比较重要的内容,闭包。在没有仔细研究过闭包的机制前,我对他的了解可能仅仅是函数中返回一个函数,且该函数引用了上一个函数的成员变量
。下面我就按我对书中知识的理解做一下总结
闭包
相信了解闭包的同学都是通过下面这个函数来认识闭包的:
function add () {
var count = 0
return function fn() {
count++
console.log(count)
}
}
var a = add()
a() // 1
a() // 2
上面这个函数实现的其实就是一个累加器的功能。
闭包实现的原理实际是跟JavaScript的词法作用域有关的,我们仔细观察 add 函数的词法作用域
在这里面,fn内部引用了父级作用域的count属性,并且在函数中最终返回了 fn 函数。这会造成什么影响呢?
作用域缓存
一般而言,当一个函数在执行的时候会经历这么几个过程:
- 开辟内存空间来放置执行代码
- 执行代码
- 执行完成,销毁对应的内存空间
所以当函数执行完成后,其在内存中开辟的空间会销毁,也就无法在访问其作用域中的成员变量
闭包中的作用域
但是它不走寻常路,在函数执行过程中,fn 是可以调用 add 作用域中的 count 成员(作用域链)。fn 不仅内部调用了 count,而且在 add 外部还被一个 a 变量接收了。这时候,浏览器的回收机制会认为:“哎?fn 还可能会使用 count,还不能回收啊”。这就会导致 add 函数执行时候生成的作用域没办法被销毁,而是一直存在内存中等待 fn 调用或者 fn 释放。
我们再来看看下面的代码:
var a = null
function add () {
var count = 0
a = function fn() {
count++
console.log(count)
}
}
a()
你认为上面的代码能不能形成闭包呢?
答案是:可以的
why?
我们仔细的研究 add 的词法作用域和执行时候的作用域就会发现
- add 作用域内部的 fn 函数同样引用了 count
- fn 同样被外部作用域的 a 所引用(接收)
这导致 add 作用域无法有效的释放而形成闭包。
误区
有人可能会想问,那我用外部变量来引用 count 能不能实现闭包呢?(我也这么想过)
// 情况1
var a = null
function add () {
var count = 0
a = count
}
// or
// 情况2
var a = null
function add () {
var data = {}
return data
}
a = add()
答案是:不可以
首先我们要明确,fn 引用的实际是 count,而不仅仅是 count 的值。也就是说 fn 实际引用的是 count 在内存中的地址。而在 情况1 和 情况2 中,a 对 count 或 data 的引用都只是引用对应的值,而不是它本身。
总结
总的来说,当函数中的某些成员变量被内部某些函数调用,并且该函数被外部其他变量引用(或者说该函数可以在别处调用时)就会形成闭包。
如何消除闭包
在闭包中很关键的一点是:函数内部的函数可以在别处调用
,所以我们可以从这个地方入手
function add () {
var count = 0
return function fn() {
count++
console.log(count)
}
}
var a = add() // 产生了闭包
a() // 1
a() // 2
a = null // 取消 a 与 fn 的联系,这个时候浏览器回收机制就能回收闭包空间
只要我们将 fn 与外部成员变量的引用关系取消,浏览器就能帮助我们回收对应的内存空间。
模块化
闭包的一个重要的使用就是模块化,它可以将成员变量的命名私有化,不会影响全局
function module() {
let name = '模块化'
function change(n) {
name = n
}
function getName() {
console.log(name)
}
return {
change,
getName
}
}
var module1 = module()
// 我们可以通过模块化访问私有变量 name,并通过对应的 api 方法来获取或者修改这个值
module1.getName() // '模块化'
module1.change('modules')
关于模块化,有许多改进或者优化的地方,这里不做更多的阐述,如果有机会再来讨论他的细节!