JavaScript之神秘序曲:预编译迷幻

325 阅读7分钟

引言

在浏览网页时,你是否曾想知道背后是什么使页面动起来?JavaScript的预编译是一个不为人所知却极为重要的环节,它是连接代码与浏览器的纽带,直接影响着网页的交互体验。本文将带你揭开Java Script预编译的神秘面纱,探索其如何影响代码执行,让你更深入地理解这门语言的精妙之处。

js独特的预编译

声明提升

众所周知 js中会出现声明提升的怪事 也就是我们可以先用再赋值 就像这样

console.log(x); // undefined 
var x = 5;

也可以这样

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

Tips

在JavaScript中,使用var声明的变量会被提升到当前作用域的顶部,但其赋值操作会保留在原位置,因此在变量声明前访问该变量会输出undefined。而使用letconst声明的变量也会被提升到当前作用域的顶部,但与var不同的是,它们不会被赋值为undefined,而是保持在暂时性死区(Temporal Dead Zone,TDZ) 中。在TDZ中,访问该变量会导致引发ReferenceError错误。因此,在let或const声明前访问该变量会报错。

由于js中的函数是一等公民,这意味着函数可以像变量一样被传递、赋值和作为参数传递给其他函数。 在预编译阶段,js会将整个函数声明提升到作用域顶部,包括函数体。这使得在函数声明之前调用函数不会引发错误。所以呢,变量提升是提升了,但赋值还呆在那里O T O

因此呢我们可以总结出 声明提升异同
  • 变量声明 声明提升
  • 函数声明 整体提升

函数中预编译

作用域

在 JavaScript 中,作用域(Scope)指的是变量和函数可访问的范围。根据作用域的不同,JavaScript 中可以分为以下三种作用域:

  1. 全局作用域(Global Scope)

    • 全局作用域是指在代码中任何地方都可以访问的范围。在浏览器中,全局作用域通常是指 window 对象下的变量和函数。
    • 在全局作用域中声明的变量和函数可以在代码中的任何地方访问。
  2. 函数作用域(Function Scope)

    • 函数作用域是指在函数内部声明的变量和函数只能在该函数内部访问,外部无法访问。
    • 在函数内部声明的变量和函数在函数执行时创建,在函数执行完毕后被销毁。
  3. 块级作用域(Block Scope)

    • 块级作用域是指由一对花括号({})包裹的代码块所创建的作用域。例如,if语句、for循环、while循环等都会创建块级作用域。
    • 在块级作用域内声明的变量只能在该代码块内部访问,外部无法访问。

有了作用域的概念 我们就先来讲讲函数中的预编译

函数的预编译是 JavaScript 引擎在执行函数体之前进行的一系列操作,包括创建函数的执行上下文、识别形参和变量声明,并将它们添加到函数的作用域链中。

下面是函数预编译的一般步骤:

  1. 创建执行上下文(AO - Activation Object)

    • 当函数被调用时,JavaScript 引擎会创建一个执行上下文来执行函数体。执行上下文包含了函数的作用域链、变量环境、this 指向等信息。
  2. 形参初始化和变量提升

    • 编译器 寻找形参和变量声明 将形参和变量名作为AO的属性 值为 undefined
  3. 统一形参和实参

    • 将传递给函数的实参赋值给对应的形参。
  4. 函数声明提升

    • 在函数体内查找函数声明(包括函数声明和命名函数表达式)提升到函数作用域的顶部。这使得在函数体内部声明的函数可以在声明之前被调用。 将函数名作为AO的属性名,值赋予函数体

我们来看一个例子

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)

开始编译:

1- 创建AO对象
    2- 形参和变量声明
                 3.统一实参和形参
                    4.函数声明提升
AO
{
    a:undefined->1->function(){}
    b:undefined   
    d:undefined   ->function(){}
}
// var b = function () { }  这只是一个表达式

至此 编译完成 可以开始执行了

初始值为


