JS预编译:V8的魔法工厂,变量提升的前世今生

143 阅读5分钟

JS预编译:V8的魔法工厂,变量提升的前世今生

前言:你以为你在写JS,其实你在和V8玩心理战

大家好,我是一个热爱“刨根问底”的前端搬砖工。你是否曾经在面试时被问到:“JS的变量提升到底是怎么回事?”你是否在debug时,console.log出来的undefined让你怀疑人生?别怕,今天我们就来揭开JS预编译的神秘面纱,看看V8引擎到底在背后搞了多少“小动作”。


一、V8的“预编译”到底是啥?

先来一段官方(其实是readme.md)解读:

  • V8引擎读取到JS代码后,先编译再执行
  • 编译顺序:编译全局 → 执行全局 → 编译函数 → 执行函数

这就像你点了一份外卖,V8会先把所有菜谱(变量、函数声明)都记下来,等你真正“吃饭”(执行)的时候,才会一道道上菜。


二、全局编译过程:变量和函数的“排排坐”

全局编译时,V8会做三件事:

  1. 创建全局执行上下文对象(Execution Context Object,简称ECO)。
  2. 找变量声明,把变量名作为ECO的属性名,值先设为undefined
  3. 找函数声明,把函数名作为ECO的属性名,值为函数体。

举个栗子:

console.log(a);
var a = 1;
function foo() {}

V8在“预编译”阶段已经把afoo都挂在了全局上下文上,只不过a的值是undefinedfoo已经是个完整的函数体了。


三、函数体编译过程:参数、变量、函数的“三国杀”

函数体的编译比全局还热闹,V8会:

  1. 创建函数的执行上下文对象。
  2. 找形参和变量声明,把它们都挂到上下文对象上,值为undefined
  3. 形参和实参统一(有实参就覆盖形参)。
  4. 找函数声明,函数名挂到上下文对象上,值为函数体(注意:会覆盖同名变量或参数)。

看个“烧脑”例子:

function fn(a) {
    console.log(a);
    var a = 123;
    console.log(a);
    function a() {}
    var b = function() {};
    console.log(b);
    function d() {}
    var d = a;
    console.log(d);
}
fn(1);

执行流程揭秘:

  • 预编译阶段,abd都被挂到fn的上下文对象上,初始值undefined
  • 形参a和实参1统一,a=1
  • 函数声明ad会覆盖同名变量/参数,a变成函数体,d变成函数体。
  • 进入执行阶段,console.log(a)输出的是函数a,不是1,也不是undefined!

输出顺序:

  1. function a() {}(因为函数声明提升且覆盖了参数a)
  2. 123(var a = 123,覆盖了上面的a)
  3. function() {}(b是函数表达式,已赋值)
  4. 123(d被赋值为a的当前值123)

四、变量提升的“宫斗剧”:var、function、参数谁说了算?

再看一个“宫斗剧”:

function foo(a, b) {
    console.log(a);
    c = 0;
    var c;
    a = 3;
    b = 2;
    console.log(b);
    function b() {}
    console.log(b);
}
foo(1);

预编译阶段:

  • 形参a=1,b=undefined
  • 变量声明:a、b、c都挂上,初始undefined
  • 函数声明b,覆盖了参数b

执行阶段:

  1. console.log(a)输出1
  2. c=0,c变成0
  3. a=3,a变成3
  4. b=2,b变成2
  5. console.log(b)输出2
  6. console.log(b)输出2

小结:

  • 函数声明优先级最高,覆盖参数
  • 变量声明只提升,不赋值
  • 形参和实参统一后,可能被函数声明覆盖

五、全局变量的“真假身份”:global、window、var的迷惑行为

再来个“灵魂拷问”:

global = 100;
function fn() {
    console.log(global); // undefined
    global = 200;
    console.log(global); // 200
    var global = 300;
}
fn();
var global;

预编译阶段:

  • 全局上下文:global=undefined,fn=函数体
  • 执行global=100,global=100
  • 执行fn时,fn的上下文对象里有var global,初始undefined
  • console.log(global)输出undefined(因为函数作用域内的global屏蔽了全局的global)
  • global=200,函数作用域内的global=200
  • var global=300,global=300
  • 函数执行完毕,回到全局,global还是100

结论:

  • 函数作用域内的变量会屏蔽全局同名变量
  • 变量提升只提升声明,不提升赋值

六、对象属性的“预编译”迷思

虽然对象属性和预编译没直接关系,但很多初学者会混淆:

var obj = {
    name: '路明非',
    age: 18,
};
obj.age = 19;
console.log(obj);

对象属性的赋值和提升无关,都是即时生效的。别把对象的“属性查找”当成变量提升哦!


七、图片助攻:一图看懂编译执行原理

image.png


八、面试官的灵魂拷问:你真的懂预编译吗?

面试官:JS的变量提升和函数提升有什么区别?

你:变量提升只提升声明,赋值在原地;函数提升提升整个函数体,优先级比变量高!

面试官:如果函数参数和函数声明、变量声明重名,谁说了算?

你:函数声明最大,参数次之,变量声明最小!

面试官:你能画出执行上下文的变化过程吗?

你:当然可以!(掏出编译执行原理.png

image.png


九、总结:和V8做朋友,和变量提升握手言和

JS的预编译和执行上下文,看似晦涩,其实就是V8在帮你“排兵布阵”。理解了这些底层原理,你就能写出更健壮、更不容易“踩坑”的代码。下次再遇到undefined,你就能淡定一笑:“这不是bug,是V8的套路!”


如果你觉得这篇文章有用,欢迎点赞、收藏、关注我!下期我们继续深扒JS底层原理,让你面试不再慌,写代码更自信!