深入理解 JavaScript 执行机制:从编译到执行的全过程

187 阅读5分钟

前言

在日常开发中,我们经常会遇到一些令人困惑的 JavaScript 行为,比如变量提升、暂时性死区、函数执行顺序等。这些现象背后其实是 JavaScript 引擎的执行机制在起作用。今天,我们就来深入探讨 JS 的底层执行机制,帮助你彻底理解代码是如何运行的。

一、JavaScript 的两阶段执行过程

与传统的编译型语言不同,JavaScript 采用了一种独特的执行方式:先编译后执行,而且是边编译边执行。我们可以先借助一段代码来简单理解一下

showName()
console.log(myName)

var myName = 'zhangsan'

function showName () {
  console.log('函数showName被执行')
}

但是这段代码在浏览器引擎中其实是这样的

var myName
function showName(){
console.log('函数showName被执行'
}
showName()
console.log(myName)
myName='zhangsan'

这就是JS中独特的变量提升,但是变量提升是如何产生的呢,这就要从JS底层执行机制谈谈了

1.1 两个关键阶段

一段代码一般会经过两个阶段,即编译阶段和执行阶段

image.png

注意: 这个过程并不是单线程的,当执行阶段遇到函数调用时,会继续先编译后执行

v8引擎一读取到这段代码就会创建执行上下文对象并放入调用栈中,然后再执行。 一个执行上下文由三部分组成:变量环境 词法环境还有可执行代码

image.png

变量环境: 一般为声明的变量或函数

词法环境: 一般为let/const声明的变量、常量和函数

调用栈: v8 引擎用来管理函数之间的调用关系的一种结构

编译阶段:发生在代码执行前的瞬间

  1. 创建执行上下文
  2. 找形参和变量声明,将形参和变量声明作为key。值为undefined
  3. 统一形参和实参的值(全局没有该步骤)
  4. 找函数声明,函数名作为key,值为函数体

二、代码执行示例

我们从一段经典的代码来详细谈一谈

var a = 1
function fn (a) {
  console.log(a)
  var a = 2
  function a () {}
  var b = a
  console.log(a)
}
fn(3)

首先,创建全局执行上下文对象,检索到var a=1,存在变量声明,所以a=undefined,由于是全局上下文对象,不存在步骤3,所以继续检索函数声明,发现functionfn(a){...},所以fn=function。此时全局上下文编译阶段完成,执行代码

image.png

将1的值赋予a,将实参3传入fn(),此时fn继续执行,将会编译内部代码,创建fn函数上下文

image.png

继续编译,首先将找到形参a,a=undefined,继续找到var a=2,再将a=undefined覆盖第一次的a,继续找到var b=a,所以b=undefined,随后进行步骤3,统一形参和实参的值,将3的值赋予a,再进行第4步,找到一个函数声明,此时a=function(){},fn执行上下文编译阶段至此结束,进入执行阶段,将2的值赋予aa=2,将a的值赋予bb=a=2,随后打印a的值,此时为2

image.png

再按照后人先出的原则,先将执行完的fn函数上下文弹出销毁,再将全局上下文弹出销毁

三、var、let、const 的区别真相

3.1 变量提升的差异

常见的误解let和 const没有变量提升

真相:三者都有变量提升,但处理方式不同:

  • var:提升到变量环境,初始值为 undefined,可重复声明
  • let/const:提升到词法环境,但处于"暂时性死区",在声明前访问会报错

3.2 暂时性死区(Temporal Dead Zone)

console.log(a)  // undefined,变量提升
var a = 1

console.log(b)  // ReferenceError: Cannot access 'b' before initialization
let b = 2

3.3 避免函数声明提升的技巧

如果想要函数不提升,可以使用函数表达式:

// 函数声明(会提升)
function normalFn() {
  console.log('我会被提升');
}

// 函数表达式(不会提升)
let arrowFn = () => {
  console.log('我不会被提升');
}

const expressFn = function() {
  console.log('我也不会被提升');
}

四、值传递与引用传递

理解执行机制还需要明白值的传递方式:

4.1 基本数据类型(值传递)

let str = 'hello'
let str2 = str  // 值的拷贝
str2 = '你好'
console.log(str, str2)  // 'hello' '你好'

4.2 复杂数据类型(引用传递)

let obj = {
  name: 'zhangsan',
  age: 18
}

let obj2 = obj  // 引用的拷贝
obj2.age++

console.log(obj.age, obj2.age)  // 19 19,指向同一个对象

这是因为简单数据类型的拷贝,是将值进行传递,而复杂数据类型的拷贝,是将地址进行传递

五、实战应用与性能优化

5.1 避免全局变量污染

理解执行上下文后,我们应该减少全局变量的使用:

// 不好的做法
var globalData = '我在全局';

// 好的做法:使用模块模式
(function() {
  var localData = '我在函数作用域内';
  // ...其他代码
})();

5.2 合理使用块级作用域

// 使用 let 避免变量泄露
for (let i = 0; i < 5; i++) {
  // i 只在循环内有效
}

// 使用 const 声明常量
const PI = 3.14159;

六、总结

JavaScript 的执行机制可以概括为以下几个要点:

  1. 先编译后执行:代码在执行前会经历编译阶段
  2. 调用栈管理:通过栈结构管理执行上下文的入栈和出栈
  3. 变量提升差异varletconst提升方式不同
  4. 执行上下文创建:包含变量环境和词法环境

理解这些底层机制,不仅能帮助我们避免常见的坑,还能写出更高效、更健壮的代码。下次当你遇到奇怪的 JavaScript 行为时,不妨从执行机制的角度来思考,问题往往就能迎刃而解。