这一次,彻底理解闭包
闭包的概念
闭包指的是有权访问另一个函数作用域中的变量的函数。
::: MDN 定义:闭包是指那些可以访问自由变量的函数。 :::
何为自由变量,指的是在函数中使用,但既不是函数也不是函数局部变量的变量。
因此更权威的定义是:
闭包 = 函数 + 函数能访问的自由变量
闭包 = 函数 + 函数能访问的自由变量
从理论角度:
从本质上理解,所有的函数在定义的时候就将执行上下文数据保存起来了,它的外部作用域的变量就变成了自由变量。
从实践角度,以下的两种情况形成了闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
js
var scope = 'global scope'
function checkscope() {
var scope = 'local scope'
function f() {
return scope
}
return f
}
var foo = checkscope()
foo()
var scope = 'global scope'
function checkscope() {
var scope = 'local scope'
function f() {
return scope
}
return f
}
var foo = checkscope()
foo()
在这个例子中,f 的执行上下文维护了一个作用域链:[A0, checkscopeContext.AO, globalContext.VO]
,正因为这个作用域链的存在,f 读取到了 checkscope 中变量的值,即使 checkscopeContext 被销毁了,js 依然会让它的变量活跃在内存中,成为自由变量。
闭包的使用场景
封装工具函数,返回一个新函数(例如防抖和节流)
在模块化的开发中,所有的文件都是在函数中运行,模块导出了方法,被方法访问到的模块顶层的变量,都会形成闭包,可以用于模块的状态管理和通信
闭包的创建和销毁
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
在上述例子中,fn 函数调用完毕后,foo 函数会自动销毁,但是 foo 函数中的 name 不会被销毁。这是因为垃圾回收机制(GC)作用时,被另一个作用域引用的变量不会被回收,除非 bar 函数解除调用。如下所示
js
fn = null // 销毁fn防止内存泄露
fn = null // 销毁fn防止内存泄露
拓展:通过闭包访问的对象一定不能被修改吗
js
let foo = (function () {
let obj = {
a: 1,
b: 2
}
return {
get(k) {
return obj[k]
}
}
})()
// 给obj添加一个key:c
let foo = (function () {
let obj = {
a: 1,
b: 2
}
return {
get(k) {
return obj[k]
}
}
})()
// 给obj添加一个key:c
可以通过劫持原型链得到对象本身
js
// 给obj添加一个key:c
Object.defineProperty(Object.prototype, 'getThis', {
get() {
return this
}
})
// foo.get('c')
let obj = foo.get('getThis')
obj.c = 3
console.log(foo.get('c'))
// 给obj添加一个key:c
Object.defineProperty(Object.prototype, 'getThis', {
get() {
return this
}
})
// foo.get('c')
let obj = foo.get('getThis')
obj.c = 3
console.log(foo.get('c'))
防范方式:断掉 obj 的原型链
js
let foo = (function () {
let obj = Object.create(null)
obj.a = 1
obj.b = 2
// 或者采用下面这种方式断掉原型链
// obj.__proto__ = null;
return {
get(k) {
return obj[k]
}
}
})()
let foo = (function () {
let obj = Object.create(null)
obj.a = 1
obj.b = 2
// 或者采用下面这种方式断掉原型链
// obj.__proto__ = null;
return {
get(k) {
return obj[k]
}
}
})()
小结
闭包的本质是函数执行上下文的创建,函数在定义的时候就会确定执行上下文,函数使用到的外部变量也被标记引用。
只要函数没有被销毁,外部变量就将一直存在,而无法被垃圾回收。