深入浅出JS红宝书 - 变量, 作用域, 内存

163 阅读5分钟

变量, 作用域, 内存

原始值与引用值

原始值( primitive value ): 最简单的数据, Undefined, Null, Boolean, Number, String, Symbol. 保存原始值的变量是按值访问的, 也就是直接操作存储在变量中的实际值. ( by value )

引用值( reference value ): 由多个数据构成, 保存在内存中的对象. 但是JavaScript是不允许直接访问内存位置的. 在操作对象时, 实际上操作的是对改对象的引用而不是对象本身. ( by reference )

动态属性

  • 原始值 -- 不可以添加属性
  • 引用值 -- 可以添加属性

复制值

  • 原始值 -- 值复制到新的变量
  • 引用值 -- 指针复制到新的变量

(函数)传递参数

  • 按值传递

    • 按值传递参数时, 值会被复制到一个局部变量( 一个命名参数 - arguments的一个槽位中 )
    • 这是JavaScript的传参行为
  • 按引用传递

    • 按引用传递参数时, 值在内存中的位置会被保存在一个局部变量中, 也就是说在函数内对入参的修改会反映到函数外部
    • 这在JavaScript中是不存在的(不太理解原著中想表达的是什么东西在ECMAScript中不存在)
    function setName (obj) {
      obj.name = 'dyc';
      obj = new Object();
      obj.name = 'fxddf';
    }
    let person = new Object();
    setName(person);
    console.log(person);   //  {name: "dyc"}

确定类型

  • typeof
typeof "dyc"          // string
typeof true           // boolean
typeof 100            // number
typeof dyc            // undefined
typeof null           // object
typeof {name: 'dyc'}  // object
typeof [1, 2, 3]      // object
// typeof 不能区分不同的 object
  • instanceof
//  我们通常不关心一个值是不是对象,而是想知道它是什么类型的对象。为了解决这个问题,ECMAScript提供了instanceof操作符
result = variable instanceof constructor
​
console.log(person instanceof Object);  // 变量person是Object吗?
console.log(colors instanceof Array);   // 变量colors是Array吗?
console.log(pattern instanceof RegExp); // 变量pattern是RegExp吗?

执行上下文与作用域

执行上下文在JavaScript中很重要. 变量或函数的上下文决定了他们可以访问哪些数据, 以及他们的行为. 每个上下文都有一个关联的变量对象( variable Object ), 而这个上下文中定义的所有变量和函数都存在于这个对象上. 无法通过代码访问到变量对象, 但是后台处理数据会用到它.

执行上下文

  • 全局上下文 -- 最外层上下文

    • 浏览器中, 全局上下文 -- window对象

      • var 定义的全局变量和函数都是 window 对象的属性和方法
      • letconst 的顶级声明不会定义在全局上下文中, 但在作用域链上解析效果是一样的.
  • 函数上下文

  • eval调用内部存在第三种上下文

作用域

  • 作用域链增强

    • try/catchcatch

    • with

      [with]  由于 with 语句影响性能且难以调试其中的代码, 通常不推荐在产品代码中使用

      [with]  with 不能再严格模式下使用

变量声明

  • var

    • 变量自动被添加到最接近的上下文
    • 如果变量未经声明就被初始化了, 会被自动添加到全局上下文
    • 声明会被拿到上下文的顶部, 位于所有代码之前
  • let

    • 作用域是块级
    • 同一作用域内不能声明两次
    • let 在 js 运行时也会被提升, 但由于"暂时性死区", 不能再声明之前使用let变量
  • const

    • let

    • 一经声明, 再其声明周期的任何时候都不能再重新赋值

    • 对象的键不受限制

    • 多使用const

      [const]  由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的V8引擎就执行这种优化。

  • 标识符查找

    • 会一级一级作用域往上找, 使用找到的第一个

垃圾回收

  • 标记清理 -- 最常用

  • 引用计数 -- 没那么常用

  • 性能

    • 无论什么时候开始收集垃圾,都能让它尽快结束工作
  • 内存管理

    • 解除引用

      • 如果数据不再必要, 设置 null 释放其引用

        [注意]  解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关的值已经不在上下文里了,因此它在下次垃圾回收时会被回收。

    • 通过const 和 let 声明提升性能

      • 因为constlet都以块(而非函数)为作用域,所以相比于使用var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。
    • 隐藏类和删除操作

      • V8在将解释后的JavaScript代码编译为实际的机器码时会利用“隐藏类”

        • 动态删除属性与动态添加属性导致的后果一样。最佳实践是把不想要的属性设置为null。这样可以保持隐藏类不变和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果。
    • 内存泄露

      • 未使用声明符号

      • 一直运行的定时器

      • 闭包

        let outer = function() {
          let name = 'Jake';
          return function() {
            return name;
          };
        };
        ​
        /**
         * outer 执行完毕,按道理应该会销毁 name,
         * 但是 newFunction,即 outer 的内函数会一直占用 name,导致 name 不会被销毁。
         * 注意:跟内部是不是匿名函数无关
         * 注意:跟 return 的这个函数是否引用父函数的变量有关。
         */
        let newFunction = outer();
        ​
        newFunction = null // 清理闭包的内存泄漏
        
    • 静态分配与对象池

      • 减少浏览器执行垃圾回收的次数

        [静态分配]  静态分配是优化的一种极端形式。如果你的应用程序被垃圾回收严重地拖了后腿,可以利用它提升性能。但这种情况并不多见。大多数情况下,这都属于过早优化,因此不用考虑。