FORMA

执行模型与作用域

执行上下文与词法作用域决定变量在何处可访问、闭包如何形成,以及 var / let / const 的差异。语言概览见 JavaScript 基础

执行上下文

执行上下文是 JavaScript 代码执行时的环境。它包含了代码执行所需的所有信息:变量函数声明this 指向等。

类型

  1. 全局执行上下文:
    • 这是默认的、最基础的上下文。
    • 任何不在函数内部的代码都在全局上下文中。
    • 在浏览器中,全局对象的 this 绑定为 window(严格模式模块中顶层 thisundefined);在 Node.js 中为 global;统一可用 globalThis
  2. 函数执行上下文:
    • 每当一个函数被调用时,都会为该函数创建一个新的执行上下文。
    • 每个函数都有自己的上下文栈。
  3. eval 执行上下文
    • eval() 内执行的代码拥有独立上下文;生产代码应避免 eval(见 安全)。

执行上下文的生命周期(创建阶段与执行阶段)

JavaScript 引擎在执行代码时,主要分为两个阶段:

  1. 创建阶段:
    • 创建变量对象:初始化函数的参数、变量声明、函数声明。
    • 建立作用域链:确定对变量的访问权限。
    • 确定 this 指向。
    • 注:此时变量会被赋值为 undefined(变量提升)
  2. 执行阶段:
    • 逐行执行代码,为变量赋值,引用函数。

执行上下文栈

引擎用**调用栈(call stack)**管理执行上下文:脚本开始全局上下文入栈;函数调用则入栈新上下文;函数返回后出栈。

作用域

作用域是指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。作用域是在代码编写时(定义时)确定的,而不是运行时确定的,这被称为“词法作用域”。

类型

  1. 全局作用域:
    • 在代码任何地方都能访问到的变量。
    • 拥有全局作用域的变量是全局变量。
  2. 函数作用域:
    • 在函数内部声明的变量,只能在函数内部访问。
    • 外部无法访问函数内部的变量。
  3. 块级作用域:
    • ES6 的 let / const 使 {} 块可形成独立作用域。
    • var 不具备块级作用域(它在 {} 外依然可见,除非在函数内)。

作用域链

当访问一个变量时,JavaScript 引擎会先在当前作用域查找,如果没有找到,就向上一级作用域查找,直到全局作用域。这种层层向上的链式查找机制就是作用域链。

闭包

闭包是 JavaScript 中最强大的特性之一,也是面试常考点。

定义

简单来说,闭包就是函数能够记住并访问它的词法作用域,即使该函数是在其词法作用域之外执行。

原理

当一个函数内部定义了另一个函数,并把这个内部函数返回或传递到外部时,内部函数会持有外部函数的变量对象的引用。即使外部函数已经执行完毕退出了执行栈,其变量对象依然不会被销毁,因为内部函数还在引用它。

代码示例

js
function outer() {
    let a = 10;
    return function inner() {
        console.log(a);
    };
}

const closureFunc = outer(); // outer 执行完毕,通常认为 a 会被销毁
closureFunc(); // 输出 10,依然能访问 a,这就是闭包

应用场景

  • 数据私有化(模拟私有方法)。
  • 柯里化。
  • 保持状态(如防抖、节流函数)。

变量提升与暂时性死区

这是理解 var、let 和 const 行为差异的关键。

变量提升

在 ES6 之前,使用 var 声明的变量,无论在函数的何处声明,都会被“提升”到函数的顶部。但是,只有声明被提升,赋值操作留在原地。

  • var 的表现:
js
console.log(myVar); // 输出 undefined,不会报错
var myVar = 10;
// 等价于:
// var myVar;
// console.log(myVar);
// myVar = 10;
  • 函数声明也会被提升:
js
sayHello(); // 可以正常调用,输出 "Hello"
function sayHello() {
    console.log("Hello");
}

暂时性死区

ES6 引入了 letconst,它们也存在变量提升,但是行为不同:它们被放入了“暂时性死区”(TDZ)。

  • TDZ 的含义:从代码块(作用域)的开始,直到变量声明语句运行完成之前,该变量都处于 TDZ。在此期间访问该变量会导致 ReferenceError。
  • 目的:为了减少运行时错误,强制开发者先声明再使用。
js
console.log(myLet); // 报错:ReferenceError: Cannot access 'myLet' before initialization
let myLet = 20;

{
    // TDZ 开始
    tmp = "abc"; // ReferenceError
    console.log(tmp); // ReferenceError

    let tmp; // TDZ 结束,tmp 被初始化为 undefined
    console.log(tmp); // undefined

    tmp = 123;
    console.log(tmp); // 123
}

总结对比

特性varlet / const
作用域函数作用域块级作用域
变量提升是,提升并初始化为 undefined是,提升但进入暂时性死区 (TDZ)
重复声明允许不允许 (报错)
全局挂载会挂载到 window (浏览器)不会挂载到 window

综合示例

为了把这些概念串起来,看下面这个例子:

js
var globalVar = "I am global";

function createCounter() {
    let count = 0; // 局部变量,createCounter 的作用域

    // 返回一个闭包
    return function () {
        count++; // 访问外部作用域的 count
        console.log(count);
        console.log(globalVar); // 通过作用域链访问全局变量
    };
}

const counter = createCounter();
counter(); // 输出 1, "I am global"
counter(); // 输出 2, "I am global"

在这个例子中:

  1. 执行上下文:代码开始运行创建全局上下文;调用 createCounter 创建函数上下文。
  2. 作用域:count 拥有函数作用域,globalVar 拥有全局作用域。
  3. 闭包:返回的匿名函数形成了闭包,使得 createCounter 执行完毕后,count 依然保存在内存中。
  4. 变量提升:let 声明的 count 在声明前处于 TDZ;若改用 var 则提升为 undefined

参考文献

以下链接在编写时均可正常访问:

资料说明
MDN:闭包闭包与词法环境
MDN:let块级作用域与 TDZ
MDN:var函数作用域与提升
MDN:thisthis 与执行上下文
ECMA-262:执行上下文规范术语(英文)

Series

js

14 / 14

安全