语法检查

语法检查是 JavaScript 解析器的工作之一,包括 Î词法分析语法分析

词法分析

把 JavaScript 代码(字符创)逐字转换为标记流

例如:

1
// a = (b - c);

转换为:

1
2
3
4
5
6
7
8
NAME "a"
EQUALS
OPEN_PARENTHESIS
NAME "b"
MINUS
NAME "c"
CLOSE_PARENTHESIS
SEMICOLON

语法分析

语法分析:JavaScript 语法分析器在经过词法分析后,将记号流按照 ECMAScript 标准把词法分析所产生的记号生成语法树 AST。

运行阶段

预解析

  1. 将 JavaScript 引擎将语法检查正确后生成的语法树复制到当前执行上下文中。
  2. JavaScript 引擎会对语法树当中的变量声明函数声明以及函数的形参进行属性填充( 声明提升)。

执行上下文包括:变量对象、作用域链、this

  • 变量对象(Variable Object):由变量声明(var declaration)、函数声明(function declaration)、参数(arguments)构成。
  • 作用域链(Scope Chain): 变量对象+父级作用域
  • this :一旦进入代码执行阶段的上下文中(预编译后的代码执行阶段),this 的值就不会变了。

变量对象/活动对象(VO/AO)属性填充

  • 变量对象/活动对象(VO/AO)填充的顺序:函数的形参->函数声明->变量声明;在变量对象/活动对象(VO/AO)中权重高低也按照函数的形参->函数声明->变量声明顺序来。

  • 函数的形参:执行上下文的变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为 undefined。

  • 函数声明:执行上下文的变量对象的一个属性,属性名和值都是函数对象创建出来的;如果变量对象已经包含了相同名字的属性,则会替换它的值。

  • 变量声明:执行上下文的变量对象的一个属性,其属性名即为变量名,其值为 undefined;如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的函数声明的属性,该声明会被忽略掉,但其包含的赋值操作不会忽略。

    1
    2
    3
    4
    5
    6
    7
    8
    // 函数提升优先级高于变量提升,所以b取函数
    var a = 1
    function b(a) {
    alert(a)
    }
    var b // 被忽略,因为函数声明优先级高于变量声明
    alert(b) // function b(a) { alert(a); }
    b() //undefined

代码执行

经过“预解析”创建执行上下文之后,就进入执行代码阶段,VO/AO 就会重新赋予真实的值,“预解析”阶段赋予的 undefined 值会被覆盖。

此阶段才是程序真正进入执行阶段,Javascript 引擎会一行一行的读取并运行代码。此时那些变量都会重新赋值。

假如变量是定义在函数内的,而函数从头到尾都没被激活(调用)的话,则变量值永远都是 undefined 值。

进入了执行代码阶段,在“预解析”阶段所创建的任何东西可能都会改变,不仅仅是 VO/AO,this 和作用域链也会因为某些语句而改变,后面会讲到。

作用域链(Scope Chain)

​ 作用域链是处理标识符时进行变量查询的变量对象列表,每个执行上下文都有自己的变量对象:对于全局上下文而言,其变量对象就是全局对象本身;对于函数而言,其变量对象就是活动对象。

作用域链和执行上下文的关系

在 Javascript 中只有函数能规定作用域,全局执行上下文中的 Scope 是全局上下文中的属性,也是最外层的作用域链。
函数内部的属性(作用域)是在“预解析”的时候就已经存在的了,它包含了所有上层变量对象,并一直保存在函数中。就算函数永远都没被激活(调用),也都还是存在函数对象上。

执行上下文定义的 Scope 属性变化过程

执行上下文中的[AO]是函数的活动对象,而[[Scope]]则是该函数属性作用域。当前函数的 AO 永远是在最前面的,保存在堆栈上,而每当函数激活的时候,这些 AO 都会压栈到该堆栈上,查询变量是先从栈顶开始查找,也就是说作用域链的栈顶永远是当前正在执行的代码所在环境的 VO/AO(当函数调用结束后,则会从栈顶移除)。

通俗点讲就是:JavaScript 解释器通过作用域链将不同执行位置上的变量对象串连成列表,并借助这个列表帮助 JavaScript 解释器检索变量的值。作用域链相当于一个索引表,并通过编号来存储它们的嵌套关系。当 JavaScript 解释器检索变量的值,会按着这个索引编号进行快速查找,直到找到全局对象为止,如果没有找到值,则传递一个特殊的 undefined 值。

是不是又想到了一条 JavaScript 高效准则:为什么说在该函数内定义的变量,能减少函数嵌套能提高 JavaScript 的效率?因为函数定义的变量,此变量永远在栈顶,这样子查询变量的时间变短了。

作用域链特性
闭包是作用域链特性的应用

保证查询有序的访问所有变量和函数
作用域链感觉就是一个 VO 链表,当访问一个变量时,先在链表的第一个 VO 上查找,如果没有找到则继续在第二个 VO 上查找,直到搜索结束,也就是搜索到全局执行环境的 VO 中。这也就形成了作用域链的概念。