“秘书”与“老板”:预编译和执行上下文

121 阅读4分钟

在编程的世界里,JavaScript是一门既强大又灵活的语言,它为我们提供了丰富的功能来创建动态网页、构建复杂的网络应用,甚至是开发桌面软件和移动应用。但在这强大的背后,JavaScript有一套自己的规则和机制,理解这些规则对于写出高效、可维护的代码至关重要。今天,我们就来揭开JavaScript中几个核心概念的神秘面纱:声明提升(Hoisting)、函数中的预编译(Precompilation)以及调用栈(Call Stack)。

一、声明提升:变量与函数的“时光旅行”

想象一下,你正在编写一段JavaScript代码,突然发现一个神奇的现象:即使你在使用变量或函数之前声明它们,代码依然可以正常工作。这背后的秘密就是“声明提升”。

1. 变量声明的提升

在JavaScript中,当你声明一个变量时(无论是使用varlet还是const),这个声明会被提升到当前作用域的顶部。但是,这里有一个重要的区别:

  • 使用var声明的变量,不仅声明会被提升,初始化的undefined也会被提升。
  • 而使用letconst声明的变量虽然逻辑上也被提升了,但实际上在赋值前处于“暂时性死区”(Temporal Dead Zone, TDZ),访问它们会报错。

举个例子:

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

这段代码看起来像是先使用了变量a,然后才声明并赋值。但实际上,因为变量声明被提升到了作用域的顶部,所以不会报错,只是输出undefined。但是使用了letconst声明的变量就会报错

2. 函数声明的提升

函数声明也有类似的提升机制,但更彻底:整个函数定义都会被提升到作用域的顶部。这意味着你可以在声明函数之前就调用它。

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

尽管sayHello的调用在它的声明之前,但由于函数声明的提升,这段代码可以正常运行。

二、函数中的预编译:AO

想象一下,在一个繁忙的公司里,老板(执行上下文)正准备处理一项重要任务——召开一次会议。为了这次会议顺利进行,他的秘书(JavaScript引擎)会事先做好一系列精心的准备工作,这就是我们所说的“预编译”。

当一个函数被调用时,JavaScript引擎会进行一系列的准备工作,我们称之为“预编译”或“函数的执行上下文创建”。

  1. 创建函数的执行上下文对象 AO {Activation Object}
  2. 找形参和变量声明,将形参和变量名作为AO的属性,值为undifined
  3. 将实参和形参统一
  4. 在函数体内找函数声明,将函数名作为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 b
    function d() {}
    var d = a
    console.log(d);   // 123
}
fn(1)

输出的分别是 function a, 123, 123, function b, 123。根据上述步骤将值传入到对应的变量作为值,咱们可以列出以下式子:

AO: {
    a: undefined  1  function a() {}  123,
    b: undefined  function b() {},
    d: undefined  function d() {}  123,
}

三、全局的预编译:GO

全局执行上下文是所有非函数代码执行的环境,也是JavaScript程序的起点。它遵循类似的预编译规则:

  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()
console.log(global); // 100
var global;

全局上下文对象GO和函数执行上下文AO如下:

GO: {
    global: undefined  100,
    fn: undefined  function () {},
}
AO: {
    global: undefined  200  300,
}

四、调用栈:函数的“回忆录”

最后,让我们谈谈调用栈。在JavaScript中,函数调用并不是孤立发生的,而是相互嵌套和依赖的。调用栈就像是记录这些函数调用过程的日记本,它帮助JavaScript引擎追踪当前执行到哪一层函数,以及如何返回到上一层。

每当一个函数被调用,它的执行上下文就会被压入调用栈的顶部。当函数执行完毕,其执行上下文就会从栈顶弹出,控制权返回到调用它的函数。如果函数调用太深,超过了调用栈的限制,就会引发“堆栈溢出”错误。

结语

通过这篇介绍,我们希望能为你揭开JavaScript中声明提升、预编译以及调用栈的神秘面纱。理解这些概念不仅能够帮助你避免许多常见的编程错误,还能让你的代码更加清晰、高效。记住,JavaScript总是在幕后默默进行着这些准备工作,以确保你的每行代码都能按照预期运行。