浅析 Javascript 作用域

Posted by jiananshi on 2015-07-26

去年有看过作用域方面的东西,后来还专门看了 ECMAScript 了解内部实现。后来发现很多知识如果不是每天在用的技巧,可能很快就忘,然后在基础上掉坑,所以决定写下来。

首先,什么是作用域?这个问题已经够专门写一篇博客了,都可以扯词法分析和词法作用域引擎。这里先简单的理解为作用域对变量的查找:

1
2
3
4
5
6
7
8
  var a = 1;
  function c() {
    var m = 2;
    console.log(m);
  }
  c(); // 2

上面这堆代码就创建了两个作用域,一个是全局作用域,它能 ”看到“ 的变量有两个:ac。通过词法分析,他还能知道 a 是一个变量,c 是一个函数。另一个作用域是c 的函数体所创建的作用域。

当执行这部分 Javascript 代码时,抛开编译器的工作,我们从函数 c 开始执行时,来看一下作用域机制是如何运行的:

JS引擎会为函数的执行创建一个 “运行期上下文(execution context)”,同一个函数多次执行,就会创建多个 EC。EC 有一个重要的属性,叫做 “作用域链(scope)”,Scope 的数据结构类似一个栈,每次进入一个新的作用域的时候,将新作用域中的相关属性(下面解释具体是哪些)放入前端,作用域链查找时也从前端开始往后查找。

code image

作用域创建

现在,我们跳出函数 c 的执行,来看一下解析器(并不一定是,还要再研究下究竟是引擎还是解析器)编译器做了哪些工作来帮助我们找到对应的 a, m 和 c:

解析器通过创建 “变量对象(variable object)” 来保存对当前作用域变量的引用,在这段脚本执行前,一个 VO (全局的 VO 也称作全局对象(global object))已经被创建了:

1
2
3
4
  VO: {
    a: 1,
    c: <function reference>
  }

当 c 的 EC 被创建时,又一个 VO 被创建了:

1
2
3
  VO: {
    m: 2
  }

函数中的 VO 又被称作 “活动对象(activation object)”,它还会额外保存几个属性:

  1. callee 指向自身函数
  2. length 表示实参的长度
  3. arguments 的索引

至此,函数执行时的作用域查找的原理就清晰了:首先在当前 AO 中查找,然后在 Scope 上向后查找,到了最后的 GO 如果依然没有找到,那么一个 Reference Error 就会被抛出。作用域嵌套时,内层作用域可以获取外层作用域的 VO,而外层作用域获取不到内层作用域 VO,这点在理解了作用域创建机制后也不难理解,画个图来表示一下:

code image

作用域查找

图中的 bar 函数体内的作用域,它的 EC::Scope 属性内有三个元素:AO(当前函数体内 VO), VO(函数 foo 内的 VO), global Object。这个排列是有顺序的,作用域链查找时会从 AO 开始,最后才是 global Object。也就是说如果内层作用域和外层作用域有同名变量标示符,作用域查找会返回内层的作用域上匹配的标示符,这叫做 “遮蔽效应”

注:原型链和作用域链是两个不搭干的东西,原型链相对好理解一些,后面再专门写一下。