JavaScript 执行上下文与作用域链

小飞兽 JavaScript 377 次阅读 2026-05-22

JavaScript 执行上下文与作用域链

一、引言

JavaScript 是一门基于执行上下文(Execution Context)运行的编程语言。理解执行上下文与作用域链的机制,是深入掌握 JavaScript 语言特性的必经之路。无论是闭包(Closure)、变量提升(Hoisting),还是 this 的行为,都与执行上下文和作用域链息息相关。本文将系统梳理这两个核心概念,帮助读者构建完整的 JavaScript 运行时模型认知。

二、执行上下文基础

2.1 什么是执行上下文

执行上下文是 JavaScript 引擎执行代码时的运行环境。当 JavaScript 代码在浏览器或 Node.js 中运行时,每一次函数调用都会创建一个新的执行上下文。所有执行上下文共同构成一个调用栈(Call Stack),栈底的全局上下文称为"全局执行上下文"(Global Execution Context),每个函数调用则产生对应的"函数执行上下文"(Function Execution Context)。

2.2 执行上下文的类型

JavaScript 中存在三种主要的执行上下文类型:

    • 全局上下文:代码首次运行时的默认环境,对应一个全局对象(在浏览器中是 window,在 Node.js 中是 global)。全局上下文只有一个,贯穿整个程序生命周期。
    • 函数上下文:每当调用一个函数时,引擎会为该函数创建一个全新的执行上下文。函数上下文可以有无数个,彼此相互独立。
    • Eval 上下文:由 eval() 函数运行产生的上下文,现已很少使用且不推荐。

    2.3 执行上下文的创建阶段

    执行上下文的创建分为两个阶段:创建阶段(Creation Phase)和执行阶段(Execution Phase)。

    在创建阶段,引擎完成以下三件事:

    1. 确定 this 的值(即 This Binding)
  • 创建词法环境(LexicalEnvironment)组件
  • 创建变量环境(VariableEnvironment)组件

词法环境用于解析标识符引用,变量环境用于存储 var 声明的变量。这一阶段也发生了"变量提升"(Hoisting)现象:var 声明的变量被初始化为 undefined,函数声明被整体提升。

三、作用域与作用域链

3.1 什么是作用域

作用域(Scope)是指变量和函数的可访问范围。JavaScript 使用词法作用域(Lexical Scope),这意味着作用域在代码书写阶段(词法分析阶段)就已经确定,与函数的调用位置无关。作用域决定了在何处可以访问哪些变量,是理解闭包和变量遮蔽(Shadowing)的基础。

3.2 JavaScript 的作用域类型

