前言
少年,恭喜你捡到这本秘籍。这本秘籍将帮助你学习JavaScript,成为JS高手。接下来让我们翻开秘籍的第一页,学习JS的预编译原理。
我们先来看一段简单的代码:
console.log(a);
var a = 10
这一段代码乍一看很怪,怎么先输出再声明变量?而且编译不会报错,输出结果为
别着急,我们一步步来,我们先简单了解一下V8引擎的工作原理:
V8引擎的工作原理
1. 分词(词法分析)
把代码字符串切割成一个个有意义的词法单元(token),比如 let x = 1 → ['let', 'x', '=', '1']。去掉空格、注释,为下一步做准备。
2. 语法分析 → AST
根据语法规则,把 tokens 组装成抽象语法树(AST)。树的每个节点对应一个语法结构(变量声明、函数调用等)。比如 let x = 1 会变成一个 VariableDeclaration 节点,下面挂 Identifier(x)和 Literal(1)子节点。
3. 代码生成
V8 不会直接生成机器码(太慢),而是生成字节码(中间码,比机器码轻量,比 AST 快)。这一步遍历 AST,产出可执行的字节码序列,交给解释器运行。后续热点字节码才会被编译成机器码。
三者关系:
源代码 → 分词(token流)→ 语法分析(AST)→ 代码生成(字节码)→ 执行
想象一下,你是一个修仙宗门的宗主
平日里,你不仅需要处理宗门内部的事务,还要专心修炼达得更高的境界。但是宗门内部的琐事实在太多,你的时间宝贵,所以你让书童先按照轻重缓急帮你整理好这些事务,再帮你磨好墨,沏好茶,让你以最高的效率解决。而预编译,就是各位宗主的书童。
预编译
在 JavaScript 中,“预编译”并不是 ECMAScript 标准里的正式术语,但常被用来描述代码执行前 创建执行上下文(Execution Context) 的过程。预编译有两种:函数体内的预编译和全局的预编译。 我们先从函数体内的预编译说起:
函数体内的预编译
这是咱们秘籍的功法口诀,一定要记牢哦:
- 创建一个执行上下文 AO: {}
- 找形参和变量声明,将形参和变量名作为属性名,添加到 AO 中,值为 undefined
- 将形参和实参统一
- 在函数体内找函数声明,将函数名作为AO中的属性名,函数体作为属性值
接下来让我们来看一段代码,帮助你理解
function fn(a) {
console.log(a);
var a = 123
console.log(a);
function a() {}
var b = function() {}
console.log(b);
function c() {}
var c = a
console.log(c);
}
fn(1)
首先我们创建一个AO
AO = {
a: undefined->1->function a() {}->123
b: undefined->function b() {}
c: undefined->function c() {}->123
}
以上是AO的属性a、b、c的属性值改变的过程,以a为例:同时存在形参a和声明的实参a,两者此时的值都为undefined,再将形参与实参统一,此时只剩一个值为undefined的变量a。此时预编译的工作已经完成,开始执行阶段:为a赋值为1,然后调用函数。输出结果如下:
你做对了吗?没做对的话可以再多练几次,熟悉这个分析流程。
全局的预编译
同样的,这也是秘籍的口诀,把它记牢可以帮助你突破练气期第一层:
- 创建一个全局执行上下文 GO: {}
- 找全局变量声明,将全局变量名作为属性名,添加到GO中,值为undefined
- 找全局函数声明,将函数名作为GO的属性名,函数体作为属性名 我们依然来看一段代码:
global = 100
function fn() {
console.log(global);//undefined
global = 200
console.log(global);//200
var global = 300
}
fn()
var global
让我们按照流程一步步分析:首先我们创建一个GO:
GO = {
global: undefined -> 100
fn: function fn() {}
}
此时编译器以为自己干完活了,美滋滋地准备休息,结果执行时发现有调用函数,又屁颠屁颠地跑回来又对函数体进行预编译:
AO = {
global: undefined -> 200 -> 300
}
这里我们要注意,比如在执行函数体时,我们首先在我们所在的域内找相对应的变量,如果域内没有的话我们再去全局寻找。输出结果应该显而易见了:
结尾
恭喜你少年,你已经学完了本秘籍的第一章!这是你成长路程的第一步,只要一步一个脚印,我们终将变得强大。与诸君共勉!