闭包
闭包 (closure) 是指函数声明时会绑定外部环境,无论传递到哪里调用,函数内都能访问到声明时的外部变量 (外部环境)。
通常用于函数一等公民的编程语言。
作用域与词法环境
变量有 3 种作用域,只在所处的作用域中可见:
- 全局作用域
- 函数作用域
- 块作用域
每个作用域有一个称为词法环境 (Lexical Environment) 的隐藏对象。
词法环境有 2 部分:
- 环境记录 (Environment Record):作用域内所有局部变量
- 对外部词法环境的引用 (全局词法环境无外部引用)
对于变量来说,词法环境预先读取所有声明的变量,设为未初始化 (Uninitialized) 状态,在遇到声明之前不能引用,就像该变量不存在一样。遇到声明语句后,变量可以被读取和修改。
对于函数来说,词法环境会预先读取并初始化所有声明的函数,允许调用下方声明的函数。
访问变量时,首先搜索当前作用域的词法环境,其次是外部以及更外部,直到全局。
如果都没找到,严格模式下报错,非严格模式下会声明一个全局变量。
闭包
函数作用域也有自己的词法环境,使用 [[Environment]]
隐藏属性存储对外部环境的引用,创建函数时会自动设置这个属性。
不管这个函数被传递到什么地方调用,调用时的外部词法环境永远是声明时的外部环境,与调用时的外部环境无关,保证了函数的可用性。
使用场景
任何需要访问函数外变量的场景,核心是创建私有变量、延长变量的生命周期。
- 各种回调函数
- setTimeout
- addEventListener
- 返回函数的函数,返回的函数需要访问函数内声明的变量
- 防抖、节流
- 柯里化
- 模拟私有属性和方法:IIFE 返回公有方法,需要访问 IIFE 函数内的私有属性和方法
- 解决 var 没有块作用域的问题
- 外面包一层 IIFE 传入参数来固定 var 变量的值
垃圾回收
函数执行结束后,如果没有其他环境引用这个函数环境,该环境会从内存中删除(垃圾回收)。
对于返回函数的函数,由于返回的函数引用了外部函数的词法环境,就不会发生垃圾回收,直到返回的函数不再使用,才会回收外部函数。
js
function f() {
let value = 123;
return function () {
console.log(value);
};
}
// g 函数存在时,f 不会被回收
let g = f();
// g 和 f 都可以回收了
g = null;
目前主流的 V8 引擎会对这种情况做一些优化,如果返回的函数没有引用外部环境的任何变量,则会将外部函数从外部环境链上删除,并允许回收外部函数。
js
let value = 'Surprise!';
function f() {
let value = 'the closest value';
function g() {
// 在 Console 中输入 value 会显示 "Surprise!",意味着 f 被回收了
debugger;
}
return g;
}
let g = f();
g();