JavaScript 红宝书笔记 - 第 4 章.变量、作用域与内存

原始值与引用值

  • 原始值有 6 种 UndefinedNullBooleanNumberStringSymbol
  • 保存原始值的变量是按值访问的,对于对象则是按引用访问的。
  • 原始值不能有属性。
  • 原始值大小固定,保存在栈内存上;引用值是对象,保存在堆内存上。

复制值

  • 原始值的复制是副本,互不干扰。
  • 引用值的复制是引用,实际上是指针,指向存储在堆内存中的对象。

传递参数

  • 所有函数的参数都是按值传递的,对本地变量的修改不会反映到函数外部。
  • 但是对象的话是相当于通过值传了指针,所以内部也可以通过引用访问对象,对象中的属性也会相对改变。

确定类型

  • typeof 确定原始值,instanceof 确定什么类型的对象(由原型链决定)。
    • *任何实现内部 [[Call]] 方法的对象都会在 typeof 的时候返回 function。因此在检测正则表达式的时候也会返回 function

执行上下文(Context)与作用域

可以把 Js 作用域与作用域链详解 作为参考。

  • 浏览器中的全局上下文(最外层的上下文)就是 window 对象。因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。
    • 上下文在所有代码执行完毕的时候销毁。
  • 每个函数调用都有自己的上下文,代码执行流进入函数时会被推入一个上下文栈中。执行完毕之后弹出。
    • 上下文中的代码执行时会创建变量对象的作用域链,它决定了各级上下文中的代码在访问变量和函数时的顺序。
    • 正在执行的上下文中的变量对象始终位于作用域链的最前端。
    • 如果上下文是函数,则其活动对象作为变量对象。
      • 活动对象最初只有一个定义变量 arguments (全局上下文中没这个)
    • 作用域链中下一个变量对象来自于包含上下文,以此类推到全局上下文。
    • 全局上下文的变量对象时钟是作用域链的最后一个变量对象。
    • 函数参数被认为是当前上下文中的变量。
  • 代码执行时标识符解析是沿作用域链逐级搜索标识符名称完成的。
  • eval() 调用内部存在第三种上下文。
1
2
3
4
5
6
7
var scope="global";
function t(){
console.log(scope);
var scope="local"
console.log(scope);
}
t();

中是因为在函数 tvar 声明的 scope 变量提升了,所以第一个打印出来的是 undefined

  • 关于 this

    官方的称呼为 This Binding,在全局执行上下文中,this 总是指向全局对象,例如浏览器环境下 this 指向 window 对象。

    而在函数执行上下文中,this 的值取决于函数的调用方式,如果被一个对象调用,那么 this 指向这个对象。否则 this 一般指向全局对象 window 或者 undefined(严格模式)。

作用域链增强

某些语句会导致作用域前端临时添加一个上下文。通常有以下两种情况。

  • with 语句
  • try/catch 语句的 catch

对于 with 语句会添加指定的对象,catch 语句则会创建一个新的变量对象,包含要抛出的错误对象的声明。

变量声明

  • 使用 var 时变量会被添加到最接近的上下文。

  • 如果变量未经声明就被初始化的话,会被自动添加到全局上下文。(是一个常见错误)

  • var 会发生变量提升。

  • let 拥有块级作用域,统一作用域内不能声明两次。

  • let 适合在循环中声明迭代变量,而 var 声明的话会泄漏到循环外部。

  • const 的情况下,如果使整个对象不能被修改的话,可以使用 Object.freeze()

    1
    2
    3
    const o3 = Object.freeze({});
    o3.name = "Colanns";
    console.log(o3.name); // undefined
  • 作用域链中的对象也有一个原型链,对标识符打的搜索可能涉及到每个对象的原型链。

垃圾回收

  • JS 采用自动内存管理。
  • 两种主要的标记策略
    • 标记清理
    • 引用计数

标记清理

  • 变量进入上下文时会被加上存在标记。
  • 变量离开上下文时会被加上离开标记。
  • GC 程序运行的时候,会标记内存中的所有变量,然后将所有在上下文中的变量和被上下文中变量引用的变量的标记去掉,之后再被加上标记的变量即为待删除的。(因为任何在上下文中的变量都访问不到他们了)

引用计数

  • 没那么常用,且有问题:循环引用。

  • 声明变量并赋予引用值时,引用数为 1。

  • 同一个值被赋给了其他变量则引用数加 1。

  • 保存对该值引用的变量被其他值覆盖则引用数减 1。

  • 引用数为 0 的时候 GC 程序将在下次运行时回收内存。

  • 循环引用:

    1
    2
    3
    4
    5
    6
    function problem() {
    let objectA = new Object();
    let objectB = new Object();
    objectA.someOtherObject = objectB;
    objectB.anotherObject = objectA;
    }

    二者的引用数将会是 2,但是函数结束后二者都不在作用域中,但是不会被释放。

  • 公开处刑 IE(P96)

性能

  • 二度公开处刑 IE(P97)

内存管理

  • 如果数据不再必要,则设为 null,解除引用,尤其对于全局变量和全局对象的属性。

  • 多多使用 constlet

  • 隐藏类:运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好。

    • 因此尽可能避免 JS 的先创建后补充式动态属性赋值,在构造函数中一次性声明所有属性。
    • delete 关键字也会造成动态添加属性一样的后果。

内存泄漏

  • 意外声明全局变量
1
2
3
function setName() {
name = 'Jake';
}
  • 定时器通过闭包引用外部变量

  • JS 闭包

1
2
3
4
5
6
7
let outer = function() {
let name = 'Jake';
// 只要返回函数存在就不能清理 name
return function() {
return name;
};
};

静态分配与对象池

极端的优化形式,一般不多见也不必考虑。

  • 为了减少 GC 次数。
  • 减少对象更替速度,比如某些情况下不要动态创建矢量对象,只修改一个对象。
  • 使用对象池:对象不存在的时候创建新的,存在的时候复用。
  • 由于 JS 数组动态可变,初始化的时候尽量创建一个大小够用的数组,防止删除后再创建而引来 GC。