变量与作用域

张玥 2026年9月16日 阅读时间 25分钟
JavaScript 变量声明 作用域 闭包 ES6
JavaScript基础之变量与作用域

变量是 JavaScript 程序中最基本的"容器",而作用域则决定了这些容器在代码中的可见范围与生命周期。看似简单的 var、let、const,背后牵扯出变量提升、暂时性死区、词法作用域、作用域链、闭包等一系列核心机制。理解它们,不仅能让你写出更可靠的代码,更是读懂框架源码、排查诡异 Bug 的必备功底。本文将从零开始,层层递进地拆解变量与作用域的完整知识体系,配合大量代码示例与可视化图解,带你用 25 分钟彻底吃透这块 JavaScript 的地基。

为什么变量与作用域是 JavaScript 的地基

在 JavaScript 中,变量是存放数据的"命名容器",而作用域是这些容器的"可见范围规则"。几乎所有 JavaScript 的"诡异行为"——循环里的回调全都输出同一个值、函数内部访问不到外部变量、变量莫名其妙变成 undefined——追根溯源,都是作用域规则在起作用。

// 变量:给数据起一个名字
let userName = "张玥";
let age = 28;
const isDeveloper = true;

// 作用域:决定变量在哪里能被访问
function greet() {
  const message = "你好," + userName;
  return message;
}

greet(); // "你好,张玥"(可以访问外层变量)
console.log(message);
// ReferenceError: message is not defined
// 函数内部声明的变量,外部无法访问
作用域嵌套示意
全局作用域
userName · age · isDeveloper
函数作用域 greet()
message
内层可以访问外层,外层不能访问内层

三种声明方式:var、let、const

ES6 之前,JavaScript 只有 var 一种声明方式;ES6 引入了 let 和 const,从根源上修补了 var 的诸多设计缺陷。理解三者的差异,是掌握作用域的第一步。

/* 1. var:函数作用域,可重复声明 */
var a = 1;
var a = 2; // 不报错,覆盖前者

/* 2. let:块级作用域,不可重复声明 */
let b = 1;
// let b = 2; // SyntaxError: 重复声明
b = 3; // 允许重新赋值

/* 3. const:块级作用域,不可重新赋值 */
const c = 1;
// c = 2; // TypeError: Assignment to constant

/* 注意:const 保证的是"绑定"不变,不是值不变 */
const list = [1, 2];
list.push(3); // ✅ 允许:修改的是对象内容
// list = []; // ❌ 禁止:重新绑定引用
作用域提升重复声明
var 函数级 是(undefined) 允许
let 块级 TDZ 禁止
const 块级 TDZ 禁止

现代开发默认使用 const,需要重新赋值时再用 let

作用域家族:全局、函数、块级

JavaScript 中的作用域可以分为三类:全局作用域、函数作用域和块级作用域。它们像俄罗斯套娃一样层层嵌套,构成了变量的"可见性地图"。

/* 1. 全局作用域:任何地方都能访问 */
const globalVar = "全局";

/* 2. 函数作用域:仅函数内部可见 */
function outer() {
  var funcVar = "函数内";
  console.log(globalVar); // ✅ 可访问

  /* 3. 块级作用域:{} 内部 */
  if (true) {
    let blockVar = "块内";
    console.log(blockVar); // ✅
  }
  // console.log(blockVar); // ❌ ReferenceError

  for (let i = 0; i < 3; i++) {
    // 每次循环都是一个新的块级作用域
  }
}

// console.log(funcVar); // ❌ ReferenceError
全局作用域
globalVar
函数作用域 outer()
funcVar
块级作用域 if {}
blockVar

作用域由内向外层层嵌套,访问方向只能从内到外

作用域链与词法作用域

当代码访问一个变量时,JavaScript 引擎会从当前作用域开始,逐层向外查找,直到找到该变量或抵达全局作用域为止。这条查找路径就叫作用域链。而作用域链的构建依据,是代码书写位置而非调用位置——这就是词法作用域(静态作用域)。

// 词法作用域:看代码写在哪里,而不是在哪里调用
const name = "全局";

function outer() {
  const name = "外层";

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

  return inner;
}

const fn = outer();
fn(); // 输出 "外层",而非 "全局"

// 查找顺序:inner 作用域 → outer 作用域 → 全局作用域
// 沿途第一个匹配的变量胜出
变量查找路径
1 inner() 作用域 —— 没有 name
2 outer() 作用域 —— 找到 name = "外层" ✅
3 全局作用域 —— 不再查找

找到即停,不会继续向外查找

变量提升与暂时性死区(TDZ)

在代码执行之前,JavaScript 引擎会先做一次"预解析",把变量和函数声明提前处理。这个机制就是变量提升(Hoisting)。它解释了为什么有些变量在声明前就能使用,而有些却会直接报错。

/* var:声明提升,初始化为 undefined */
console.log(a); // undefined(不报错)
var a = 1;

/* 上面的代码等价于 */
var a;
console.log(a); // undefined
a = 1;

/* let / const:也存在提升,但进入 TDZ */
console.log(b);
// ReferenceError: Cannot access 'b'
// before initialization
let b = 2;

/* 函数声明:整体提升,可直接调用 */
sayHi(); // ✅ "hi"
function sayHi() {
  console.log("hi");
}

