内存角度理解闭包

195 阅读2分钟

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

内存角度理解闭包

作用域内的原始类型数据会被存储到栈空间,引用类型则会被存储到堆空间。

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = { 
        setName:function(newName){
            myName = newName
        },
        getName:function(){
            console.log(test1)
            return myName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

首先盘一下执行流程

  1. 首先先创建全局执行上下文并压入调用栈中

    • 全局执行上下文

      • 变量环境:bar = undefined foo()...
      • 词法环境:空
  2. 执行foo,将返回的innerBar给bar

    1. 执行前首先会编译foo函数。创建一个空的执行上下文。

    2. JavaScript引擎对这内部函数做一次词法扫描,首先是setName函数,发现函数内部引用了foo函数的myName,由于是内部函数引用了外部函数的变量,因此JavaScript引擎判断这是一个闭包,因此就会在堆空间中创建一个“closure(foo)”指向的对象(这是一个内部对象,JavaScript 是无法访问的)

    3. 接着到另一个内部函数getName,JS引擎接着词法扫描,发现这个内部函数还引用了外部的foo函数的test1,于是JS引擎又将test1添加到了“closure(foo)”对象中,这时候堆中的“closure(foo)”对象包含了myName和test1两个变量。

    4. 由于 test2 并没有被内部函数引用,所以 test2 依然保存在调用栈中foo执行上下文的词法环境中。

      此时的foo执行上下文

      image-20210811234914948

    5. 当return返回对象innerBar之后,foo的执行上下文弹出调用栈,但是由于返回的对象innerBar存在指向其父级作用域的引用,产生了闭包closure(foo),还依然存在在堆空间中。

  1. 所以当执行到bar.setName("极客邦");bar.getName();依然可以访问到closure(foo)这个闭包对象里的属性。

总结

产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中。