探秘Javascript闭包:预编译、作用域链和垃圾回收

150 阅读4分钟

要想深入了解闭包,必须要有对下面几个方面的了解:预编译作用域链自动垃圾收集机制

预编译

在 Javascript 解释器进入到一个执行上下文时,会有三个步骤:

  1. 语法分析
  2. 预编译
  3. 执行

我们这里主要关注在代码执行前的预编译阶段发生了什么:

  1. 开辟内存,创建变量对象 VO(在全局执行上下文中,VO是全局对象自身;在函数执行上下文中,VO 是不能直接访问的,此时由活动对象 AO 扮演 VO 的角色);
  2. 变量声明、函数声明、实参值赋予形参(仅在函数执行上下文中),将声明的数据保存在 VO / AO 中。

用伪代码表示这一过程:

// 全局执行上下文
var foo = 1
function bar() {}
var baz = function() {}
qux = 2 // 非声明

// 预编译
VO(window): {
  Math: <...>,
  String: <...>
  ...
  ...
  foo: undefined,
  baz: undefined,
  bar: function() {}
}

// 函数执行上下文
add(1, 2)
function add (x, y) {
  return x + y
}

// 预编译
AO(add): {
  arguments: [1, 2],
  x: 1,
  y: 2
}

作用域链

[[scope]] 是函数创建时所有父变量对象的层级链,它是函数的属性,储存在函数对象中,在函数创建时即已确定。如何理解这句话呢?观察下面实例:

var x = 10
function fn() {
    console.log(x)
}

function show(f) {
    var x = 20
    f()
}
show(fn) //10

fn 函数被调用时,其作用域链是 AO(fn) + VO(window),并不包括 AO(show)。当函数被调用时,解释器创建并进入一个新的执行上下文,这个执行上下文类似于这样:

activeExecutionContext = {
  AO: vars + function declarations + arguments,
  this: context object,
  scope: AO + [[scope]] // 原型链,执行上下文的属性
}

作用域链本质上是一个指向变量对象的指针列表,标识符解析沿着作用域链一级一级进行搜索,保证了对执行上下文中有权访问的所有变量和函数的有序访问。

自动垃圾收集机制

现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。

标记清除

这是 JavaScript 中最常见的垃圾回收方式。为什么说这是种最常见的方法,因为从2012年起,所有现代浏览器都使用了标记清除的垃圾回收方法,除了低版本 IE,它们采用的是引用计数法。

那什么叫标记清除呢? JavaScript 中有个全局对象,浏览器中是 window。定期的,垃圾回收期将从这个全局对象开始,找所有从这个全局对象开始引用的对象,再找这些对象引用的对象,对这些活着的对象进行标记,这是标记阶段。清除阶段就是清除那些没有被标记的对象。

标记清除法的一个问题就是不那么有效率,因为在标记-清除阶段,整个程序将会等待,所以如果程序出现卡顿的情况,那有可能是收集垃圾的过程。

2012年起,所有现代浏览器都使用了这个方法,所有的改进也都是基于这个方法,比如标记整理法。

标记清除有一个问题,就是在清除之后,内存空间是不连续的,即出现了内存碎片。如果后面需要一个比较大的连续的内存空间时,那将不能满足要求。而标记整理法可以有效地解决这个问题。标记阶段没有什么不同,只是标记结束后,标记整理法会将活着的对象向内存的一边移动,最后清理掉边界的内存。不过可以想象,这种做法的效率没有标记清除法高。

引用计数

在内存管理环境中,对象 A 如果有访问对象 B 的权限,叫做对象 A 引用对象 B。引用计数的策略是将“对象是否不再需要”简化成“对象有没有其他对象引用到它”,如果没有对象引用这个对象,那么这个对象将会被回收。

闭包

var foo = function() {
  var x = 1
  var bar = function() {
    console.log(x)
  }
  return bar
}

var bar = foo()

函数 foo 运行结束后,执行上下文被弹出环境栈,随即上下文环境连同其属性(AO(foo), this, scope)被销毁,内存被释放,控制权交还之前的执行上下文。虽然作用域链被销毁了,但在闭包中,函数 bar 被返回,其属性 [[scope]] 也仍然存在,对变量 x 依然存在引用,内存无法被释放。

通过销毁函数,断开对变量 x 的引用,即可释放内存。

bar = null