/* 函数表达式:按变量规则处理 */
sayBye(); // TypeError: sayBye is not a function
var sayBye = function () {};
暂时性死区(TDZ)
进入作用域
作用域开始 let 声明语句
TDZ 区间:访问即报错
ReferenceError
声明之后:正常访问
值已初始化

闭包:作用域的"超能力"

当内部函数引用了外部函数的变量,并且这个内部函数在外部函数返回后仍然被使用,就形成了闭包(Closure)。闭包让函数拥有了"记忆"能力,是模块化、私有变量、函数柯里化等高级模式的基石。

/* 闭包:计数器 */
function createCounter() {
  let count = 0; // 被闭包"捕获"

  return {
    increment() {
      count += 1;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getCount();  // 2

/* count 是私有的,外部无法直接修改 */
// console.log(counter.count); // undefined
闭包内存模型
createCounter() 作用域(已返回)
count = 2
↓ 被引用,不被回收
返回的对象方法
increment() getCount()

外部函数已执行完毕,但其变量依然存活

循环中的变量陷阱:var vs let

这是面试与实战中出现频率极高的经典问题:在循环中创建函数,使用 var 和 let 会得到截然不同的结果。理解它,就真正理解了块级作用域的价值。

/* ❌ var:只有一个共享的作用域 */
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 100);
}
// 输出:3 3 3

/* ✅ let:每次迭代生成新的块级作用域 */
for (let j = 0; j < 3; j++) {
  setTimeout(() => {
    console.log(j);
  }, 100);
}
// 输出:0 1 2

/* 使用 IIFE 模拟块级作用域(ES5 时代的方案) */
for (var k = 0; k < 3; k++) {
  (function (n) {
    setTimeout(() => console.log(n), 100);
  })(k);
}
// 输出:0 1 2
var:共享同一个 i
i → 三个回调都指向它 → 最终值 3
let:每次迭代独立变量
j=0 j=1 j=2 → 输出 0 1 2

全局对象、模块作用域与 IIFE

不同声明方式在全局环境中会产生不同的"副作用"。同时,模块与 IIFE 提供了隔离作用域的有效手段,避免污染全局命名空间。

/* 1. var 在全局会挂载到 window */
var globalVar = 1;
console.log(window.globalVar); // 1

/* 2. let / const 不会挂载到 window */
let globalLet = 2;
console.log(window.globalLet); // undefined

/* 3. 跨环境的全局对象:globalThis */
console.log(globalThis === window); // 浏览器
console.log(globalThis === global); // Node.js

/* 4. IIFE:立即执行函数表达式,隔离作用域 */
(function () {
  var privateVar = "外部访问不到";
  console.log(privateVar);
})();

/* 5. ES Module:每个模块拥有独立作用域 */
// module.js
const moduleVar = 1;
export default moduleVar;
// 模块内变量默认不污染全局
var 挂载到 globalThis ⚠️
let / const 不挂载 ✅
IIFE 函数作用域隔离 ✅
ES Module 模块作用域隔离 ✅

常见错误排查清单

掌握了原理,还要能快速定位问题。下面这份清单覆盖了变量与作用域相关的绝大多数"翻车现场"。

/* 错误 1:TDZ 访问 */
console.log(x);
let x = 1;
// → ReferenceError

/* 错误 2:const 未初始化 */
// const y; // SyntaxError

/* 错误 3:隐式全局变量(严格模式下报错) */
function test() {
  undeclared = 1; // 不要这么做!
}

/* 错误 4:块级作用域失效(用 var) */
for (var i = 0; i < 3; i++) {}
console.log(i); // 3(变量泄漏到外层)

/* 错误 5:this 与作用域混淆 */
const obj = {
  name: "obj",
  arrow: () => this.name,
  normal() { return this.name; }
};
obj.arrow();  // undefined(箭头函数无自身 this)
obj.normal(); // "obj"
快速自查
报 ReferenceError?先检查 TDZ 与拼写
值为 undefined?检查 var 提升
循环回调值相同?换成 let
变量意外共享?检查作用域边界
this 指向不对?区分箭头函数与普通函数

建议始终开启严格模式 "use strict"

最佳实践与总结

  • 默认使用 const:只有确实需要重新赋值时才用 let,彻底告别 var
  • 声明即初始化:避免声明与赋值分离,减少 TDZ 风险
  • 缩小作用域:变量声明在尽可能靠近使用位置的最小作用域内
  • 避免隐式全局:始终开启严格模式,防止变量意外泄漏
  • 善用块级作用域:用 {} 显式划分逻辑边界,配合 let 隔离临时变量
  • 理解闭包开销:闭包会延长变量生命周期,避免无谓的内存占用
  • 模块化隔离:使用 ES Module 或 IIFE 避免全局污染
  • 区分作用域与 this:作用域由书写位置决定,this 由调用方式决定
/* 一段规范的现代 JavaScript 代码 */
"use strict";

const TAX_RATE = 0.06;

function createCart() {
  let items = [];

  return {
    add(item) {
      items.push(item);
    },
    total() {
      const sum = items.reduce((s, i) => s + i.price, 0);
      return sum * (1 + TAX_RATE);
    }
  };
}

/* items 被闭包保护,外部无法直接篡改 */