看了《浏览器工作原理与实践》然后再次回顾了下《你不知道的javascript(上)》的第一部分内容。对整体作用域和闭包有了更深刻的理解。

1. 代码的编译

确定一点JavaScript是先编译再运行的。(大部分情况是JavaScript编译发生在代码执行前的几微米甚至更短)

程序中的一段源代码在执行之前会经历三个步骤,统称为’编译’

  • 分词/词法分析:将字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(Token)

eg: var a = 2; 分解为 -> var 、a、 = 、2、;。

  • 解析/语法分析:将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树,这个树被称为“抽象语法树”(Absolute Syntax Tree,AST)

  • 代码生成:将AST转换为可执行代码的过程称为代码生成。

2. 如何处理var a = 1;这个代码

下面我们看一下一段代码的执行:

  1. var a = 1;
  1. 首先编译器将这段代码分解成词法单元;(产生词法作用域)

  2. 然后将词法单元解析成一个树结构(AST);

  3. 当编译器开始进行代码生成的时候会进行如下操作:

(1)遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中,

  • 如果是,编译器会忽略该声明,继续进行编译;
  • 否则,它会要求作用域在当前作用域集合中声明一个新的变量,并命名为a;

(2)接下来编译器会为引擎生成运行时的代码,这些代码用于处理 a = 2这个赋值操作。

  • 引擎运行时首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。
  • 如果是,引擎就会使用这个变量;
  • 否则,引擎会继续(根据作用域链)查找该变量,最终找到了就使用这个变量。(LHS查询)

上面就是一个代码从编译到执行的过程的具体描述。

2.1 引擎查找方式

当然其中引擎执行编译器给的代码(赋值操作)时,会通过查找这个变量来判断这个变量是否已经声明,这个过程需要作用域的协助,而查找的方式分为两种:

  • LHS(“赋值操作的目标是谁”)
  • RHS(“谁是赋值操作的源头”)

区分RHS和LHS也很重要,尤其分析异常时

  • RHS未找到:引擎会抛出错误RefrenceError
  • LHS未找到:引擎(或引擎中的编译器)会帮你在顶层作用域声明一个具有该名称的变量。(严格模式除外)。

2.2 执行上下文和可执行代码

一段代码经过编译后,会生成两个部分内容:执行上下文(Execution context)和可执行代码。

执行上下文:是JavaScript执行一段代码时的运行环境。(比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等)

这里做一下区分: 执行上下文:函数每次调用一次,都会产生一个新的执行上下文环境。 作用域:除了全局作用域,函数会创建自己的作用域。作用域在函数定义时就已经确定了,不是在函数调用确定(区别于执行上下文环境,当然this也是上下文环境里的成分)

2.3 调用栈(执行上下文栈)

JavaScript引擎利用栈这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript引擎会将执行上下文压入栈中,通常把这种用来管理上下文的栈称为调用栈。

通过一段代码来详细看下这个流程

  1. var a = 2;
  2. function add(b,c) {
  3. return b+c
  4. }
  5. function addAll(b,c) {
  6. var d = 10;
  7. result = add(b,c);
  8. return a+result+d
  9. }
  10. addAll(3,6)
  1. 创建全局上下文,并将其压入栈底。

浏览器中的javaScript执行机制 - 图1
变量a、函数add和addAll都保存到了全局上下文的环境对象中。

全局执行上下文压入到调用栈后,JavaScript引擎便开始执行全局代码了。首先会执行a=2的赋值操作,(通过LHS查找到a,然后将2赋值给a)执行该语句会将全局上下文变量环境中a的值设置为2,设置后的全局上下文的状态如下图:
浏览器中的javaScript执行机制 - 图2

  1. 调用addAll函数

当调用该函数时,JavaScript引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中
浏览器中的javaScript执行机制 - 图3
addAll函数的执行上下文创建好之后,便进入函数代码的执行阶段,先执行d=10的赋值操作,执行语句会将addAll函数执行上下文中的undefined变成了10。

  1. 当执行到add函数

同时创建执行上下文,并将其压入栈中。
浏览器中的javaScript执行机制 - 图4
当add函数返回时,该函数的执行上下文会从栈顶弹出,并将result的值设置为add函数的返回值,也就是9;
浏览器中的javaScript执行机制 - 图5
紧接着addAll函数执行最后一个相加操作后并返回,addAll的执行上下文也会从栈顶部弹出,此时调用栈就只剩下全局上下文了。
浏览器中的javaScript执行机制 - 图6
至此,整个JavaScript流程执行结束了。

3. 词法作用域及作用域链

作用域:我们将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。(变量的生命周期或者说变量与函数的可访问范围)

作用域分为:词法作用域和动态作用域。JavaScript采用的就是词法作用域。

3.1 词法作用域

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的,编译的词法分析阶段基本能够知道全部标识符在哪里以及如何声明的,从而能够预测在执行过程中如何对它们进行查找。

为了更好的理解词法作用域请看图片:
浏览器中的javaScript执行机制 - 图7
从图中可以看出,词法作用域就是根据代码的位置来决定的,其中main函数包含了bar函数,bar函数包含了foo函数,因为JavaScript作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo函数作用域 -> bar函数作用域 -> main函数作用域 -> 全局作用域。

3.1 全局作用域、函数作用域以及块级作用域

在ES6之前,ES的作用域只有两种: 全局作用域和函数作用域。
ES6开始支持:块级作用域

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
  • 块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。(作用域块内声明的变量不影响块外面的变量)