本文转载于 SegmentFault 社区
社区专栏:前端
作者:君前
·
引擎:
引擎爸爸的工作,负责整个 JavaScript 程序的编译及执行过程
·
编译器:
引擎的好朋友,负责词法、语法分析及代码生成等脏活累活
·
作用域:
引擎的另一个好朋友,负责创建并维护所有的声明
(变量,函数)
,并实施一套严格的规则,规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
如果想深入学习作用域的相关知识请查看
《JavaScript深入之作用域》
。
我们经常说 JavaScript 是一门解释型语言,区别于编译型语言,但是实际上 JavaScript 是一门编译语言。但与传统的编译语言不同, 它不是提前编译的, 编译结果也不能在分布式系统中进行移植。
事实上,任何 JavaScript 代码片段在执行前都要进行编译,然后做好执行它的准备,通常编译后就会马上执行。
我们以下面这段代码为例来说明 JavaScript 到底是如何执行代码的。
var a = 2;
function m() {
console.log('m');
}
m();
上面我们说了 JavaScript 在执行代码前是先进行编译的,上述代码虽然只有三块
(变量声明、函数声明、函数调用)
,但是对于引擎来说却相当于四个指令:
·
var a
和
function m() {}
:编译阶段执行;
(所有的变量和函数声明都是在编译阶段执行的)
下面我们将详细介绍一下在编译和执行阶段具体是如何处理的。
编译阶段
如果不了解编译原理的相关知识(词法分析、语法分析、AST、代码生成),请查看
《编译原理之基础篇》
https://segmentfault.com/a/1190000021931476
1. 编译器首先会将这段程序分解成词法单元
(词法分析)
,然后将词法单元解析成 AST
(语法分析)
,然后开始根据 AST 生成机器指令
(代码生成)
。
当编译器遇到
var a
时,编译器会询问作用域是否已经存在一个该名称的标识符,如果存在,编译器则忽略该指令,继续编译;否则它会要求作用域在当前作用域的集合中声明一个命名为 a 的标识符
(变量)
。
同理,在编译器遇到
function m() {}
时,也会去询问作用域是否存在命名为 m 的函数声明,如果存在,编译器则忽略该指令,继续编译;否则它会要求作用域在当前作用域的集合中声明一个命名为 m 的函数。
3. 编译器为引擎生成运行时所需代码之后,引擎开始执行代码。
执行阶段
遇到
a = 2
时,会询问作用域,在当前作用域是否存在一个叫作 a 的标识符,如果是,作用域就会将其返还给引擎,引擎则会使用这个标识符;如果否,引擎会继续查找该变量
(询问当前作用域的父级作用域,依次类推,直到顶层(全局作用域))
。
否则在严格模式下,引擎会抛出一个
ReferenceError 异常
(同作用域判别失败相关,作用域中没有找到想要的标识符);
在非严格模式下,如果在顶层
(全局作用域)
中也无法找到目标标识符,全局作用域中就会隐式创建一个具有该名称的标识符, 并将其返还给引擎。
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量
(如果之前没有声明过)
,然后再运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
当引擎遇到
m();
时,查找 m 标识符的过程同上。不同点是:在找不到 m 是,在非严格模式下,不会隐式创建一个具有该名称的标识符,在下面
【执行过程中引擎是如何查找变量的】
中会解释原因。
还有一个需要注意的点是,当找到 m 标识符后,引擎会开始执行该函数。此时如果该标识符代表的是一个函数,那么函数可以正常执行;但是如果标识符代表的是一个变量,那么就会抛出
TypeError
异常
(代表作用域判别成功了, 但是对结果的操作是非法或不合理的)
,如下图所示:
此时 a 是一个变量,所以当尝试执行它时,会报错 a 不是一个函数。
执行过程中引擎是如何查找变量的