天阶功法现世!JavaScript变强秘籍第一章:JS的预编译原理

1 阅读4分钟

前言

少年,恭喜你捡到这本秘籍。这本秘籍将帮助你学习JavaScript,成为JS高手。接下来让我们翻开秘籍的第一页,学习JS的预编译原理。

古风修仙少年捡秘籍.png 我们先来看一段简单的代码:

console.log(a);
var a = 10

这一段代码乍一看很怪,怎么先输出再声明变量?而且编译不会报错,输出结果为

屏幕截图 2026-05-15 153710.png

别着急,我们一步步来,我们先简单了解一下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)→ 代码生成(字节码)→ 执行

想象一下,你是一个修仙宗门的宗主

修仙宗门宗主理事场景.png 平日里,你不仅需要处理宗门内部的事务,还要专心修炼达得更高的境界。但是宗门内部的琐事实在太多,你的时间宝贵,所以你让书童先按照轻重缓急帮你整理好这些事务,再帮你磨好墨,沏好茶,让你以最高的效率解决。而预编译,就是各位宗主的书童。

预编译

在 JavaScript 中,“预编译”并不是 ECMAScript 标准里的正式术语,但常被用来描述代码执行前 创建执行上下文(Execution Context) 的过程。预编译有两种:函数体内的预编译和全局的预编译。 我们先从函数体内的预编译说起:

函数体内的预编译

这是咱们秘籍的功法口诀,一定要记牢哦:

  1. 创建一个执行上下文 AO: {}
  2. 找形参和变量声明,将形参和变量名作为属性名,添加到 AO 中,值为 undefined
  3. 将形参和实参统一
  4. 在函数体内找函数声明,将函数名作为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,然后调用函数。输出结果如下:

屏幕截图 2026-05-15 163202.png

你做对了吗?没做对的话可以再多练几次,熟悉这个分析流程。

全局的预编译

同样的,这也是秘籍的口诀,把它记牢可以帮助你突破练气期第一层:

  1. 创建一个全局执行上下文 GO: {}
  2. 找全局变量声明,将全局变量名作为属性名,添加到GO中,值为undefined
  3. 找全局函数声明,将函数名作为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
}

这里我们要注意,比如在执行函数体时,我们首先在我们所在的域内找相对应的变量,如果域内没有的话我们再去全局寻找。输出结果应该显而易见了:

屏幕截图 2026-05-15 165604.png

结尾

恭喜你少年,你已经学完了本秘籍的第一章!这是你成长路程的第一步,只要一步一个脚印,我们终将变得强大。与诸君共勉!