闭包

232 阅读5分钟

定义

MDN

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。

也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

百度百科

闭包是能够读取其他函数内部变量的函数。例如在JavaScript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数”。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

闭包包含自由(未绑定到特定对象)对象,这些变量不是在这个代码块或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。

“闭包”一次来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。

阮一峰的网络日志

  • 变量的作用域

要理解闭包,首先必须理解JavaScript特殊的变量作用域。

变量的作用域无非就是两种:全局变量和局部变量。

JavaScript语言的特殊之处,就在于函数内部可以直接读取全局变量。

另一方面,在函数外部自然无法读取函数内的局部变量。

这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不使用的话,你实际上声明了一个全局变量。

  • 如何从外部读取局部变量 处于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。

那就是在函数的内部,在定义一个函数。

闭包就是能够读取其他函数内部变量的函数。

由于在JavaScript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

全局作用域和函数作用域

全局作用域里面定义的变量在任何地方都可以访问。 函数作用域里面定义的变量只能在函数内部进行访问。 使用闭包可以在函数外部访问到函数作用域里面定义的变量。

image.png 如上: b和c是通过闭包访问到、 a是全局变量可以直接访问、d是内部变量可以直接访问

闭包的作用

  1. 可以让函数外部访问函数内部的变量
  2. 让变量的值保存在堆内存中
  • 栈内存中变量一般在它的当前执行环境结束就会被销毁被垃圾回收, 而堆内存中的变量则不会,因为不确定其他地方是不是还有一些对它的引用。堆内存中的变量只有所有对它的引用都结束的时候才会被回收。

常见形式

闭包最常见方式就是在函数的内部创建另一个函数

V8是如何实现闭包的?

V8执行JavaScript代码,需要经过编译和执行两个阶段

  • 编译过程: 是指V8将JavaScript代码转换为字节码或者二进制机器代码的阶段
  • 执行阶段: 是指解析器解析执行字节码,或者是CPU直接执行二进制机器代码的阶段

惰性解析

在编译JavaScript代码的过程中,V8并不会一次性将所有的JavaScript解析为中间代码,这主要是基于以下两点:

  1. 如果一次解析和编译所有的JavaScript代码,过多的代码会增加编译时间,这会严重影响到首次执行JavaScript代码的速度,让用户感觉到卡顿;
  2. 解析完成的字节码和编译之后的机器代码都会放在内存中,如果一次性解析和编译所有的JavaScript代码,那么这些代码和机器将会一直占用内存,特别是在手机普及的年代,内存是非常宝贵的资源。

基于以上的原因,所有主流的JavaScript虚拟机都实现惰性解析。 所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成AST和字节码,而仅仅生成顶层代码的AST和字节码。

预解析器

V8引入预解析器,比如当解析顶层代码的时候,遇到一个函数,那么预解析器并不会直接跳过该函数,而是对函数做一次快速的预解析, 其主要目的有两个:

  1. 判断当前函数是不是存在一些语法上的错误
  2. 检查函数内部是否引用了外部变量,如果引用了外部变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用。

闭包的应用场景

实现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