变量、作用域与内存

原始值与引用值

  • 保存原始值的变量是按值访问的;保存引用值的变量是按引用访问的
  • 动态属性:原始值不能有属性,尽管尝试给原始值添加属性不会报错,但是这个属性会被忽略(undefined);只有引用值可以动态添加后面可以使用的属性
  • 复制值:原始值复制时会创建一个新值,两个值互不影响;引用值复制时会复制一个指针,两个变量指向同一个对象
  • 传递参数:ECMAScript 中所有函数的参数都是按值传递的
  • 确定类型:
    • typeof 最适合用来判断一个变量是否为原始类型
    • instanceof 用来判断变量是否是给定引用类型的实例 result = variable instanceof constructor

执行上下文与作用域

执行上下文(execution context,EC):JavaScript 代码被解析和执行时所在环境的抽象概念

全局上下文是最外层的上下文,根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样,浏览器中是 window 对象,Node.js 中是 global 对象

上下文在其所有代码都执行完毕后会被销毁,包括定义在上下文中的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)

上下文中的代码在执行的时候,会创建变量对象的一个作用域链,这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序

代码正在执行的上下文变量对象始终位于作用域链最前端,全局上下文的变量对象始终位于作用域链的最末端

如果是函数上下文,其活动对象(activation object,AO)用作变量对象

函数参数被认为是当前上下文中的变量

作用域链增强

虽然执行上下文主要有全局上下文和函数上下文两种(eval() 调用内部存在第三种上下文),但有其他方式来增强作用域链

某些语句会导致在作用域链前端临时添加一个变量对象,该变量对象会在代码执行后被移除

  • try-catch 语句的 catch 块:创建一个新的变量对象,其中包含被抛出的错误对象的声明
  • with 语句:在作用域链前端添加指定的对象

变量声明

严格来讲, let 在 JavaScript 运行时中也会被提升,但由于暂时性死区(temporal dead zone,TDZ)的存在,直到执行到 let 语句时,变量才会被添加到执行上下文中

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

垃圾回收

JavaScript 是使用垃圾回收的语言,执行环境负责在代码执行时管理内存,这个过程是周期性的,垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行

JavaScript 通过自动内存管理实现内存分配和闲置资源回收

标记清理

JavaScript 最常用的垃圾回收策略是标记清理。当变量进入上下文,这个变量就会被加上存在于上下文中的标记,当变量离开上下文时,就会被加上离开上下文的标记

垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记就是待删除的了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并回收它们所占用的内存空间

引用计数

引用计数的思路是对每个值都记录它被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收程序下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存

引用计数的问题在于循环引用,两个对象相互引用,导致它们的引用次数都不为 0,所以垃圾回收程序不会回收它们占用的内存,比如:

function problem() {
	var objectA = new Object()
	var objectB = new Object()
	objectA.someOtherObject = objectB
	objectB.anotherObject = objectA
}

内存管理

如果数据不再必要,最好通过将其值设置为 null 来释放其引用,这个做法叫解除引用。这个建议最适合全局变量和全局对象的属性,局部变量在超出作用域后会被自动解除引用,比如

function createPerson(name) {
	var localPerson = new Object()
	localPerson.name = name
	return localPerson
}
var globalPerson = createPerson('Nicholas')
// 手动解除引用
globalPerson = null
  1. 通过 const 和 let 声明提升性能

    const 和 let 都以块(而非函数)为作用域,所以相比于使用 var 声明,使用这个两个关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存

  2. 隐藏类和删除操作

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

    • 避免 JavaScript 的“先创建再补充(ready-fire-aim)”式的动态属性赋值,并在构造函数中一次性声明所有属性
      function Article() {
      	this.title = 'Inauguration Ceremony Features Kazoo Band'
      }
      const a1 = new Article()
      const a2 = new Article()
      // 导致两个 Article 实例的对应两个不同的隐藏类
      a2.author = 'Jake'
      
      function Article(opt_author) {
      	this.title = 'Inauguration Ceremony Features Kazoo Band'
      	this.author = opt_author
      }
      const a1 = new Article()
      const a2 = new Article('Jake')
      
    • 使用 delete 关键字会导致生成新的隐藏类,最佳实践是把不想要的属性设置为 null
  3. 内存泄露

  • 意外的全局变量
  • 闭包
  1. 静态分配与对象池

    为了提升 JavaScript 性能,一个关键的问题就是如何减少浏览器执行垃圾回收的次数

    浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度,越快越频繁

    为了提升性能,V8 引入了对象池,它会对一些常见的对象结构进行缓存,当需要创建这些对象时,就会从对象池中取出,而不是重新创建

Last Updated: