JavaScript代码运行机制:执行栈、执行上下文、作用域和闭包

42 阅读5分钟

执行栈、执行上下文、作用域和闭包

执行栈(call-stack)

执行栈,也就是在其它编程语言中所说的“调用栈”,是一种基于后进先出(LIFO,Last In, First Out)原则的数据结构,用于管理代码在运行时创建的所有执行上下文。

什么是栈结构?

栈结构(Stack)是一种常见的数据结构,它遵循后进先出(LIFO,Last In, First Out)的原则。这意味着在栈中,最后一个被放入的数据项(称为入栈)是第一个被取出的数据项(称为出栈)。

栈结构如下图所示:

image-20240910215144465

下面使用JavaScript语言实现一个简单的栈:

class Stack {
  constructor() {
    this.stack = [];
  }
  // 入栈操作
  push(element) {
    this.stack.push(element);
  }
    pop() {
    if (this.isEmpty()) {
      return "Stack is empty";
    }
    return this.stack.pop();
  }
}

执行栈的实际运用

在浏览器开发工具的 Sources 面板中,Call Stack 代表了执行栈。它显示了当前代码的执行位置和执行顺序。通过这个工具,开发者可以调试和跟踪代码的执行流程。

image-20240910223139591

执行上下文(Execution Context)

执行上下文(Execution Context)是 JavaScript 中一个核心概念,描述了代码在运行时的环境。每当 JavaScript 代码执行时,它都会在某个执行上下文中运行,该上下文决定了变量、函数及 this 关键字的访问权限。

执行上下文通常对应于一个代码作用域(scope),管理变量的可见性和生命周期。

根据代码执行的环境,执行上下文可以分为以下几种类型:

  • 全局执行上下文:这是最外层的执行上下文,在浏览器环境中,它绑定到全局对象 window 上。在 Node.js 环境中,绑定到 global 对象上。
  • 函数执行上下文:每当一个函数被调用时,会创建一个新的执行上下文。
  • Eval 执行上下文:当使用 eval 函数执行代码时,会创建一个新的执行上下文。

执行上下文的组成部分

每个执行上下文包含以下几个关键部分:

  1. 变量对象(Variable Object, VO) :存储函数的参数、内部声明的变量和函数声明。
  2. 作用域链(Scope Chain) :用于解析变量。它由当前执行上下文的变量对象和其父级执行上下文的变量对象组成,直到全局执行上下文。
  3. this 关键字this 的值在执行上下文创建时确定,并根据调用的方式而不同。

在浏览器开发工具中的 Sources 面板中,Scope代表执行上下文的作用域,可以看到当前执行上下文的作用域及其中的变量:

image-20240910222855580

作用域(scope)

作用域是一个变量和函数的可访问范围,它通过作用域链来决定在代码中如何查找和访问这些变量和函数。作用域链是执行上下文的一部分,用于解决变量的查找问题。

JavaScript的作用域链规则是:优先在当前执行上下文的变量对象中查找变量。如果在当前作用域中未找到,则继续在父级执行上下文的变量对象中查找,直到找到变量或到达全局作用域。

闭包(Closure)

闭包是指一个函数和它创建时所处的词法环境(也就是作用域)的组合。闭包使得函数可以“记住”并访问它定义时的变量,即使这个函数在定义的外部环境中被调用。换句话说,闭包使得内部函数能够访问外部函数的变量,即使外部函数已经执行完毕。

闭包的形成

闭包通常发生在以下情况下:

  • 函数返回另一个函数:当一个函数在其内部定义并返回另一个函数时,返回的内部函数形成闭包。这个内部函数可以访问并操作外部函数的变量。
  • 函数作为返回值:当一个函数在其内部定义并返回另一个函数时,返回的内部函数形成闭包。这个内部函数可以访问并操作外部函数的变量。

闭包的实现机制

闭包的实现依赖于 JavaScript 的执行上下文和作用域链。具体来说:

  • 执行上下文:当函数被调用时,JavaScript 引擎会为这个函数创建一个执行上下文。这个上下文包含了函数的局部变量、参数和外部作用域的引用。
  • 作用域链:每个函数都可以访问其定义时所在的作用域,通过作用域链,内部函数可以访问外部函数的变量。闭包正是通过作用域链来实现的。

当一个函数返回了一个内部函数时,即使外部函数已经执行完毕,内部函数依然保留了对外部函数作用域的引用。这种引用使得外部函数的变量得以在内存中继续存在。

闭包的优缺点

优点:

  • 数据封装:闭包可以将数据封装在函数内部,只暴露必要的接口,从而实现数据的私有化。
  • 保持状态:闭包可以用来创建带有状态的函数,比如计数器、缓存等。

缺点:

  • 内存泄漏:由于闭包持有对外部函数作用域的引用,当闭包被长时间引用时,可能导致外部函数的变量无法被垃圾回收,进而造成内存泄漏。