AO
{
    a:function(){}
    b:undefined   
    d:function(){}
}

我们一步一步来执行

console.log(a) //function(){} 这里直接输出就行了

var a = 123 这句代码中a的值发生了改变 a:function(){}->123

console.log(a) //123

function a() { }这句是函数声明而不是调用 直接下一句

console.log(a) // 123 正常输出

这里对b进行了一个赋值

var b = function () { }// b:undefined->function () { }

console.log(b) // function () { } 相信大家都知道了

var d = a //d:function(){} ->a=123

console.log(d) //123

image.png

全局预编译

  1. 创建全局执行上下文对象

    • 全局执行上下文对象包括全局作用域中的变量和函数。
  2. 找变量声明

    • 在全局作用域中寻找变量声明,并将变量名作为全局执行上下文对象的属性名,值为undefined。
  3. **在全局中找函数声明 **

    • 在全局作用域中寻找函数声明,并将函数名作为全局执行上下文对象的属性名,值为函数体。
global =100
function fn(){
    console.log(global );
    global=200
    console.log(global);
    var global = 300
}
fn ()
var global;
  1. 全局GO对象
 GO{
    
}
  1. 找变量声明
 GO{
    global :undefined 
}
  1. 在全局中找函数声明
GO{
    global :undefined 
    fn:function fn(){}
}

全局预编译到此结束 代码开始执行

  1. global =100 global :undefined ->100
  2. 函数调用带来函数预编译
function fn(){
    console.log(global );
    global=200
    console.log(global);
    var global = 300
}
// AO{            log      log
//     global:undefined ->200 ->300
// }

image.png

调用栈

调用栈(Call Stack)是一个用于存储函数调用的栈结构,它跟踪代码的执行过程,记录了程序在执行过程中的函数调用关系。

结构

调用栈是一个后进先出(LIFO) 的数据结构,它的操作是线性的,只能在栈顶进行操作。每当调用一个函数时,该函数的执行上下文会被压入调用栈的顶部;当函数执行完毕,执行上下文会被弹出栈顶。这样,调用栈始终记录着当前代码执行的上下文。

过程

  1. 入栈(Push) :当调用一个函数时,该函数的执行上下文被创建,并被压入调用栈的顶部。
  2. 执行(Execution) :函数的代码被执行,程序按照函数体内的语句顺序执行,包括对其他函数的调用。
  3. 出栈(Pop) :当函数执行完成,其执行上下文被弹出栈顶,控制权返回给调用该函数的上下文。

示例

让我们通过一个简单的示例来理解调用栈的工作原理:

javascript
function greet(name) {
    console.log("Hello, " + name + "!");
}

function welcome() {
    console.log("Welcome!");
}

function sayHello() {
    greet("Alice");
    welcome();
}

sayHello();

在这个示例中,当调用 sayHello() 函数时,会依次发生以下过程:

  1. sayHello() 函数被调用,其执行上下文被压入调用栈的顶部。
  2. sayHello() 函数内部调用 greet() 函数,greet() 函数的执行上下文被压入调用栈的顶部。
  3. greet() 函数执行完成,其执行上下文被弹出栈顶,控制权返回给 sayHello() 函数。
  4. sayHello() 函数内部调用 welcome() 函数,welcome() 函数的执行上下文被压入调用栈的顶部。
  5. welcome() 函数执行完成,其执行上下文被弹出栈顶,控制权返回给 sayHello() 函数。
  6. sayHello() 函数执行完成,其执行上下文被弹出栈顶,控制权返回给全局执行上下文。

作用

调用栈的主要作用是跟踪函数的调用关系,它确保了函数的执行顺序和上下文的正确管理。通过调用栈,程序可以追踪函数的执行流程,及时发现错误和调试问题。

总结

总的来说,JavaScript 的预编译是代码执行前的重要准备工作,通过预先处理变量和函数的声明,确保了代码的顺利执行,为实现更复杂的交互体验奠定了基础。