本文分两个部分讨论变量存储模式

  • 局部/ 全局/ 闭包变量的存储机制: 在这部分我们讨论什么样的变量有资格存储在栈中
  • 不同类型变量的存储机制: 在这部分我们讨论存储在栈中的元素究竟存的是字面量还是引用

先说结论: 万物都存在堆中, 有的变量会在栈上存储引用地址

堆与栈

  • 堆是一个很大的内存存储空间, 你可以在里面存储任何类型数据. 操作系统不会自动回收. 在栈中存储不了的数据比如对象就会被存储在堆中, 在栈中呢是保留了对象在堆中的地址, 也就是对象的引用.
  • 栈是内存中一块用于存储局部变量和函数参数的线性结构, 遵循着先进后出的原则. 数据只能顺序的入栈, 顺序的出栈. 内存中栈区的数据, 在函数调用结束后, 就会自动的出栈, 不需要程序进行操作, 操作系统会自动回收

于是出现了一个问题, 在闭包出现时, 函数是如何访问到闭包所在的已经销毁的栈中的变量的呢?

局部/全局/闭包变量的存储机制

  • 局部变量: 最简单的, 局部变量存储在作用域所在的栈空间中, 例如

    function demo() {let a = 1;let b = '213';let c = [213];let d = new Object();
    }console.dir(demo);// ƒ demo()
    //   arguments: null
    //   caller: null
    //   length: 0
    //   name: "demo"
    //   prototype:
    //     constructor: ƒ demo()
    //     [[Prototype]]: Object
    //   [[FunctionLocation]]: demo.html:53
    //   [[Prototype]]: ƒ ()
    //   [[Scopes]]: Scopes[1]
    //     0: Global {0: Window, window: Window, self: Window, document: document,
    

    在上面我们找不到定义的变量, 在DevTools的内存-堆分析中也找不到他们

  • 全局变量

    • 使用var声明的全局变量
      使用var声明全局变量其实仅仅是为global对象添加了一条属性, 全局变量会被默认添加到函数作用域链的最底端, 也就是[[Scopes]]中的最后一个

      var aaa = 1;          // 随便var一个变量
      // 等同于 window.aaa = 1;
      console.dir(()=>{})   // 随便打印一个函数看看他的作用域// anonymous()
      //   length: 0
      //   name: ""
      //   arguments: (…)
      //   caller: (…)
      //   [[FunctionLocation]]: VM167:1
      //   [[Prototype]]: ƒ ()
      //   [[Scopes]]: Scopes[1]    <- 看到函数的作用域
      //     0: Global              <- 只有global(window)作用域
      //       aaa: 1               <- 看到window上的aaa
      //       alert: ƒ alert()
      //       atob: ƒ atob()
      //       blur: ƒ blur()
      //       btoa: ƒ btoa()
      
    • 使用let/const声明全局变量不会修改window对象, 而是将变量的声明放在了一个特殊的对象Script

      let t1 = 1;
      const t2 = 2;
      console.dir(()=>{})// anonymous()
      //   length: 0
      //   name: ""
      //   arguments: (…)
      //   caller: (…)
      //   [[FunctionLocation]]: VM99:1
      //   [[Prototype]]: ƒ ()
      //   [[Scopes]]: Scopes[2]      <- 查看作用域
      //     0: Script {t1: 1, t2: 2}   <- 看到这些数据被存储到了Script对象中
      //     1: Global {window: Window, self: Window, document: document,...}
      
  • 闭包中的变量: 闭包中的变量会在子函数调用的时候存储为一个对象(存储在堆中), 并在[[Scopes]]Closure(闭包)中体现

    function testCatch1 () {let a1 = 1;var a2 = 'a';const a3 = true;let a4 = {a: 1};return function () {console.log(a1, a2, a3, a4)}
    }function testCatch2 () {let a1 = 1;
    var a2 = 'a';
    const a3 = true;
    let a4 = {a: 1};
    return function () {console.log(a1, a2, a3, a4)
    }
    }console.dir(testCatch1())// ƒ anonymous()
    //   arguments: null
    //   caller: null
    //   length: 0
    //   name: ""
    //   prototype: {constructor: ƒ}
    //   [[FunctionLocation]]: VM469:6
    //   [[Prototype]]: ƒ ()
    //   [[Scopes]]: Scopes[2]
    //     0: Closure (testCatch1) {a1: 1, a2: 'a', a3: true, a4: {…}}  <- 可以看到是按照对象存储在堆中的
    //     1: Global {window: Window, self: Window, document: document, name: '',...} <- global在作用域最后console.dir(testCatch2())// ƒ anonymous()
    //   arguments: null
    //   caller: null
    //   length: 0
    //   name: ""
    //   prototype: {constructor: ƒ}
    //   [[FunctionLocation]]: VM469:16
    //   [[Prototype]]: ƒ ()
    //   [[Scopes]]: Scopes[2]
    //     0: Closure (testCatch2) {a1: 1, a2: 'a', a3: true, a4: {…}} <- 可以看到是按照对象存储在堆中的, 但是闭包名不同
    //     1: Global {window: Window, self: Window, document: document, name: '',}console.dir(testCatch1().a4 === testCatch1().a4)// true <- 连引用对象都是相同的
    

小结: 除了局部变量, 其他变量都在堆中

那么, 栈中变量是如何存储的呢? 是如开头所说基本数据类型存字面值, 对象存引用地址吗?

不同类型变量的存储机制

  • 对于对象类型的数据, 毫无疑问: 栈中存储的是对象在堆中的地址
  • 对于基础数据类型呢? 他存储的是字面量吗? 首先我们清楚基础类型包括
    • Number & String & Boolean
    • Null & Undefined
    • Symbol & BigInt

String类型