JavaScript 包含以下几种作用域:

    • 全局作用域:最外层声明的变量,属于全局作用域,在代码任意位置都可以访问。
    • 函数作用域:在函数内部声明的变量,仅在该函数及其内部嵌套函数中可见。
    • 块级作用域:由 letconst 声明的变量,仅在 {} 代码块内有效。这是 ES6 引入的新特性。
    • 模块作用域:ES6 模块中,每个文件拥有独立的作用域,模块内的顶层变量不会泄露到全局。

    3.3 作用域链的本质

    作用域链(Scope Chain)是 JavaScript 引擎解析变量标识符时的查找链条。当在某个作用域内访问一个变量时,如果当前作用域内找不到该变量,引擎会沿着作用域链向上一层作用域查找,直到全局作用域为止。如果全局作用域中也没有找到,则抛出 ReferenceError

    作用域链的构建规则如下:

    • 每个执行上下文关联一个词法环境
    • 词法环境包含一个对外部环境的引用(outer
    • 这个引用链条即为作用域链

    四、代码示例

    4.1 作用域链的查找过程

    var globalVar = '全局变量';
    

    function outer() {
    var outerVar = '外层变量';

    function inner() {
    var innerVar = '内层变量';
    console.log(innerVar); // 直接在inner作用域找到
    console.log(outerVar); // inner没有,沿着作用域链向上找
    console.log(globalVar); // outer也没有,继续向上找
    }

    function inner2() {
    console.log(innerVar); // ReferenceError: innerVar is not defined
    // inner2 与 inner 是兄弟关系,不能跨函数访问
    }

    inner(); // 正常输出:内层变量、外层变量、全局变量
    inner2(); // 抛出 ReferenceError
    }

    outer();


    运行结果:


    内层变量
    外层变量
    全局变量
    ReferenceError: innerVar is not defined

    4.2 词法作用域与闭包

    function createCounter() {
        var count = 0;  // 被内部函数引用,形成闭包
    

    return {
    increment: function() {
    count++;
    console.log('当前计数:', count);
    },
    decrement: function() {
    count--;
    console.log('当前计数:', count);
    },
    getCount: function() {
    return count;
    }
    };
    }

    var counter = createCounter();
    counter.increment(); // 当前计数: 1
    counter.increment(); // 当前计数: 2
    counter.decrement(); // 当前计数: 1
    console.log(counter.getCount()); // 1


    运行结果:


    当前计数: 1
    当前计数: 2
    当前计数: 1
    1

    4.3 块级作用域与 var 的对比

    // var 声明:函数作用域,存在变量提升
    console.log('var前:', x); // undefined(变量提升,值为undefined)
    var x = 10;
    

    // let 声明:块级作用域,不存在变量提升
    try {
    console.log('let前:', y);
    } catch(e) {
    console.log('let前报错:', e.message); // y is not defined
    }
    let y = 20;

    // 块级作用域示例
    {
    let blockVar = '块内变量';
    const blockConst = '块内常量';
    console.log(blockVar); // 块内变量
    }
    // console.log(blockVar); // ReferenceError: blockVar is not defined


    运行结果:


    var前: undefined
    let前报错: y is not defined
    块内变量

    五、常见问题

    Q1: var、let、const 的作用域有何区别?

    var 是函数作用域,在函数内任何位置声明的 var 变量在整个函数内可见;letconst 是块级作用域,仅在 {} 包裹的代码块内有效。此外,var 存在变量提升(值为 undefined),而 letconst 存在"暂时性死区"(Temporal Dead Zone),在声明前访问会抛出 ReferenceError

    Q2: 作用域链的尽头是什么?

    作用域链的最顶层是全局执行上下文,全局上下文的外部引用(outer)为 null,标志着作用域链的终点。如果在全局作用域中仍找不到某个变量(非严格模式下),JavaScript 会在全局对象上尝试创建该属性。

    Q3: 循环中的闭包问题如何解决?

    // 错误写法:所有闭包引用同一个 i
    for (var i = 0; i < 3; i++) {
        setTimeout(function() {
            console.log(i); // 输出 3 3 3
        }, 100);
    }
    

    // 解决方式1:使用 let(块级作用域)
    for (let i = 0; i < 3; i++) {
    setTimeout(function() {
    console.log(i); // 输出 0 1 2
    }, 100);
    }

    // 解决方式2:使用 IIFE 创建独立作用域
    for (var i = 0; i < 3; i++) {
    (function(j) {
    setTimeout(function() {
    console.log(j); // 输出 0 1 2
    }, 100);
    })(i);
    }

    Q4: 作用域链和原型链有什么区别?

    作用域链用于解析变量(标识符)访问,解决"某处能访问哪些变量"的问题;原型链用于对象属性(Property)的查找,解决"某对象有哪些属性和方法"的问题。两者是完全不同的机制:前者处理变量作用域,后者处理对象继承。

    六、延伸阅读

    • 《JavaScript 高级程序设计》(第4版) - 第 10 章"函数"与第 11 章"Promise 与异步编程"
    • 《你不知道的 JavaScript(上卷)》 - 第 1 章"作用域"与第 2 章"词法作用域"
  • ECMAScript 规范 - Lexical Environmenttc39.es/ecma262/#sec-lexical-environments
  • JavaScript 引擎执行原理系列文章:深入理解 V8 引擎如何创建执行上下文与词法环境