JS执行机制解析

38 阅读5分钟

JavaScript 执行机制解析

JavaScript 作为一种广泛使用的脚本语言,其执行机制与传统编译型语言有显著差异。本文将结合具体代码示例,深入探讨 V8 引擎下 JavaScript 的执行原理,包括编译与执行阶段、变量提升、函数提升以及不同数据类型的存储方式等核心概念。

一、JS 执行的两个核心阶段

V8 引擎执行 JavaScript 代码分为两个主要阶段:编译阶段执行阶段,这两个阶段交替进行,形成了 JS 独特的执行模式。

  • 编译阶段:在代码执行前的一瞬间完成,主要负责语法错误检测、变量和函数提升等准备工作
  • 执行阶段:按照编译后的结果执行代码,处理具体的逻辑运算

这种 "边编译边执行" 的特性,使得 JavaScript 既保持了脚本语言的灵活性,又能通过预编译优化执行效率。

二、执行上下文与调用栈

V8 引擎通过执行上下文调用栈来管理代码执行:

  • 执行上下文是一个包含代码执行所需所有信息的对象
  • 调用栈则负责管理执行上下文的创建和销毁

全局代码执行时,首先会创建全局执行上下文并压入调用栈。当执行函数时,会创建新的函数执行上下文并压入栈顶,函数执行完毕后其执行上下文会被弹出栈并销毁。

三、变量提升与函数提升

变量提升和函数提升是编译阶段的重要工作,通过以下代码可以清晰理解:

1. 变量提升示例

// 1.js
showName();
console.log(showName);
console.log(hero);

var myName = '张三';
let hero = '钢铁侠';
function showName(){
    console.log(myName);
    console.log('函数showName被执行');
}

执行结果分析:

  • showName() 可以正常执行,因为函数声明被提升
  • console.log(showName) 会输出函数体,证明函数被提升
  • console.log(hero) 会报错,因为 let 声明的变量存在暂时性死区
  • showName 函数中打印 myName 会输出 undefined,因为变量提升后尚未赋值

V8 引擎对这段代码的编译处理类似:

// 编译阶段的处理(模拟)
function showName(){
    console.log(myName);
    console.log('函数showName被执行');
}
var myName; // 初始化为undefined

// 执行阶段
showName();
console.log(showName);
console.log(hero); // 报错,let声明的变量未到初始化阶段
myName = '张三';
let hero = '钢铁侠'; // let变量在执行到此处才初始化

2. 函数提升优先级

函数声明的提升优先级高于变量提升:

// 2.js
showName();
console.log(myName);
myName = '张三';

// 编译阶段处理(模拟)
function showName(){
    console.log('函数showName被执行');
}
var myName; // undefined

执行结果:

  • 函数正常执行并输出 "函数 showName 被执行"
  • 打印 myName 输出 undefined(已提升但未赋值)

3. 函数表达式不提升

与函数声明不同,函数表达式不会被提升:

// 6.js
func(); // 报错:func is not a function
let func = () => {
    console.log('函数表达式不会提升');
}

执行时会报错,因为函数表达式使用 let 声明,既不会像函数声明那样提升,也处于暂时性死区中无法访问。

四、var、let 和 const 的区别

1. 重复声明特性

// 4.js
console.log(a); // undefined
var a = 1;
var a = 2; // 重复声明,不报错,会被忽略
console.log(a); // 2

let b = 3;
// let b = 4; // 重复声明,会报错
console.log(b); // 3
  • var 允许重复声明,后面的声明会覆盖前面的
  • let/const 不允许重复声明,会直接报错
  • 即使在严格模式下,var 仍然允许重复声明

2. 暂时性死区

let 和 const 声明的变量存在暂时性死区,在声明之前访问会报错,而 var 声明的变量会提升为 undefined

五、函数执行上下文

函数执行时会创建独立的执行上下文,包含参数处理、变量声明等:

// 3.js
var a = 1;
function fn(a){
    console.log(a); // 输出函数a
    var a = 2;
    // function a(){}
    var b = a;
    console.log(a); // 2
}
fn(3);
console.log(a); // 1(全局变量不受影响)

编译阶段对函数 fn 的处理:

  1. 创建函数执行上下文
  2. 处理参数 a
  3. 发现函数声明 function a(){} 并提升
  4. 处理变量声明 var a 和 var b

执行过程中,函数参数、函数声明和变量声明的优先级为:函数声明 > 参数 > 变量声明。

六、数据类型的存储方式

JavaScript 中不同数据类型的存储方式不同:

// 7.js
let str = 'hello'; // 简单数据类型
let str2 = str;    // 值的拷贝
str2 = '你好';
console.log(str, str2); // hello 你好

let obj = {        // 复杂数据类型
    name: '张三',
    age: 18
}
let obj2 = obj;    // 引用式拷贝(复制地址)
obj2.age++;
console.log(obj2, obj); // 两者的age都是19

存储机制:

  • 简单数据类型(字符串、数字等)存储在栈内存中,赋值时进行值拷贝
  • 复杂数据类型(对象、数组等)存储在堆内存中,变量仅保存引用地址,赋值时复制地址

总结

JavaScript 执行机制的核心要点:

  1. 代码执行分为编译和执行两个阶段,交替进行
  2. 编译阶段会进行变量提升和函数提升,为执行做准备
  3. 调用栈管理执行上下文的创建和销毁
  4. var 与 let/const 在提升行为、重复声明等方面有显著区别
  5. 函数声明提升优先级高于变量提升,函数表达式不提升
  6. 不同数据类型采用不同的存储方式,影响赋值行为

理解这些机制有助于我们编写更可预测的代码,避免因执行顺序与编写顺序不同而产生的 bugs。