JavaScript 闭包(Closure)

简介

**闭包(Closure)**是 JavaScript 中最强大和最容易让人困惑的概念之一。理解闭包对于掌握 JavaScript 至关重要。

参考文档

  • js-function.md — 函数文档,包含闭包的基本介绍
  • 本文档是闭包的专题深入文档,从原理到实践全面展开

词法作用域(Lexical Scoping)

要理解闭包,首先需要理解词法作用域

作用域链

let globalVar = 'global';

function outer() {
  let outerVar = 'outer';

  function inner() {
    let innerVar = 'inner';
    console.log(globalVar);  // "global"(访问全局)
    console.log(outerVar);  // "outer"(访问外部函数)
    console.log(innerVar);  // "inner"(访问自身)
  }

  inner();
}

outer();
// console.log(outerVar);  // ReferenceError(外部无法访问内部变量)

嵌套函数的作用域规则

function outer() {
  let x = 10;

  function inner() {
    let y = 20;
    console.log(x + y);  // 30(inner 可以访问 outer 的 x)
  }

  inner();
  // console.log(y);  // ReferenceError(outer 不能访问 inner 的 y)
}

outer();

变量查找顺序

当在函数内使用一个变量时,查找顺序是:

  1. 先在当前函数作用域中查找
  2. 如果没找到,到外层函数作用域查找
  3. 继续向外,直到全局作用域
  4. 如果全局也没找到,报错 ReferenceError
let a = 'global-a';

function level1() {
  let b = 'level1-b';

  function level2() {
    let c = 'level2-c';

    function level3() {
      let d = 'level3-d';
      console.log(a);  // "global-a"(从全局找到)
      console.log(b);  // "level1-b"(从 level1 找到)
      console.log(c);  // "level2-c"(从 level2 找到)
      console.log(d);  // "level3-d"(自身)
    }

    level3();
  }

  level2();
}

level1();

闭包的定义和原理

什么是闭包

闭包是指函数可以记住并访问它的词法作用域,即使函数在其词法作用域外执行。

function outer() {
  let count = 0;  // outer 的局部变量

  function inner() {
    count++;            // inner 访问了 outer 的变量
    console.log(count);
  }

  return inner;  // 返回 inner 函数
}

let counter = outer();  // outer 执行完毕
counter();           // 1 ← 但 inner 仍然可以访问 count!
counter();           // 2 ← count 的值被"记住"了
counter();           // 3

发生了什么?

  1. outer() 执行时,创建了 count = 0inner 函数
  2. outer() 返回 inner 函数,赋值给 counter
  3. outer() 执行完毕,按理说其局部变量应该被销毁
  4. inner 函数"关闭(closure)"了 count 变量,使其不会被垃圾回收
  5. 每次调用 counter(),仍然可以访问和修改 count

闭包的构成

闭包 = 函数 + 其词法作用域的引用

function createClosure() {
  let secret = '我是秘密';

  return function() {
    return secret;  // 这个函数"关闭"了 secret
  };
}

let getSecret = createClosure();
console.log(getSecret());  // "我是秘密"(即使 createClosure 已执行完)

每次调用产生独立的闭包

function makeCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}

let counterA = makeCounter();
let counterB = makeCounter();  // 新的闭包,新的 count

console.log(counterA());  // 1
console.log(counterA());  // 2
console.log(counterB());  // 1(独立的 count)
console.log(counterA());  // 3
console.log(counterB());  // 2

闭包的基本示例

最简单的闭包

function outer() {
  let message = 'Hello';

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

  inner();
}

outer();  // "Hello"

这已经是一个闭包!inner 访问了 outermessage 变量。

闭包中的变量共享

function create() {
  let value = 0;

  return {
    increment() {
      value++;
      return value;
    },
    decrement() {
      value--;
      return value;
    },
    getValue() {
      return value;
    }
  };
}

