先有鸡还是先有蛋?--预编译

347 阅读5分钟

在JavaScript中,执行上下文和作用域链是理解代码执行流程、变量查找机制以及函数行为的核心概念。本文将深入探讨声明提升、函数中的预编译过程、全局预编译以及调用栈的工作原理,帮助您构建对JavaScript运行机制的深刻理解。

先有鸡还是先有蛋

我们先来看看以下代码,你认为会输出什么呢?

a = 2;
var a;
console.log(a);

有的人会认为是undefined,因为 var a 声明在a = 2后面,他们认为变量被重新赋值了,但真正的输出结果是2,你对了吗?接下来,我们再看到另外一段代码

consol.log(a);
var a = 2;

这里肯定有的人认为,a在使用之前没有声明,会抛出ReferenceError异常;还有人认为会和上面一样输出2,但不幸的是这俩种答案都是不对的,这里会输出undefined

那么这到底发生了什么呢?到底是蛋(声明)在前,还是鸡(赋值)在前?

声明提升(Hoisting)

首先,我们要知道什么是声明提升

声明提升是JavaScript中一个独特的行为,它涉及到变量声明和函数声明在代码执行前被“提前”到当前作用域的顶部。这一机制使得我们可以在声明之前使用变量和函数。

  1. 变量声明提升:使用var声明的变量会被提升至其所在作用域的顶部,但其赋值操作不会提升。因此,在变量声明之前的代码访问该变量时,其值为undefined
  2. 函数声明提升:整个函数声明(包括名称和函数体)都会被提升到作用域的顶部。这意味着你可以在函数声明之前调用该函数。

看完以上内容,我们再重新看到我们之前的代码,就可以写出他们的运行的流程了

第一段代码会以如下形式处理:

var a;           
a = 2;          //由于变量声明提升,var a声明会被提升到作用域的顶部,所以var a先运行
console.log(a);

再来看看第二段代码,他们会以如下形式处理:

var a;                  
console.log(a);
a = 2;        //当你看到var a = 2时,它其实是俩个声明,var a和a = 2,然后变量声明提升,var a被提到作用域的顶部 

所以在这里我们得到前面的答案,先有蛋(声明)后有鸡(赋值)

函数中的预编译

接下来我们在看到以下代码,你认为会怎么样输出呢?

function fn(a) {
    console.log(a);
    var a = 123
    console.log(a);
    function a() {}
    console.log(a);
    var b = function () {} // 函数表达式
    console.log(b);
    function d() {}
    var d = a
    console.log(d);
  }
  fn(1)

现在公布答案

[Function: a]

123

123

[Function: b]

123

你答对了吗?这里我们就需要了解当一个函数被运行时,会进行什么样的步骤了。

当一个函数被调用时,会进行以下预编译步骤:

  1. 创建激活对象(AO, Activation Object) :为该函数创建一个新的执行上下文,即激活对象,用于存储函数执行期间的所有局部变量、参数和内部函数。
  2. 初始化形参和变量声明:将函数的形参和函数内部的变量声明作为激活对象的属性,初始值均为undefined。这一步确保了所有声明都在函数体执行前可用。
  3. 实参与形参绑定:如果函数调用时提供了实参,这些实参会与对应的形参进行匹配并赋值。
  4. 处理函数声明:在函数体中查找函数声明,并将其添加为激活对象的属性,覆盖之前可能因变量声明提升产生的同名属性。此时,函数声明的值为实际的函数体。

全局的预编译

我们依旧看一段代码

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

下面是输出的结果:

undefined

200

这里我们需要了解全局的预编译

对于全局执行上下文,预编译过程如下:

  1. 创建全局执行上下文对象(GO, Global Object) :在浏览器环境中,这通常是window对象;在Node.js环境中,则是global对象。
  2. 变量声明:将所有使用var声明的全局变量作为全局对象的属性,初始值为undefined
  3. 函数声明:与函数内的处理类似,全局函数声明也会被添加到全局对象作为属性,其值为函数体。这意味着你可以直接通过全局对象访问这些函数。

调用栈(Call Stack)

调用栈是一种数据结构,用于跟踪函数的调用顺序和状态。在JavaScript中,每当一个函数被调用时,都会创建一个新的执行上下文并压入调用栈顶。函数执行完毕后,其执行上下文会从栈顶弹出,控制权返回到调用者。

  • 工作原理:调用栈确保了函数的执行顺序,遵循“先进后出”的原则。它帮助JavaScript引擎记住当前执行的位置和上下文,以及哪些函数需要返回去继续执行。
  • 应用场景:理解调用栈对于调试、特别是处理递归调用或理解某些错误(如堆栈溢出错误)至关重要。

总结

总之,声明提升、预编译过程以及调用栈共同构成了JavaScript程序执行的基础框架。通过深入理解这些机制,开发者可以更有效地编写、调试和优化代码,避免潜在的陷阱,提高程序的可读性和健壮性。

好的,这次的内容就分享到这了,如果小友觉得整的还不错的,可以留下一个小小的赞帮助俺找回自己的脑子,谢谢啦!!!