第四章 变量、作用域与内存
原始值与引用值
动态属性
原始值类型的初始化可以只使用原始字面量形式。如果使用的是 new 关键字,则 JavaScript 会创建一个 Object 类型的实例,但其行为类似原始值
1 | let name1 = 'haha' |
执行上下文与作用域
变量对象( variable object )
每一个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上
作用域链( scope chain )
活动对象( activation object )
作用域链增强
某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。通常在两种情况下会出现这个现象:
- try / catch 语句的 catch 块
- with 语句
这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明
1 | function buildUrl(){ |
这里 with 语句将 location 对象作为上下文,因此 location 会被添加到作用域链前端。当 with 中的代码引用变量 href 时,实际上引用的是 location.href,也就是自己变量对象的属性;而 qs 则是定义在 buildUrl() 中的那个变量,它定义在函数上下文的变量对象上。with 块中使用 let 声明的变量,被限制在块级作用域,buildUrl() 中没有定义,所以执行到 return 行会报错,如果换成 var 则可以正常执行
变量声明
var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前,这个现象叫做“提升”(hoisting)
if块、while块、function块,甚至连单独的块也是 let 声明变量的作用域
1 | // 这不是对象字面量,而是一个独立的块 |
使用 const 声明的变量必须同时初始化为某个值
如果想让整个对象都不能修改,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错,但会静默失败
垃圾回收
标记清理( mark-and-sweep )
垃圾回收程序运行的时候,会标记内存中存储的所有变量,然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了,最后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回他们的内存
到了2008年,IE、Firefox、Opera、Chrome 和 Safari 都在自己的 JavaScript 实现中采用标记清理(或其变体),只是在运行垃圾回收的频率上有所差异
引用计数( reference counting )
对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数位1,如果同一个值又被赋给另一个变量,那么引用数加1。类似的,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。当一个值的引用数为0时,就说明没办法再访问到这个值了,因此可以安全地回收其内存了。垃圾回收程序下次运行的时候就会释放引用数为0的值的内存
内存管理
解除引用:如果数据不再必要,那么把它设置为 null,从而释放其引用(这个建议最适合全局变量和全局对象的属性)
1、通过 const 和 let 声明提升性能
const 和 let 都以块为作用域(而非函数),所以相比于使用 var,使用这两个新关键字可能会更早的让垃圾回收程序介入,尽早回收应该回收的内存
2、隐藏类和删除操作
同一个构造函数创建的两个实例,共用一个隐藏类,如果其中一个实例进行动态属性赋值操作,会导致两个实例对应两个不同的隐藏类。所以,应该避免“先创建再补充”( ready-fire-aim )式的动态属性赋值,并在构造函数中一次性声明所有属性
1 | function Article(){ |
使用 delete 关键字会导致生成相同的隐藏类片段,最佳实践是把不想要的属性设置为 null