创见博客
深入理解 JavaScript 词法作用域:变量到底从哪儿来?
七崽爱吃小饼干2026/11/03阅读 0

要理解 JS 的很多行为(闭包、变量查找、var 与 let 的差异、模块封装),都绕不开一个底层机制——词法作用域。它是 JavaScript 作用域模型的基石,也是面试里区分「背题」和「真懂」的分水岭。

本文从「是什么」「为什么这么设计」「常见例子」「和 this 的区别」四个角度,把词法作用域讲清楚。


一、词法作用域是什么

1. 一句话定义

词法作用域(Lexical Scope),也叫静态作用域(Static Scope):变量能被访问的范围,由代码书写时的位置决定,在编译(解析)阶段就已经确定,与函数在哪里被调用无关。

「词法」二字来自编译原理——作用域由源码的词法结构决定,也就是你在哪个函数、哪个代码块里写了这段代码,而不是运行时谁调用了它。

2. 作用域链与变量查找

JS 解析时,会为每个函数/代码块创建作用域,并按嵌套关系形成一条作用域链。查找变量时:

  1. 先找当前作用域;
  2. 找不到就沿作用域链去外层作用域找;
  3. 一直找到全局作用域,还找不到就报 ReferenceError。

关键点:这条链在代码写出来的时候就固定了,运行时不会因为调用位置发生变化。

3. 与动态作用域对比

维度词法(静态)作用域动态作用域
由什么决定代码书写位置函数调用位置/调用栈
确定时机编译期运行期
JS 是否采用✅ 是❌ 否
典型语言JS、C、JavaBash、早期 Lisp

理解了这张表,就理解了下面那个经典陷阱。


二、为什么 JS 要这么设计

1. 可预测、可读

词法作用域让「一个变量来自哪里」只看代码结构就能确定,不用追踪运行时调用栈。

js
var value = 1;
function foo() {
  console.log(value);
}
function bar() {
  var value = 2;
  foo(); // 调用位置在 bar 内部
}
bar(); // 输出 1,而不是 2

foo 定义在全局,它看到的是全局的 value。即使它在 bar 里被调用,也不会去用 bar 里的 value。如果 JS 是动态作用域,这里就会输出 2——那样代码的行为会强烈依赖「谁调用了它」,非常难以推理。

2. 静态分析 / 编译优化

因为作用域在编译期就定了,引擎可以:

  • 提前确定变量的存储位置(是局部变量还是闭包变量);
  • 做内联、逃逸分析、死代码消除等优化;
  • 让编辑器、TypeScript、ESLint 在不运行代码的情况下就能做跳转、补全、类型检查。

动态作用域下,一个变量指向哪里要运行时才知道,这些优化几乎无法进行。

3. 闭包的基础

闭包的本质就是「函数记住并访问它定义时所在的作用域」。词法作用域是闭包能够成立的前提——函数返回后,它的外层作用域仍然能通过作用域链被访问到。

js
function outer() {
  var message = 'hello';
  function inner() {
    console.log(message); // inner 记住了定义时的外层作用域
  }
  return inner;
}
const fn = outer();
fn(); // hello

4. 减少命名冲突

有了作用域嵌套,内层可以定义同名变量而不污染外层,配合模块化就能做封装。


三、JS 中常见的词法作用域例子

1. 三种作用域

js
var globalVar = 'global'; // 全局作用域

function demo() {
  var funcVar = 'function'; // 函数作用域
  if (true) {
    let blockVar = 'block'; // 块级作用域(ES6)
    console.log(blockVar);  // block
  }
  console.log(funcVar);     // function
}

2. var 是函数作用域,let/const 是块级作用域

js
if (true) {
  var a = 1;
  let b = 2;
}
console.log(a); // 1,泄漏到外层
console.log(b); // ReferenceError,b 只属于 if 块

3. 经典 for 循环陷阱

js
// var:所有回调共享同一个 i
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3 3 3
}

// let:每次迭代创建新的块级绑定
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 0 1 2
}

let 的表现正是因为块级作用域是按书写结构确定的——每次循环体都是一个新的词法作用域。

4. 闭包与私有变量

js
function createCounter() {
  let count = 0;
  return {
    increment() { return ++count; },
    get() { return count; }
  };
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.get()); // 2

count 不属于返回对象,但因为闭包持有 createCounter 的作用域,它既不会被回收,也无法被外部直接篡改——这就是模块化私有变量的原理。

5. 变量提升也是词法解析的结果

js
console.log(x); // undefined
var x = 1;

console.log(y); // ReferenceError(暂时性死区)
let y = 1;

var 的声明在解析阶段被提升到所属函数/全局作用域顶部,let/const 也有提升但处于「暂时性死区」,本质都是编译期的词法分析在起作用。


四、普通函数的 this 和词法作用域有什么不同

这是最容易混淆的地方。核心区别:

词法作用域管的是「变量怎么找」,this 是一套独立的特殊绑定,普通函数的 this 由调用方式在运行时决定。

1. 普通函数:this 是动态绑定

js
function show() {
  console.log(this.name);
}

const obj = { name: 'obj', show };
const other = { name: 'other' };

obj.show();        // obj     —— 隐式绑定,指向调用它的对象
show.call(other);  // other   —— 显式绑定
const fn = obj.show;
fn();              // undefined/global —— 默认绑定,丢失了 obj
new show();        // 指向新创建的实例

this 的指向完全取决于调用点(call site),同一份函数代码,换个调用方式结果就变。这叫动态绑定。

2. 箭头函数:this 是词法绑定

箭头函数没有自己的 this,它把 this 当作外层词法作用域里的一个普通变量来解析——沿作用域链找到最近的非箭头函数的 this。

js
const obj = {
  name: 'obj',
  regular() {
    console.log(this.name);
  },
  arrow: () => {
    console.log(this.name); // 这里的 this 来自模块/全局,不是 obj
  }
};

obj.regular(); // obj
obj.arrow();   // undefined(非严格全局下是 window.name)

所以箭头函数一旦定义,this 就固定了,不受调用方式影响——这正是借用了词法作用域的解析机制,术语上叫词法 this(Lexical this)。

3. 对比表

维度词法作用域(变量查找)普通函数的 this箭头函数的 this
决定时机编译期运行时编译期(跟随外层)
由什么决定代码书写位置调用方式(call site)定义时所在外层作用域
是否可变否每次调用可变否
绑定方式词法/静态动态词法/静态
常见规则作用域链逐层查找默认/隐式/显式/new沿作用域链找外层 this

4. 一个记忆窍诀

  • 问「这个变量是谁的?」→ 看代码写在哪个作用域里 → 词法作用域。
  • 问「这个 this 是谁?」→ 看普通函数怎么被调用 → 动态绑定;箭头函数则看它定义在哪 → 词法 this。

千万别把两者混为一谈:JS 的作用域永远是词法的,只有普通函数的 this 是动态的。 所谓「动态作用域」是另一个概念,JS 并不支持。


总结

  1. 词法作用域:变量的可见范围由代码书写位置决定,编译期固定,靠作用域链查找,与调用位置无关。
  2. 为什么这么设计:可预测、可静态分析优化、是闭包和模块封装的基础、减少命名冲突。
  3. 常见体现:var/let 作用域差异、for 循环闭包、私有变量、变量提升。
  4. 与 this 的区别:作用域解决「变量从哪来」;普通函数的 this 是运行时按调用方式动态绑定,箭头函数没有自己的 this,采用词法 this,跟随外层作用域——这才是和词法作用域最接近的一处。
评论
0/100