墨言妹带你细读《你不知道的
JavaScript
》系列的世界,深入JavaScript
语言内部,弄清楚JavaScript
每一个零部件的用途。
作用域是什么
- 问题1:变量存储在哪里?
- 问题2:程序需要时如何找到它们?
1.1 编译原理
通常,把 JavaScript
归类为 “ 动态 ” 或 “ 解释执行 ” 的语言,但是事实上它是一门 编译语言,不提前编译,编译结果也不在分布式系统中进行移植。
JavaScript 引擎进行编译的步骤和传统的编译语言非常相似,在某些环节比它要复杂。
传统编译语言,在执行之前的三个步骤,统称为 “ 编译 ” 。
-
分词/词法分析(
Tokenizing/Lexing
)将有字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(
token
)。var a = 2; 复制代码
被分解成词法单元:
var
、a
、=
、2
、;
。空格在该语言中有意义,则会被当做词法单元,否则不是。 -
解析/语法分析(
Parsing
)将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的 “ 抽象语法树 ”(
Abstract Syntax Tree ,
AST
)。var a = 2; 复制代码
以上代码的抽象语法树如下所示:
VariableDeclaration
顶级节点Identifer
子节点,值为a
AssignmentExpression
子节点NumericLiteral
子节点,值为2
-
代码生成
将
AST
转换为可执代码的过程被称为代码生成。这个过程与语言、目标平台等相关。即通过某种方法,将
var a = 2 ;
的AST
转化为一组机器指令,用来创建一个变量a
,并将值存储在a
中。引擎,可以根据需要创建并存储变量。
1.2 理解作用域
1.2.1 演员表
- 引擎,从头到尾负责整个
JavaScript
程序的编译及执行过程。 - 编译器,负责语法分析及代码生成等脏活累活。
- 作用域,负责收集并维护由所有声明的标识符(变量)组成一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
1.2.2 对话
JavaScript
引擎是如何处理 JavaScript
代码的?
比如 var a = 2;
存在2个不同的声明,变量的赋值操作会执行两个动作。
- 遇到
var a
,在编译阶段,编译器先询问当前作用域,在作用域集合中是否存在变量a
,若不存在则声明一个新变量名为a
;接着编译器会为引擎生成运行时所需的代码,处理a = 2
这个赋值操作。 - 遇到 (
a = 2
),在执行阶段,引擎运行时先询问作用域,在作用域中查找该变量a
,如果找到就将值2
赋值给变量a
,否则引擎就会举手示意并抛出一个异常。
1.2.3 编译器有话说
-
如何理解引擎、编译器、作用域的关系
- 代码先编译后执行,当编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量
a
来判断它是否已经声明过。 - 查找的过程由作用域进行协助,但是引擎执行怎样的查找,会影响最终的查找结果。对
JavaScript
引擎的性能要求很高。
- 代码先编译后执行,当编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量
-
引擎查找的两种方式:
RHS
和LHS
LHS
查询(左侧):找到变量的容器本身,然后对其赋值,即赋值操作的目标是谁。比如a = 2;
,为= 2
这个赋值操作找到一个目标。RHS
查询(非左侧):查找某个变量的值,理解为retrieve his source value
,即谁是赋值操作的源头。比如:console.log( a );
,需要获取到变量a
的值,则对变量a
的RHS
查询,并传值给console.log(...)
。
function foo(a){
console.log( a ); //2
}
foo(2);
复制代码
上述代码共有1处 LHS
查询,3处 RHS
查询。
LHS
查询有:- 隐式的
a = 2
中,在2
被当做参数传递给foo(...)
函数时,需要对参数a
进行LHS
查询
- 隐式的
RHS
查询有:- 最后一行
foo(...)
函数的调用需要对foo
进行RHS
查询,意味着 “去找到foo
的值,并把它给我 ” ,并且(...)
意味着foo
的值需要被执行,因此它最好真的示意函数类型的值。 console.log( a );
中对a
进行RHS
查询,并且将得到的值传给了console.log(...)
。console.log(...)
本身对console
对象进行RHS
查询,并且检查得到的值中是否有一个叫作log
的方法。
- 最后一行
1.3 作用域嵌套
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。
- 如果查找的目的是对变量进行赋值,那么就会使用
LHS
查询;如果目的是获取变量的值,就会使用RHS
查询。 - 赋值操作符会导致
LHS
查询。=
操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。
遍历嵌套作用域链的规则: 引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
1.4 异常
为什么区分 LHS
和 RHS
?变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行为是不一样的。
function foo(a){
console.log(a + b);
b = a;
}
foo(2);
复制代码
对一个 “未声明 ” 的变量 b
进行 RHS
查询时,在任何相关的作用域中都无法找到它。
ReferenceError
和作用域判别失败相关,而 TypeError
则代表作用域判别成功了,但是对结果的操作是非法或不合理的。
RHS
查询在作用域链中搜索不到所需的变量,引擎会抛出ReferenceError
异常。- 非严格模式下,
LHS
查询在作用域链中搜索不到所需的变量,全局作用域中会创建一个具有该名称的变量并返还给引擎。 - 严格模式下(
ES5
开始,禁止自动或隐式地创建全局变量),LHS
查询失败并不会创建并返回一个全局变量,引擎会抛出同RHS
查询失败时类似的ReferenceError
异常。 - 在
RHS
查询成功情况下,对变量进行不合理的操作,引擎会抛出TypeError
异常。(比如对非函数类型的值进行函数调用,或者引用null
或undefined
类型的值中的属性)。
最后, 书读百遍其义自见,希望对小伙伴们有帮助,谢谢您的点赞支持与分享。
参考文献: [木易杨博客] github.com/yygmind/Rea…