let obj = create();
console.log(obj.increment());  // 1
console.log(obj.increment());  // 2
console.log(obj.decrement());  // 1
console.log(obj.getValue());   // 1

// value 不能直接访问
// console.log(obj.value);  // undefined(私有变量)

闭包的生命周期

function outer() {
  let hugeData = new Array(1000000).fill('*');  // 大量数据
  let counter = 0;

  return function() {
    counter++;
    console.log(counter);
    // hugeData 也被闭包"关闭"了!
  };
}

let fn = outer();  // hugeData 不会被释放
// 只要 fn 还存在,hugeData 就占用内存

fn = null;  // 现在 fn 被释放,闭包消失,hugeData 可以被垃圾回收

闭包的实际应用

1. 数据封装(私有变量)

模拟私有属性和方法:

function createUser(name) {
  let age = 0;  // 私有变量,外部无法直接访问

  return {
    getName() {
      return name;  // name 也是闭包捕获的
    },
    getAge() {
      return age;
    },
    setAge(newAge) {
      if (newAge >= 0) {
        age = newAge;
        return true;
      }
      return false;
    }
  };
}

let user = createUser('Alice');
console.log(user.getName());  // "Alice"
console.log(user.getAge());   // 0
user.setAge(25);
console.log(user.getAge());   // 25

// 无法直接访问私有变量
// console.log(user.age);  // undefined
// user.age = 30;             // 无效

2. 函数工厂

创建特定功能的函数:

function multiplyBy(factor) {
  return function(number) {
    return number * factor;  // factor 被闭包捕获
  };
}

let double = multiplyBy(2);
let triple = multiplyBy(3);
let quadruple = multiplyBy(4);

console.log(double(5));     // 10
console.log(triple(5));     // 15
console.log(quadruple(5));  // 20

另一个例子——创建问候函数:

function greeter(greeting) {
  return function(name) {
    return `${greeting}, ${name}!`;
  };
}

let sayHello = greeter('Hello');
let sayHi = greeter('Hi');
let sayBonjour = greeter('Bonjour');

console.log(sayHello('Alice'));    // "Hello, Alice!"
console.log(sayHi('Bob'));         // "Hi, Bob!"
console.log(sayBonjour('Charlie')); // "Bonjour, Charlie!"

3. 模块化(模块模式)

在 ES Modules 出现之前,闭包是实现模块化的主要方式:

let myModule = (function() {
  let privateVar = '秘密数据';  // 私有变量

  function privateMethod() {          // 私有方法
    console.log('私有方法');
  }

  return {
    publicMethod() {
      console.log('公共方法,可以访问:', privateVar);
      privateMethod();
    },
    getSecret() {
      return privateVar;
    }
  };
})();  // 立即执行

myModule.publicMethod();  // "公共方法,可以访问: 秘密数据" + "私有方法"
console.log(myModule.getSecret());  // "秘密数据"
// myModule.privateMethod();  // 错误:privateMethod 不是公共的

4. 定时器中的闭包

for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);  // 全部输出 4!(经典闭包陷阱)
  }, i * 1000);
}
// 输出:4 4 4(3 秒后)

// 原因:setTimeout 的回调捕获了同一个 i 的引用
// 当回调执行时,循环早已结束,i 已经是 4

解决方法见下文"闭包的常见陷阱"章节。

5. 事件监听中的闭包

function setupButtons() {
  for (let i = 0; i < 3; i++) {
    let button = document.createElement('button');
    button.textContent = `按钮 ${i}`;

    button.addEventListener('click', function() {
      alert(`你点击了按钮 ${i}`);  // i 被闭包捕获
    });

    document.body.appendChild(button);
  }
}

// 每个按钮点击时,会显示正确的序号
// 因为 let 每次迭代创建新的词法作用域

6. 偏函数(Partial Application)

固定部分参数,产生新函数:

