🚀🚀带你手撕大厂面试高频题---预编译机制以及深入理解调用栈

110 阅读5分钟

引言

前面我们掌握了一部分JavaScript的基础用法,但是对于要冲击大厂的各位来说,掌握一门语言的使用还不够,还应当理解一些底层的运行机制。JavaScript作为一门广泛应用于前端开发的脚本语言,其执行过程分为两个阶段:预编译阶段和执行阶段。理解JavaScript的预编译机制和执行上下文是深入掌握这门语言的关键。本文将深入探讨JavaScript中的预编译过程,解析变量和函数的声明提升,并结合实际代码示例详细讲解全局和函数级上下文的创建与管理,以及调用栈的运行机制。

目录

  1. 预编译与声明提升

  2. 函数中的预编译

  3. 全局预编译

  4. 调用栈与执行上下文

  5. 总结

预编译与声明提升

在JavaScript中,变量和函数的声明会在代码执行之前被提升(Hoisting)到其作用域的顶部。这意味着变量和函数可以在它们定义之前被引用。

变量声明提升

console.log(a); // 输出: undefined
var a = 1;

在预编译阶段,JavaScript引擎将变量声明var a提升到当前作用域的顶部,但不会提升其赋值部分。因此,在执行console.log(a)时,a的值为undefined

函数声明提升

foo(); // 输出: "Hello, world!"
function foo() {
    console.log("Hello, world!");
}

函数声明会整体提升到作用域顶部,因此可以在函数声明之前调用它们。

函数中的预编译

函数预编译过程涉及创建函数执行上下文对象(Activation Object, AO),并处理形参、变量和函数声明。

示例代码

function fn(a) {
    console.log(a); // 输出: function a() {}
    var a = 123;
    console.log(a); // 输出: 123
    function a() {}
    console.log(a); // 输出: 123
    var b = function() {};
    console.log(b); // 输出: function() {}
    function d() {}
    var d = a;
    console.log(d); // 输出: 123
}

fn(1);

预编译阶段 AO 对象的变化

  1. 创建AO对象:AO = {}

  2. 处理形参和变量声明:

    • 形参aAO = { a: undefined }
    • 变量aAO = { a: undefined }
    • 变量bAO = { a: undefined, b: undefined }
    • 变量dAO = { a: undefined, b: undefined, d: undefined }
  3. 将形参和实参统一:AO = { a: 1, b: undefined, d: undefined }

  4. 处理函数声明:

    • 函数aAO = { a: function a() {}, b: undefined, d: undefined }
    • 函数dAO = { a: function a() {}, b: undefined, d: function d() {} }

执行阶段

  1. 执行console.log(a):输出function a() {}
  2. 赋值a = 123AO = { a: 123, b: undefined, d: function d() {} }
  3. 执行console.log(a):输出123
  4. 执行console.log(a):输出123
  5. 赋值b = function() {}AO = { a: 123, b: function() {}, d: function d() {} }
  6. 执行console.log(b):输出function() {}
  7. 赋值d = aAO = { a: 123, b: function() {}, d: 123 }
  8. 执行console.log(d):输出123

全局预编译

全局预编译类似于函数预编译,但作用于全局作用域,创建全局执行上下文对象(Global Object, GO)。

示例代码

var global = 100;
function fn() {
    console.log(global); // 输出: undefined
}

fn();

预编译阶段 GO 对象的变化

  1. 创建GO对象:GO = {}

  2. 处理变量声明:

    • 变量globalGO = { global: undefined }
  3. 处理函数声明:

    • 函数fnGO = { global: undefined, fn: function fn() {} }
  4. 赋值global = 100GO = { global: 100, fn: function fn() {} }

调用栈

什么是栈?

栈(Stack)是一种数据结构,它遵循“后进先出”(LIFO, Last In First Out)的原则。栈的操作主要包括:

  1. 压栈(unshift) :将一个元素压入栈顶。
  2. 弹栈(shift) :将栈顶的元素弹出。

调用栈

调用栈(Call Stack)是管理函数调用关系的一种数据结构。每当一个函数被调用时,会创建一个新的执行上下文并将其推入调用栈;函数执行完毕后,该上下文会从栈中弹出。

示例代码

function first() {
    second();
    console.log("First");
}

function second() {
    console.log("Second");
}

first();

调用过程

  1. 调用first,创建first的执行上下文并推入调用栈
  2. first内部调用second,创建second的执行上下文并推入调用栈
  3. second执行完毕,弹出second的执行上下文
  4. first继续执行,直至完毕,弹出first的执行上下文

调用栈的演示代码

我们通过上面的GO预编译和AO预编译的代码来演示调用栈的运行机制。

示例代码

var global = 100;

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

fn();

调用栈分析

1.全局上下文进入栈中

  • 创建全局执行上下文(GO)。
  • 处理全局变量和函数声明:
GO = {
    global: undefined,
    fn: function fn() {}
}

  • 赋值全局变量:
GO = {
    global: 100,
    fn: function fn() {}
}

  1. 调用fn函数,创建函数执行上下文并推入调用栈
  • 在fn函数执行前,创建fn的执行上下文(AO)。

  • 处理fn函数中的变量声明:

AO = {
            global: undefined
        }
  1. fn函数执行阶段
  • 执行console.log(global)

  • 查找AO,globalundefined

  • 赋值global = 200

AO = {
            global: 200
        }
  • 执行console.log(global)

  • 查找AOglobal200

  • 赋值global = 300

AO = {
            global: 300
        }
  1. fn函数执行完毕,函数上下文从调用栈弹出
  2. 全局上下文从栈中弹出

总结

理解JavaScript的预编译机制和调用栈的工作原理是掌握这门语言的关键。通过创建和管理执行上下文对象(GO和AO),JavaScript引擎能够在代码执行之前准备好所需的上下文信息。调用栈则确保函数调用顺序和上下文管理,使得代码能够按预期执行。希望本文能再深化你对JS这门语言的理解和认知,并在编写和调试JavaScript代码时有所助益。望助你在编程之路越走越远!