闭包与作用域

张玥 2026年2月14日 阅读时间 35分钟
JavaScript 闭包 作用域 核心教程
JavaScript 闭包与作用域概念图

作用域和闭包是 JavaScript 中最核心也是最容易被误解的概念。理解它们,你才能真正驾驭这门语言。本文将带你从执行上下文开始,逐步深入词法作用域、作用域链,并通过数十个代码示例彻底搞懂闭包的机制、应用场景以及内存管理。无论你是正在准备面试,还是希望写出更健壮的代码,这篇文章都值得花35分钟细细品味。

什么是作用域?

作用域决定变量和函数的可访问范围。JavaScript 采用词法作用域(静态作用域),即函数的作用域在函数定义时就决定了。

// 全局作用域
var globalVar = "我在任何地方都能访问";

// 函数作用域
function demo() {
  var funcVar = "只能在函数内访问";
  console.log(globalVar); // ✅ 可访问
}
console.log(funcVar); // ❌ ReferenceError

// 块级作用域 (ES6)
if (true) {
  let blockVar = "ES6 块级";
  const alsoBlock = "常量也是块级";
  var stillGlobal = "var 没有块级作用域";
}
console.log(stillGlobal); // ✅ "var 没有块级作用域"
console.log(blockVar); // ❌ ReferenceError
三种作用域

全局 : 生命周期贯穿整个程序,挂载在window/global。

函数 : 每次函数调用创建,参数和内部var/function声明。

块级 : let / const{ } 内形成,循环、条件中尤其重要。

词法作用域 & 作用域链

函数的作用域在书写时确定,内部函数可以访问外部函数的变量,这种嵌套关系形成作用域链。

// 词法作用域示例
var a = 1;
function outer() {
  var b = 2;
  function inner() {
    var c = 3;
    console.log(a, b, c); // 1 2 3
  }
  inner();
}
outer();

// 作用域链查找:从当前作用域向上直到全局
var x = 'global';
function f1() {
  var x = 'f1';
  function f2() {
    console.log(x); // 'f1' (不是global)
  }
  f2();
}
f1();
inner作用域: c = 3
outer作用域: b = 2
全局作用域: a = 1, 函数声明

⬆️ 变量访问沿着链条向上查找,直到找到或报错。

执行上下文 & 活动对象

每次执行函数都会创建新的执行上下文,包含变量对象(VO/活动对象AO)、作用域链和this。

// 执行上下文伪代码
// 全局上下文
GlobalExecutionContext = {
  VO: { a: 1, fn: <reference> },
  scopeChain: [GlobalContext.VO],
  this: window
}

// outer函数被调用时
outerContext = {
  AO: { b: 2, inner: <reference> },
  scopeChain: [outerContext.AO, GlobalContext.VO],
  this: ...
}
执行上下文栈 (ECS)

程序启动: 推入全局上下文。
调用outer: 推入outer上下文。
调用inner: 推入inner上下文。
inner返回后弹出,outer返回后弹出,最后只剩全局。

闭包 (Closure) —— 定义与原理

闭包是指内部函数可以访问外部函数作用域的变量,即使外部函数已经执行完毕。JavaScript 函数都会形成闭包,但通常我们关注被返回或传递的函数。

// 最简单的闭包
function createCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// createCounter 执行完后,它的 AO 本该销毁,但因为被内部函数引用,依然存活。
闭包三要素
  • 函数嵌套函数
  • 内部函数引用外部变量
  • 内部函数在外部函数之外被调用(或保持可访问)

应用一:私有变量与模块模式

利用闭包可以创建真正的私有成员,这是模块模式的基础。

// 私有变量计数器
const bankAccount = (function() {
  let balance = 0; // 私有
  return {
    deposit(amount) { balance += amount; },
    getBalance() { return balance; }
  };
})();
bankAccount.deposit(100);
console.log(bankAccount.getBalance()); // 100
console.log(bankAccount.balance); // undefined

// 现代模块模式 (ES6 之前)
var myModule = (function() {
  var privateVar = 'secret';
  function privateMethod() {}
  return { publicMethod: function() {} };
})();
为什么私有?

外部无法直接访问 balance,只能通过暴露的接口操作。数据封装、隐藏实现细节。

应用二:回调、事件与防抖节流

闭包在异步编程中无处不在,用于保存状态。

// 防抖函数 (debounce) 经典闭包应用
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 使用闭包保存循环变量
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 100); // 0,1,2
  })(i);
}
闭包保存状态

每次循环的匿名函数都通过闭包“记住”了当时的 i 值(参数 j)。

let 则直接在块级作用域解决了这个问题。

经典闭包陷阱:循环中的var

一个让无数开发者头疼的问题,以及它的多种解决方案。

// 问题代码
for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出 4,4,4 (循环结束后i=4)
  }, 100);
}

// 解法1: 闭包 + 立即执行函数
for (var i = 1; i <= 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}

// 解法2: 使用 let (块级作用域)
for (let i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 100); // 1,2,3
}
原因

var 没有块级作用域,所有 setTimeout 共享同一个 i。闭包通过函数参数保存每次的副本。

let 在每次迭代都会创建一个新的绑定,更简洁。

闭包与内存管理

闭包会阻止垃圾回收,因为外部变量仍然被内部函数引用。使用不当会造成内存泄漏。

// 内存泄漏示例
function heavy() {
  let bigData = new Array(1000000).fill('*');
  return function() {
    console.log('闭包引用了 bigData');
  };
}
const leak = heavy(); // bigData 永远不会被释放

// 手动解除
leak = null; // 现在 bigData 可以被回收

// 避免不必要的闭包引用
function good() {
  let data = 'useful';
  return function() { console.log(data); };
} // 如果 data 很小且必要,则合理
垃圾回收小贴士
  • 闭包只引用必要的变量,避免无意中引用大对象
  • 及时将不需要的闭包置为 null
  • 使用弱映射 WeakMap 存储与对象关联的数据

ES6+ 对作用域的增强

let/const、箭头函数、块级作用域大大简化了闭包的使用场景。

// 箭头函数没有自己的 this,继承父级作用域的 this
function Person(name) {
  this.name = name;
  setTimeout(() => {
    console.log(this.name); // 箭头函数,this 指向 Person 实例
  }, 100);
}

// 块级作用域简化闭包
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), i*100); // 0,1,2,3,4
}
最佳实践:优先使用 let/const,结合箭头函数,代码更简洁且闭包陷阱更少。

调试闭包:Chrome DevTools

在浏览器中观察闭包非常直观,可以查看 [[Scopes]] 属性。

// 断点调试
function makeAdder(x) {
  return function(y) {
    debugger; // 在此处暂停
    return x + y;
  };
}
const add5 = makeAdder(5);
add5(3);

// 在 Sources 面板右侧 Scope 中可以看到 Closure (makeAdder) 包含 x:5
技巧

在 console 中直接输入函数名,点击展开 [[Scopes]] 可以看到闭包持有的变量。

总结与核心要点

  • 作用域分为全局、函数、块级;JavaScript 采用词法作用域。
  • 每次函数执行创建新的执行上下文,作用域链由当前AO和所有父级AO组成。
  • 闭包 = 函数 + 函数定义时的词法环境。它使得内部函数可以访问外部变量,即使外部函数已返回。
  • 闭包用途广泛:私有变量、模块化、回调、函数柯里化、防抖节流等。
  • 注意闭包可能引起内存泄漏,合理管理引用,及时置 null。
  • ES6 的 let/const 极大简化了块级作用域和循环闭包问题。