function partial(fn, ...fixedArgs) {
  return function(...remainingArgs) {
    return fn(...fixedArgs, ...remainingArgs);  // fixedArgs 被闭包捕获
  };
}

function add(a, b, c) {
  return a + b + c;
}

let add5and10 = partial(add, 5, 10);  // 固定前两个参数
console.log(add5and10(3));  // 18(5 + 10 + 3)

let add5 = partial(add, 5);      // 只固定第一个参数
console.log(add5(10, 3));        // 18(5 + 10 + 3)

7. 柯里化(Currying)

将多参数函数转换为一系列单参数函数:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return function(...moreArgs) {
      return curried.apply(this, args.concat(moreArgs));
    };
  };
}

function add(a, b, c) {
  return a + b + c;
}

let curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3));    // 6
console.log(curriedAdd(1, 2)(3));    // 6
console.log(curriedAdd(1)(2, 3));    // 6

8. 记忆化(Memoization)

缓存函数结果,避免重复计算:

function memoize(fn) {
  let cache = {};  // 闭包捕获的缓存对象

  return function(...args) {
    let key = JSON.stringify(args);

    if (cache[key]) {
      console.log('从缓存读取:', key);
      return cache[key];
    }

    console.log('计算:', key);
    let result = fn.apply(this, args);
    cache[key] = result;
    return result;
  };
}

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

let memoizedFib = memoize(fibonacci);

console.log(memoizedFib(10));  // 计算(较慢)
console.log(memoizedFib(10));  // 从缓存读取(立即)

闭包的常见陷阱

陷阱 1:循环中的闭包(var vs let)

问题代码(使用 var):

for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);  // 全部输出 4!
  }, 100);
}
// 输出:4 4 4

// 原因:
// 1. var 是函数作用域,整个循环中只有一个 i
// 2. setTimeout 的回调捕获了同一个 i 的引用
// 3. 回调执行时,循环早已结束,i 已经是 4

解决方法 1:使用 let(推荐)

for (let i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);  // 1 2 3
  }, i * 100);
}
// 输出:1 2 3

// 原因:let 是块级作用域,每次迭代创建新的 i

解决方法 2:使用闭包(IIFE)

for (var i = 1; i <= 3; i++) {
  (function(index) {
    setTimeout(function() {
      console.log(index);  // 1 2 3
    }, index * 100);
  })(i);  // 每次迭代创建新的作用域,捕获当时的 i 值
}

解决方法 3:使用 forEach

[1, 2, 3].forEach(function(i) {
  setTimeout(function() {
    console.log(i);  // 1 2 3
  }, i * 100);
});

陷阱 2:this 指向问题

let obj = {
  count: 0,
  start() {
    // 这里的 this 是 obj
    console.log(this.count);  // 0

    setTimeout(function() {
      this.count++;  // ❌ 这里的 this 不是 obj!
      console.log(this);  // 浏览器: window,Node.js: global 或 undefined(严格模式)
    }, 100);
  }
};

obj.start();

解决方法 1:使用箭头函数

let obj = {
  count: 0,
  start() {
    setTimeout(() => {
      this.count++;  // ✅ 箭头函数继承外层 this
      console.log(this.count);  // 1
    }, 100);
  }
};

obj.start();

解决方法 2:保存 this 引用

let obj = {
  count: 0,
  start() {
    let self = this;  // 用闭包保存 this
    setTimeout(function() {
      self.count++;  // ✅ 使用 self
      console.log(self.count);  // 1
    }, 100);
  }
};

obj.start();

陷阱 3:变量共享问题

function createFunctions() {
  let result = [];
  for (var i = 0; i < 3; i++) {
    result.push(function() {
      return i;  // 所有函数共享同一个 i!
    });
  }
  return result;
}

let funcs = createFunctions();
console.log(funcs[0]());  // 3(不是 0!)
console.log(funcs[1]());  // 3(不是 1!)
console.log(funcs[2]());  // 3(不是 2!)

解决方法:使用 let 或闭包

