原型链将一个个原型对象串起来,从而实现对象属性的查找。作用域链就是将一个个作用域串起来,实现变量查找的路径。讨论作用域链,实际就是在讨论按照什么路径查找变量的问题。

作用域就是存放变量和函数的地方。

函数作用域与全局作用域

每个函数在执行时都需要查找自己的作用域,我们称为函数作用域,在执行阶段,在执行一个函数时,当该函数需要使用某个变量或者调用了某个函数时,便会优先在该函数作用域中查找相关内容。

  1. var x = 4
  2. function test_scope() {
  3. var name = 'foo'
  4. console.log(name)
  5. console.log(type)
  6. console.log(test)
  7. var type = 'function'
  8. var test = 1
  9. console.log(x)
  10. }
  11. test_scope()

作用域链 - 图1

另外系统还为我们添加了另外一个隐藏变量 this,V8 还会默认将隐藏变量 this 存放到作用域中。

那么另一个问题来了,我在 test_scope 函数使用了变量 x,但是在 test_scope 函数的作用域中,并没有定义变量 x,那么 V8 应该如何获取变量 x?如果在当前函数作用域中没有查找到变量,那么 V8 会去全局作用域中去查找,这个查找的线路就称为作用域链。

全局作用域和函数作用域类似,也是存放变量和函数的地方,但是它们还是有点不一样:全局作用域是在 V8 启动过程中就创建了,且一直保存在内存中不会被销毁的,直至 V8 退出。 而函数作用域是在执行该函数时创建的,当函数执行结束之后,函数作用域就随之被销毁掉了。

V8 启动之后就进入正常的消息循环状态,这时候就可以执行代码了,比如执行到上面那段脚本时,V8 会先解析顶层 (Top Level) 代码,我们可以看到,在顶层代码中定义了变量 x,这时候 V8 就会将变量 x 添加到全局作用域中。

作用域链是怎么工作的?

  1. var name = '极客时间'
  2. var type = 'globle'
  3. function foo(){
  4. var name = 'foo'
  5. console.log(name)
  6. console.log(type)
  7. }
  8. function bar(){
  9. var name = 'bar'
  10. var type = 'function'
  11. foo()
  12. }
  13. bar()

现在,我们结合 V8 执行这段代码的流程来具体分析下。首先当 V8 启动时,会创建全局作用域,全局作用域中包括了 this、window 等变量,还有一些全局的 Web API 接口,创建的作用域如下图所示:

作用域链 - 图2

V8 启动之后,消息循环系统便开始工作了,这时候,我输入了这段代码,让其执行。V8 会先编译顶层代码,在编译过程中会将顶层定义的变量和声明的函数都添加到全局作用域中,最终的全局作用域如下图所示:

作用域链 - 图3

全局作用域创建完成之后,V8 便进入了执行状态。前面我们介绍了变量提升,因为变量提升的原因,你可以把上面这段代码分解为如下两个部分:

  1. //======解析阶段--实现变量提升=======
  2. var name = undefined
  3. var type = undefined
  4. function foo(){
  5. var name = 'foo'
  6. console.log(name)
  7. console.log(type)
  8. }
  9. function bar(){
  10. var name = 'bar'
  11. var type = 'function'
  12. foo()
  13. }
  14. //====执行阶段========
  15. name = '极客时间'
  16. type = 'globle'
  17. bar()

第一部分是在编译过程中完成的,此时全局作用中两个变量的值依然是 undefined,然后进入执行阶段;第二部代码就是执行时的顺序,首先全局作用域中的两个变量赋值“极客时间”和“globle”,然后就开始执行函数 bar 的调用了。

当 V8 执行 bar 函数的时候,同样需要经历两个阶段:编译和执行。在编译阶段,V8 会为 bar 函数创建函数作用域,最终效果如下所示:

作用域链 - 图4

然后进入了 bar 函数的执行阶段。在 bar 函数中,只是简单地调用 foo 函数,因此 V8 又开始执行 foo 函数了。同样,在编译 foo 函数的过程中,会创建 foo 函数的作用域,最终创建效果如下图所示:

作用域链 - 图5

好了,这时候我们就有了三个作用域了,分别是全局作用域、bar 的函数作用域、foo 的函数作用域。现在我们就可以将刚才提到的问题转换为作用域链的问题了:foo 函数查找变量的路径到底是什么?

因为 JavaScript 是基于词法作用域的,词法作用域就是指,查找作用域的顺序是按照函数定义时的位置来决定的。bar 和 foo 函数的外部代码都是全局代码,所以无论你是在 bar 函数中查找变量,还是在 foo 函数中查找变量,其查找顺序都是按照当前函数作用域–> 全局作用域这个路径来的。

作用域链 - 图6