JavaScript 执行机制:变量提升和执行上下文详解

168 阅读5分钟

引言

JavaScript 是一种广泛使用的编程语言,尤其在前端开发中占据重要地位。理解 JavaScript 的执行机制对于编写高效、无误的代码至关重要。本文将详细解释 JavaScript 的两个核心概念:变量提升和执行上下文。

JavaScript 是脚本语言

JavaScript 是一种脚本语言,不需要独立的编译阶段。在代码执行之前,JavaScript 引擎会进行一个短暂的编译过程,这个过程主要包括解析代码结构、进行变量提升和函数提升等准备工作。然后,引擎会按照调用栈的顺序执行代码。

作用域

在介绍变量提升之前我们先来简单介绍一下作用域的概念

变量一定属于某个作用域。JavaScript 中主要有三种作用域:全局作用域、函数作用域和块作用域。查找变量时,会从当前作用域开始,逐级向上查找,直到找到为止。

  • 全局作用域

  • 全局作用域是最外层的作用域,存在于整个程序中。在全局作用域中声明的变量和函数可以在程序的任何地方访问

  • 函数作用域

  • 函数作用域是指在函数内部声明的变量和函数,只能在该函数内部访问。函数作用域是 JavaScript 中最常见的作用域。

  • 块级作用域

    • ES6 引入了 let 和 const 关键字,它们允许开发者创建块级作用域。
    • 块级作用域是在代码块(如 if 语句、for 循环等)内部定义的作用域。
    • let 和 const 声明的变量只能在它们被定义的块级作用域内访问。

变量提升(Hoisting)

变量提升是指 JavaScript 引擎在编译阶段将变量和函数的声明提升到其所在作用域的顶部。需要注意的是,只有声明会被提升,而赋值不会被提升。

var 声明的变量提升
console.log(name); // 输出 "undefined"
var name = "wql";
console.log(name); // 输出 "wql"

在这个例子中,尽管 name 的赋值语句在 console.log 之后,但由于变量提升,name 实际上在编译阶段已经被提升到了全局作用域的顶部。因此,第一次 console.log(name) 输出的是 undefined

函数提升

函数提升不仅包括声明的提升,还包括函数体的提升。这意味着你可以在函数声明之前调用该函数。

sayHello(); // 输出 "Hello, wql!"

function sayHello() {
  console.log("Hello, wql!");
}

在这个例子中,尽管 sayHello 函数的定义在调用之后,但由于函数提升,sayHello 函数在编译阶段已经被提升到了顶部,因此可以正常调用。

letconst 不会提升

var 不同,letconst 声明的变量不会被提升。如果在声明之前使用这些变量,会抛出引用错误。

console.log(age); // 抛出 ReferenceError: Cannot access 'age' before initialization
let age = 25;

在这个例子中,尝试在 let age = 25; 之前访问 age 会导致引用错误,因为 let 声明的变量不会被提升到作用域的顶部。

执行上下文(Execution Context)

执行上下文是 JavaScript 在执行阶段的上下文,为即将到来的代码执行做准备。每个函数调用都会创建一个新的执行上下文。执行上下文包括变量环境、作用域链和 this 值等。

  • 全局执行上下文
  • 全局执行上下文是最外层的执行上下文,对应于全局作用域。全局执行上下文在程序启动时创建,并且在整个程序运行期间一直存在。
  • 函数执行上下文:
// 全局执行上下文 
console.log(this); // 输出全局对象,例如在浏览器中是 window 对象
function foo() { 
// 函数执行上下文 
console.log(this); // 输出调用 foo 函数的对象
var x = 10; console.log(x); // 输出 10
} foo(); // 调用 foo 函数,创建函数执行上下文
console.log(x); // 抛出 ReferenceError: x is not defined,因为 x 是函数 foo 内部的局部变量

在这个案例中:

  • 当 JavaScript 引擎开始执行脚本时,首先会创建一个全局执行上下文,并输出全局对象。
  • 然后定义了一个函数 foo,当调用 foo 函数时,会创建一个新的函数执行上下文,并输出调用 foo 函数的对象。
  • 在函数 foo 内部,定义了一个局部变量 x,并输出其值。
  • 当函数 foo 执行完毕后,其执行上下文会被销毁,局部变量 x 也会随之消失。
  • 最后,尝试在全局执行上下文中访问局部变量 x,会抛出 ReferenceError,因为 x 只在函数 foo 的执行上下文中存在。

调用栈(Call Stack)

JavaScript 的执行机制基于调用栈。调用栈是一个后进先出(LIFO)的数据结构,用于管理函数的调用顺序。每当调用一个函数时,都会创建一个新的执行上下文并将其压入调用栈的顶部。函数执行完毕后,该执行上下文会被从栈顶弹出。

function bar() {
  console.log("Inside bar");
}

function foo() {
  console.log("Inside foo");
  bar();
}

console.log("Start");
foo();
console.log("End");

在这个例子中,调用栈的变化如下:

  1. 全局执行上下文被创建并压入栈顶。
  2. foo 函数被调用,创建一个新的执行上下文并压入栈顶。
  3. bar 函数被调用,创建一个新的执行上下文并压入栈顶。
  4. bar 函数执行完毕,其执行上下文从栈顶弹出。
  5. foo 函数执行完毕,其执行上下文从栈顶弹出。
  6. 全局执行上下文继续执行,直到程序结束。

总结

JavaScript 的变量提升和执行上下文机制是理解其运行原理的关键。通过了解这些概念,可以更好地编写和调试 JavaScript 代码。变量提升使得 var 声明的变量和函数在编译阶段被提升到作用域的顶部,而 letconst 声明的变量则不会被提升。执行上下文和调用栈管理着函数的调用顺序和变量的作用域。希望本文能帮助读者深入理解 JavaScript 的这些特性,提高编程水平。 。