人人都会AI编程

词法作用域(静态作用域)本质

更新时间:2026-07-11

词法作用域(Lexical Scoping),也称静态作用域,是 JavaScript 作用域模型的核心规则。理解它,是理解闭包、变量查找、模块隔离等一切作用域相关机制的前提。

定义:作用域在代码编写时就已确定

词法作用域的含义是:函数的作用域在函数定义时就已经决定了,而不是在函数调用时决定。 换句话说,一个函数内部能访问哪些变量,取决于这个函数写在源码的哪个位置,即它所处的词法环境,而与它在何处被调用无关。

const value = 'global';

function outer() {
  const value = 'outer';

  function inner() {
    console.log(value);
  }

  return inner;
}

const fn = outer();
fn(); // 输出 "outer",而不是 "global"

inner 函数定义在 outer 函数内部,因此它的词法作用域包含 outer 函数的局部作用域以及全局作用域。即使 inner 被返回到外部,在全局环境中调用,它依然访问的是 outer 内部的 value,而不是全局的 value。这就是词法作用域的体现——作用域链由函数定义的位置决定,不会被调用位置改变。

作用域链就是基于词法作用域构建的

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的顶端是当前函数的活动对象,然后是包含该函数的外层作用域,再然后是再外层作用域,一直到全局对象。这条链就是完全按照代码的嵌套结构(词法结构)建立起来的。

const a = 1;

function foo() {
  const b = 2;

  function bar() {
    const c = 3;
    console.log(a, b, c); // 1, 2, 3
  }

  bar();
}

foo();

bar 函数的作用域链:[bar 作用域] → [foo 作用域] → [全局作用域]。这个结构由代码的嵌套书写顺序决定,与调用顺序无关。

与动态作用域的对比

与词法作用域相对的是动态作用域,动态作用域中函数的作用域取决于调用时的环境(调用栈),而不是定义时的环境。某些语言(如 Bash 脚本、Emacs Lisp 的早期版本)采用动态作用域。

以动态作用域为例,同样代码可能会输出 "global",因为 inner 被调用时的调用环境如果是全局,就会去全局作用域查找 value。JavaScript 从不使用动态作用域,它的 this 机制有时会被误解为动态作用域,但 this 是函数调用时的绑定,而不是变量的作用域规则。变量查找永远遵循词法作用域。

为什么强调“静态”二字

静态作用域意味着我们可以通过阅读代码就能准确知道每个变量来源于哪个作用域,不需要考虑运行时调用堆栈。这给代码的可读性、可预测性、可调试性带来了巨大好处。因为作用域关系是确定的,引擎也能够在编译阶段就优化变量查找,生成更高效的代码。

实践中的两个重要结论

  1. ES6 的 letconst 强化了词法作用域,并引入了块级作用域。
   if (true) {
     let x = 1;
   }
   console.log(x); // ReferenceError
   

块级作用域同样遵循词法规则:变量的作用域由 {} 所在的位置决定,在块外部无法访问。

  1. 闭包是词法作用域的直接产物。 当内部函数引用了外层作用域的变量,并且内部函数被传递到外层作用域之外执行时,它仍然保持着对原始作用域的引用,这种机制就叫闭包。没有词法作用域,闭包就无法存在。

掌握词法作用域后,你就会明白:在 JavaScript 里,变量去哪找,代码写在哪,比在哪调用更重要。 这是语言作用域设计的基石,也是后续理解闭包、模块化的关键。