JavaScript 作用域指南

JavaScript 作用域链如何工作

通过可运行的小型 JavaScript 执行轨迹理解词法作用域、标识符查找、变量遮蔽与闭包。

“作用域链”描述的是:当前作用域没有对应绑定时,JavaScript 如何继续寻找一个标识符。关键在于“词法”二字:这条链由代码写在什么位置决定。

这篇文章要区分一个常见误解:JavaScript 不会为了找变量而搜索调用栈。它会沿当前代码关联的外部词法环境继续查找。

一句话回答

  • 一个绑定属于它被声明所在的词法作用域。
  • 查找从最近环境开始,持续向外,直到找到绑定或外部链结束。
  • 内层同名声明会让内层代码优先看到它,从而遮蔽外层声明。

1. 作用域由源码结构确定

函数会关联到它被创建时所在的词法环境。因此,即使嵌套函数从别处被调用,它仍能读取外层函数中的绑定。

调用栈当然决定代码按什么顺序运行,但它不决定函数体能够看到哪些外部绑定。

2. 查找从最近的绑定开始

inner 读取 label。它自己的环境里没有 label,因此查找继续到 outer 的环境,并在那里找到 label。这条“最近优先”规则让局部变量具有可预测性。

inner 没有声明 label,因此它在 outer 中解析

function outer() {
  const label = "outer";

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

  inner();
}

outer();

3. 变量遮蔽只改变内层看到的结果

当 inner 再声明一个 label 时,这个绑定会先被找到。外层绑定仍然存在;只是 inner 中未限定的 label 引用不再选择它。

遮蔽不是修改。给 inner 的 label 赋值只会改变内层绑定,除非代码通过其他方式明确访问某个对象或外层绑定。

内层 label 遮蔽外层 label

const label = "global";

function outer() {
  const label = "outer";

  function inner() {
    const label = "inner";
    console.log(label);
  }

  inner();
}

outer();

4. 闭包保留词法连接

闭包不是把外层函数里所有值都复制一份。它描述的是:外层调用返回后,函数仍能访问其词法环境中绑定的情况。

在这个例子中,increment 仍能访问 count。每次调用都会通过函数创建位置关联的环境解析 count,然后更新那个绑定。

返回后的函数仍会在 makeCounter 的环境中解析 count

function makeCounter() {
  let count = 0;

  return function increment() {
    count += 1;
    return count;
  };
}

const next = makeCounter();
console.log(next());

5. 一种可靠的变量排错方式

当一个变量引用让你意外时,先写下当前所在的函数或块。接着问:这个名字在这里声明了吗?如果没有,就向外移动一层词法作用域并重复。到达全局环境仍找不到时,引用就会失败。

作用域链可视化器会把这个查找过程高亮出来。选择一个嵌套范例,先预测查找路径,再用高亮结果检查你的推理。

亲自试一试追踪一次作用域查找运行嵌套函数,观察 Deep JS 高亮标识符查找经过的每一个环境。