// 方法 1:let
function createFunctions() {
  let result = [];
  for (let i = 0; i < 3; i++) {  // let 每次迭代创建新的 i
    result.push(function() {
      return i;  // 每个函数捕获各自的 i
    });
  }
  return result;
}

let funcs = createFunctions();
console.log(funcs[0]());  // 0 ✅
console.log(funcs[1]());  // 1 ✅
console.log(funcs[2]());  // 2 ✅

内存管理和性能

闭包会占用多少内存?

function createClosure() {
  let small = 42;                // 很小的数据
  let huge = new Array(1000000).fill('*');  // 很大的数据

  return function() {
    console.log(small);  // 只使用了 small
    // 但 huge 也被闭包持有,无法释放!
  };
}

let fn = createClosure();
// 即使只使用 small,huge 数组也一直占用内存

内存泄漏问题

// 危险:闭包持有大量数据
function setup() {
  let hugeData = new Array(1000000).fill('*');

  document.getElementById('btn').addEventListener('click', function() {
    // 这个事件监听函数形成了闭包
    console.log('clicked');
    // 它捕获了 setup 的作用域,包括 hugeData!
  });

  // hugeData 永远不会被释放(只要事件监听还在)
}

// 解决方法:手动移除监听,或避免捕获不需要的数据
function setupFixed() {
  let hugeData = new Array(1000000).fill('*');
  let btn = document.getElementById('btn');

  function handler() {
    console.log('clicked');
  }

  btn.addEventListener('click', handler);

  // 稍后移除
  // btn.removeEventListener('click', handler);
  // 或者:
  // handler = null;  // 断开引用
}

如何"释放"闭包

function create() {
  let data = 'some data';
  return function() {
    return data;
  };
}

let closure = create();
console.log(closure());  // "some data"

// 释放闭包(断开所有引用)
closure = null;  // 现在闭包可以被垃圾回收

性能考虑

// 函数内部的函数定义(每次调用都创建新闭包)
function outer() {
  let x = 10;
  return function() {  // 每次调用 outer,都创建新的闭包
    return x;
  };
}

// 更好的方式:避免频繁创建闭包
const inner = (function() {
  let x = 10;
  return function() {
    return x;
  };
})();  // 只创建一次

// 或者:如果不需要闭包,就不要使用
function add(a, b) {
  return a + b;  // 不需要闭包,更高效
}

闭包 vs 类(Class)

ES6 引入了 class,有时可以替代闭包实现封装:

