JS-图解作用域链,一看就懂系列💡之看不懂来捶我

1,270 阅读8分钟

作用域链是个什么东东,看完下面的图就知道了

在这之前,我们先了解一些基础概念:

  1. 作用域:作用域是指程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,及作用域控制着变量和函数的可见性和生命周期。
  2. 作用域的本质,就是当前执行上下文的一个变量对象。该作用域中的变量,是以属性的形式存放在这个变量对象中

《高程》中,这样写道:在全局作用域中,这个对象叫变量对象,在函数作用域中,这个对象叫做活动对象。

下面为了方便理解,统称为变量对象

  1. 作用域分为全局作用域,函数作用域,块作用域,eval作用域、with作用域,这几种,我们这篇先讨论前两种

关于执行上下文,不仅仅只有这个存放变量的对象,还有this等东西

  1. 作用域形成时机

    • 当对文件顶部代码完成编译后,会生成全局执行上下文,同时就形成了全局作用域;
    • 当执行一个函数时,会先对函数的代码进行编译。编译完成之后,就会生成函数的执行上下文,同时就形成了函数的作用域
    • 对于块作用域,会在代码执行到该代码块的时候,生成。

图解作用域链

我们来分析这样一个代码,这里我们省略了变量和函数声明的描述

1

首先,这个代码是在写全局作用域中的,所以在这个代码编译的时候,会创建全局上下文,同时全局的变量对象也就产生了,这个对象中有thiswindowdocument等变量,也会有在全局中声明的变量outera

2

在全局代码编译过程中,声明了outer函数。这时,outer函数就会有一个[[Scope]]的属性,这个属性指向了一个作用域链(下面简称函数体中指向的作用域链),这个作用域链只有一个对像,这个对象就是全局上下文中的变量对象。这个变量对象中会有thiswindowdocumenta等变量

上图中的a还是undefined,因为代码只是编译完成,没有得到执行,所以变量a并没有被绑定初始值

我们把变量的初始化叫做绑定初始值,和赋值有些区别

3

当代码执行到第10行的时候,调用outer函数。这个时候,编译器会编译outer中的代码。编译的时候,会生成outer的执行上下文。与此同时,会复制outer函数中的[[Scope]]来创建对应的作用域链(下面简称执行上下文的作用域链)。接着,会创建outer函数的变量对象,将其推入作用域链的前端。

该变量对象中,会预先放入arguments的变量。如果该函数有形参,也会放进其中。接着会放入var或者function声明的变量,即ainner

注意:函数和函数的执行上下文是两个不同的对象,所以,其中的作用域链也不是同一个

上图中,可以看到this是函数编译完成之后,被加到该函数的执行上下文中的。函数体的属性是没有this的

如果当outer中代码执行的时候,想要去访问打印a的值,那么JS引擎会在当前作用域中查找a,当前作用域就是作用域链第0的位置所指的变量对象。如果没有查找到a,就会顺着作用域链往下找,也就是往索引为1的位置所指的对象里面找。如果在全局作用域里面还没有找到a,就会报错ReferenceError: a is not defined

可惜,当前作用域中就已经有a了,你看不到这么有趣的过程了🙈

变量对象中是否存在arguments?这个ECMAScript中是有的,但是V8似乎会优化它。

当代码中没有用到arguments的时候,V8生成的变量对象中就不会有arguments(这个只是暂时的推测,具体问题还得看代码的反编译)

4

当JS引擎编译outer函数的代码,会声明inner函数。inner函数对应的[[Scope]]属性也被赋予了值--作用域链。这个作用域链有两个对象,第0的位置是outer函数的变量对象,第1的位置是全局上下文的变量对象

这里,在第0的位置就是outer函数的闭包。对的,闭包在编译期间就产生了,和执行期的行为无关。下面有见到闭包的更详细的介绍

5

当代码执行到第6行的时候,inner函数被调用。编译器会编译inner函数中的代码。编译的时候会生成inner函数的执行上下文,与此同时,会复制inner函数的函数作用域链,来创建对应的执行上下文作用域链。接着,会创建inner函数的变量对象,并将其推入作用域链的前端。这个变量对象,会被放入argumentsa变量

在整个JS代码运行的过程中,堆中只会有一个outer函数的执行上下文,也只会有一个全局的执行上下文。

也就是说outer作用域链上的outer变量对象,和inner函数作用域链上的,是同一个。

引用不同,但是指向的对象是同一个。这个很好理解,没有必要在堆中创建两个一模一样的对象。

要记住,在inner中,是可以操作outer的变量对象中的变量,改变值之后,outer作用域中是会同步改变的。翻来覆去地说,就是想让你记住上面的结论。

如果,在inner代码执行的时候,想要去打印b的值。那么JS引擎会首先在当前作用域中查找b,如果没有查找到,就会去下一个作用域中查找--也就是索引为1的位置所指的变量对象。如果还没有查找到,JS引擎会一直往下找,直到全局作用域为止。如果在全局作用域中还没找到,就会报错。

当然,如果在顺着作用域链查找的过程,中找到了b,就会立即停止查找的动作,并将这个变量的值返回

可以,知道了代码的我们,会知道,如果真的要打印b,JS引擎一定会报错的😉

在作用域查找变量的过程中,查找的顺序像一个链条,所以我们把这个穿起来的作用域叫做作用域链

概念讲述完毕。

6

当代吗执行到第7行的时候,inner函数执行完毕。这个时候,inner的执行上下文会被销毁,对应的变量对象也被销毁了。这个过程和inner函数的函数作用域链没半毛钱关系

函数的执行上下文被销毁了,对应的变量对象也被销毁了。

但是不会影响作用域链上的其他两个对象--outer变量对象、全局变量对象,它们的销毁有自己的条件。

即使inner函数的执行上下文被销毁了,inner函数体中作用域链函数完全不受影响。

别感到伤心😄,当inner函数被再次调用的时候,会再次创建inner的执行上下文的

7

当代码执行到11行的时候,outer函数执行完毕。outer的执行上下文会被销毁,对应的变量对象也被销毁了

全局上下文还没有被销毁,所以outer函数还是存在的

等等。这个时候,可能就有人问了,inner函数中的函数作用域链中,是不是也没有了对outer变量对象的引用了。

其实,随着outer的执行上下文被销毁的同时,outer作用域也被销毁了。也就是说,其中的inner函数也被销毁了。上面提出来的问题也就不存在了

8

关闭该浏览器窗口。全局上下文被销毁。

下面我们来做一个经典的面试题

为什么在函数内部可以访问函数外部的变量,而在函数外面不能访问函数内部?

  • 第一种解释:函数作用域链的访问顺序,是从第0的位置,往更大索引的位置访问的,所以作用域链的查找顺序只能由内而外,不能由外而内

  • 第二种解释:访问内部函数的变量有两个位置。一个是内部函数执行之前,一个是内部函数执行之后。那么在内部函数执行之前,内部函数的执行上下文都没有生成,谈何访问内部的变量?在内部函数执行完成之后,内部函数的执行上下文都被销毁了,又谈何访问内部的变量?

怎么样,有没有一种开脑洞的赶脚呢?

下一篇文章的内容,是关于闭包的(JS-图解闭包),也很精彩啊。快去看看。

总结

  1. 简述了什么是作用域
  2. 图解了作用域链
  3. 下一章节,我们来通过这个作用域链,来讲解闭包是如何产生的。当然也是超简单😎
  4. 如果我哪里讲的不明白,一定要告诉我,谢谢!