定义
MDN
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
百度百科
闭包是能够读取其他函数内部变量的函数。例如在JavaScript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数”。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
闭包包含自由(未绑定到特定对象)对象,这些变量不是在这个代码块或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。
“闭包”一次来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。
阮一峰的网络日志
- 变量的作用域
要理解闭包,首先必须理解JavaScript特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。
JavaScript语言的特殊之处,就在于函数内部可以直接读取全局变量。
另一方面,在函数外部自然无法读取函数内的局部变量。
这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不使用的话,你实际上声明了一个全局变量。
- 如何从外部读取局部变量 处于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。
那就是在函数的内部,在定义一个函数。
闭包就是能够读取其他函数内部变量的函数。
由于在JavaScript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
全局作用域和函数作用域
全局作用域里面定义的变量在任何地方都可以访问。 函数作用域里面定义的变量只能在函数内部进行访问。 使用闭包可以在函数外部访问到函数作用域里面定义的变量。
如上: b和c是通过闭包访问到、 a是全局变量可以直接访问、d是内部变量可以直接访问
闭包的作用
- 可以让函数外部访问函数内部的变量
- 让变量的值保存在堆内存中
- 栈内存中变量一般在它的当前执行环境结束就会被销毁被垃圾回收, 而堆内存中的变量则不会,因为不确定其他地方是不是还有一些对它的引用。堆内存中的变量只有所有对它的引用都结束的时候才会被回收。
常见形式
闭包最常见方式就是在函数的内部创建另一个函数
V8是如何实现闭包的?
V8执行JavaScript代码,需要经过编译和执行两个阶段
- 编译过程: 是指V8将JavaScript代码转换为字节码或者二进制机器代码的阶段
- 执行阶段: 是指解析器解析执行字节码,或者是CPU直接执行二进制机器代码的阶段
惰性解析
在编译JavaScript代码的过程中,V8并不会一次性将所有的JavaScript解析为中间代码,这主要是基于以下两点:
- 如果一次解析和编译所有的JavaScript代码,过多的代码会增加编译时间,这会严重影响到首次执行JavaScript代码的速度,让用户感觉到卡顿;
- 解析完成的字节码和编译之后的机器代码都会放在内存中,如果一次性解析和编译所有的JavaScript代码,那么这些代码和机器将会一直占用内存,特别是在手机普及的年代,内存是非常宝贵的资源。
基于以上的原因,所有主流的JavaScript虚拟机都实现惰性解析。 所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成AST和字节码,而仅仅生成顶层代码的AST和字节码。
预解析器
V8引入预解析器,比如当解析顶层代码的时候,遇到一个函数,那么预解析器并不会直接跳过该函数,而是对函数做一次快速的预解析, 其主要目的有两个:
- 判断当前函数是不是存在一些语法上的错误
- 检查函数内部是否引用了外部变量,如果引用了外部变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用。
闭包的应用场景
实现setTimeout回调函数传递参数
for(var i=0; i<5; i++){
setTimeout(function fun(){
console.log(i)
}, 1000)
}
上面代码输出的结果是5 5 5 5 5
for(var i=0; i<5; i++){
function outter(n){
return function inner(){
console.log(n)
}
}
const f = outter(i)
setTimeout(f, 1000)
}
上面代码输出的结果是0 1 2 3 4
- V8将setTimeout的回调函数fun添加到延迟队列,并记录创建时间和延迟时间;
- 到达指定时间后会将回调函数添加到消息队列(即宏任务队列)中
防抖节流
//防抖
// 在指定时间里,保留最后一次执行
function debounce(fn, delay = 300){
let timer //闭包引用的外界变量
return function () {
const args = arguments
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, args)
}, delay);
}
}
模拟块级作用域
function fn(){
(function(){
for(var i=0; i<5; i++){
console.log(i)
}
})();
console.log("输出:", i) // ReferenceError: i is not defined
}
fn()
用于在对象中创建私有变量
var obj = (function(){
let count = 0
function setter(){
count ++
}
function getter(){
return count
}
return {
set : setter,
get : getter
}
})()
console.log(obj.count) // undefined
console.log(obj.get()) // 0
obj.set()
console.log(obj.get()) // 1