// 使用闭包实现封装
function createCounter() {
  let count = 0;
  return {
    increment() {
      count++;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

let c1 = createCounter();
console.log(c1.increment());  // 1
// count 是私有的
// 使用 class 实现(ES6)
class Counter {
  #count = 0;  // 私有字段(ES2022)

  increment() {
    this.#count++;
    return this.#count;
  }

  getCount() {
    return this.#count;
  }
}

let c2 = new Counter();
console.log(c2.increment());  // 1
// c2.#count  // SyntaxError(私有字段)
特性 闭包 Class
私有性 完全私有(闭包内) 私有字段 #(ES2022)
内存 每个实例独立闭包 方法共享原型
适合场景 函数式编程、工厂函数 面向对象编程、复杂对象
性能 每个实例独立闭包 方法在原型上共享

综合示例

示例 1:实现可配置的计时器

function createTimer(interval) {
  let timerId = null;
  let count = 0;
  let callbacks = [];  // 闭包捕获的回调列表

  return {
    start() {
      if (timerId) return;
      timerId = setInterval(() => {
        count++;
        callbacks.forEach(cb => cb(count));
      }, interval);
    },
    stop() {
      clearInterval(timerId);
      timerId = null;
    },
    reset() {
      count = 0;
    },
    onTick(callback) {
      callbacks.push(callback);
    },
    getCount() {
      return count;
    }
  };
}

let timer = createTimer(1000);
timer.onTick(count => console.log('tick:', count));
timer.start();

// 5 秒后停止
setTimeout(() => {
  timer.stop();
  console.log('最终计数:', timer.getCount());
}, 5000);

示例 2:实现观察者模式

function createObservable() {
  let observers = [];  // 闭包私有的观察者列表

  return {
    subscribe(callback) {
      observers.push(callback);
      // 返回取消订阅的函数
      return () => {
        observers = observers.filter(cb => cb !== callback);
      };
    },
    notify(data) {
      observers.forEach(cb => cb(data));
    }
  };
}

let observable = createObservable();

let unsub1 = observable.subscribe(data => console.log('观察者1:', data));
let unsub2 = observable.subscribe(data => console.log('观察者2:', data));

observable.notify('Hello!');
// 输出:
// 观察者1: Hello!
// 观察者2: Hello!

unsub1();  // 取消订阅
observable.notify('World!');
// 输出:
// 观察者2: World!(只有观察者2 收到)

示例 3:解决循环中的闭包问题(完整方案)

// 问题:循环中的闭包
function createWrong() {
  let result = [];
  for (var i = 0; i < 3; i++) {
    result.push(() => console.log(i));  // 全部输出 3
  }
  return result;
}

// 方案 1:使用 let(推荐)
function createWithLet() {
  let result = [];
  for (let i = 0; i < 3; i++) {  // let 块级作用域
    result.push(() => console.log(i));  // 0, 1, 2
  }
  return result;
}

// 方案 2:使用闭包(IIFE)
function createWithIIFE() {
  let result = [];
  for (var i = 0; i < 3; i++) {
    ((index) => {
      result.push(() => console.log(index));  // 0, 1, 2
    })(i);
  }
  return result;
}

// 方案 3:使用 forEach
function createWithForEach() {
  let result = [];
  [0, 1, 2].forEach(i => {
    result.push(() => console.log(i));  // 0, 1, 2
  });
  return result;
}

// 测试
let f1 = createWithLet();
f1.forEach(fn => fn());  // 0 1 2

let f2 = createWithIIFE();
f2.forEach(fn => fn());  // 0 1 2

示例 4:实现缓存装饰器

function cached(fn) {
  let cache = new Map();  // 闭包缓存

  return function(...args) {
    let key = JSON.stringify(args);

    if (cache.has(key)) {
      console.log('缓存命中:', key);
      return cache.get(key);
    }

    console.log('计算中:', key);
    let result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// 使用
function expensiveCalculation(x, y) {
  console.log('(执行耗时计算...)');
  return x * y;
}

let cachedCalc = cached(expensiveCalculation);

console.log(cachedCalc(3, 4));  // 计算中: [3,4] → 12
console.log(cachedCalc(3, 4));  // 缓存命中: [3,4] → 12
console.log(cachedCalc(5, 6));  // 计算中: [5,6] → 30

总结:闭包速查表

概念 说明
定义 函数 + 其词法作用域的引用
构成 内部函数 + 访问外部变量
原理 词法作用域;变量不会被销毁
每次调用 产生独立的闭包(独立作用域)
内存 闭包会持有外部变量的引用
释放 断开引用(赋值为 null)
经典陷阱 循环中的闭包(用 let 解决)
this 问题 普通函数 this 动态;箭头函数继承
常见应用 封装、工厂、模块化、偏函数、记忆化

闭包检查清单

当你写代码时,问自己:

  • 这个函数访问了外部变量吗?(如果是,它就是闭包)
  • 外部变量会在函数执行完后还存在吗?(闭包会让它存在)
  • 是否需要释放闭包?(赋值为 null)
  • 循环中创建闭包时,用 let 了吗?(避免共享变量问题)
  • this 指向正确吗?(考虑用箭头函数)

核心记忆:闭包就是函数可以记住它被创建时的环境,即使后来在别处执行,仍然可以访问那个环境中的变量。