闭包:JS 里那个让人头大的"坑"

0 阅读4分钟

一、深入了解闭包,从作用域链开始

  1. 什么是作用域链?
  • 每一个执行上下文的变量环境中都存在一个 outer 指针,用来指向外部的执行上下文(指向词法环境)。当 V8 引擎在查找一个变量时,在当前执行上下文中没有找到,就会顺着 outer 所指向的执行上下文继续查找,以此类推,直到找到全局为止。我们把这个查找的链条关系称为作用域链

先看一段代码

let count = 1  // 全局作用域
function main(){
    let count = 2  // main作用域
    function bar(){
        let count = 3  // bar作用域
        foo()  // 调用外部定义的foo
    }
    bar()
}
function foo(){
    console.log(count);  // 输出: 1 (访问全局count)
}
main()
  1. 这段代码执行时先在栈中创建一个全局执行上下文,全局变量与函数声明有count变量,main函数和foo函数,注意letconst定义的变量是在词法环境中,outer指针为null(全局已经是最外层)
  2. 然后全局调用了main函数,再创建一个main 函数执行上下文,全局变量与函数声明有count变量,bar函数,outer指针指向全局词法环境
  3. main函数内部又调用了bar函数,此时再创建一个bar 函数执行上下文,全局变量与函数声明有count变量,outer指针指向main 函数词法环境(定义在main函数里)。
  4. bar函数内部又调用了foo函数,最后又创建一个foo 函数执行上下文outer指针指向全局函数词法环境(定义在全局)。
  5. foo函数执行console.log(count) ,此时会先在当前执行上下文中找count变量,由于没有找到,会顺着 outer 找,由于outer指向全局,全局count值为 1,因此最终输出结果为 1。
  • 具体流程如下图 js_execution_context.png~tplv-a9rns2rl98-image-face-cut_0_0_1785_1185_1785_1185.jpeg

二、什么是闭包

  1. 一个函数执行完毕后,它的执行上下文会被销毁
  2. 根据作用域的查找规则,内部函数一定可以访问外部函数中的变量
  • 当一个外部函数中的内部函数被拿到外部函数之外来执行,哪怕外部函数执行完毕了,被内部函数引用的那部分变量依然需要被保留。我们把这部分变量的集合称之为闭包

代码示例

function add() {
    let count = 0

    return function() {
        count++
        return count
    }
}

var bar = add()
console.log(bar()) // 1
console.log(bar()) // 2
console.log(bar()) // 3
  1. 这段代码执行时先在栈中创建一个全局执行上下文,全局变量与函数声明有bar变量、add函数,outer指针为null
  2. 然后全局执行add()函数,创建一个add 函数执行上下文,词法环境中存放let count = 0outer指针指向全局词法环境add函数执行完成后,返回内部匿名函数,并赋值给全局变量baradd执行上下文被销毁,但count变量因被内部的匿名函数引用不会被销毁,而是留下一个空间存放count变量
  3. 全局第一次调用bar()(匿名函数),创建匿名函数执行上下文,自身无变量声明,outer指针指向add 函数词法环境;执行count++,找到add中的count并自增为1,返回1,执行完毕上下文出栈销毁。
  4. 全局第二次调用bar()(匿名函数),再次创建匿名函数执行上下文outer指针依旧指向add 函数词法环境;执行count++,找到add中的count,自增为2,返回2,执行完毕上下文出栈销毁。
  5. 全局第三次调用bar()(匿名函数),执行count++count值自增为3,返回3
  • 匿名函数执行count++时,会先在当前执行上下文中找count变量,由于没有找到,会顺着outer找,由于outer指向add函数词法环境,找到持久保存的count变量,因此依次输出结果为1、2、3。具体流程如下图(第2,3次创建匿名函数执行上下文省略)

无标题.png

三、闭包的应用:解决循环问题

使用 var 定义循环变量时,由于其没有块级作用域的特性,会遇到经典的问题:

var arr = []

for (var i = 1; i <= 5; i++) {
        arr.push(function() {
            console.log(i)
        })
}

for (var n = 0; n < arr.length; n++) {
    arr[n]() // 输出 6,6,6,6,6  
}
  • 这里通过在循环内部创建一个函数 fn,将当前的 i 作为参数 j 传入。由于形成了闭包,每个内部函数都保留了对各自 j 值的引用,从而解决了 var 循环变量的问题,正确输出1,2,3,4,5
for(var i = 1; i <= 5; i++) {
    function fn(j){
    arr.push(function(){
        console.log(j);
    })
}
fn(i) 
}

优点:

  • 定义私有模块,防止全局变量污染
  • 允许函数记住其创建时的环境

缺点:

  • 可能导致内存泄漏,因为被闭包引用的变量不会自动释放