海川的日志

闭包与作用域

作者:海川,发表于:2026年1月27日 11:21:36

从作用域链到闭包陷阱,深度掌握 JavaScript 的作用域机制、闭包原理、内存管理,包括 10 个实战模式和性能优化

前言

闭包和作用域是 JavaScript 最”玄学”的概念。很多开发者能用,但说不清原理。结果面试被虐,项目里埋雷。

本文从作用域的本质作用域链的机制闭包的原理,到10 个实战模式性能优化,让你真正理解 JavaScript 的作用域和闭包。


一、核心速览(60 秒)

作用域概念

类型作用范围存在时间创建方式
全局作用域整个程序程序开始到结束全局
函数作用域函数内部函数调用到返回function
块级作用域{} 内部块级执行完成let/const
动态作用域函数被调用的位置调用时确定this

闭包本质

闭包 = 函数 + 它能访问的外层变量

简单说:一个函数能记住并访问它外层作用域的变量,即使外层函数已经执行完毕

闭包的三个条件

  1. 有一个内层函数
  2. 内层函数引用外层变量
  3. 外层函数已执行完毕,内层函数仍存在

二、作用域基础

作用域链(Scope Chain)

const global = "global"; // 全局作用域

function outer() {
  const outerVar = "outer"; // 函数作用域

  function inner() {
    const innerVar = "inner"; // 函数作用域

    console.log(innerVar); // 'inner'(本层)
    console.log(outerVar); // 'outer'(父层)
    console.log(global); // 'global'(全局)
  }

  inner();
}

outer();

访问顺序

  1. 先在本层查找 innerVar → 找到
  2. 再在父层查找 outerVar → 找到
  3. 再在全局查找 global → 找到
  4. 如果全部找不到 → ReferenceError

关键点:变量查找是单向的,内层可以访问外层,但外层无法访问内层。

const x = "global";

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

  function inner() {
    const x = "inner";
    console.log(x); // 'inner'(就近原则)
  }

  inner();
}

outer();

就近原则:如果多层都有同名变量,使用最近的。

全局作用域

// 全局变量
var globalVar = "var"; // 会成为 window 属性
let globalLet = "let"; // 不会
const globalConst = "const"; // 不会

console.log(window.globalVar); // 'var'
console.log(window.globalLet); // undefined
console.log(window.globalConst); // undefined

问题:全局变量污染,所有函数都能修改。

// ❌ 全局污染
var count = 0;

function increment() {
  count++; // 任何地方都能修改
}

// ✅ 避免全局污染
const counter = {
  count: 0,
  increment() {
    this.count++;
  },
};

函数作用域 vs 块级作用域

// var:函数作用域(ES5)
function test() {
  if (true) {
    var x = 1;
  }
  console.log(x); // 1(泄漏出来)
}

// let/const:块级作用域(ES6)
function test() {
  if (true) {
    let y = 1;
  }
  console.log(y); // ReferenceError(不泄漏)
}

// for 循环中特别明显
for (var i = 0; i < 3; i++) {} // var i 是函数级
console.log(i); // 3(污染全局)

for (let j = 0; j < 3; j++) {} // let j 是块级
console.log(j); // ReferenceError

为什么 let/const 更好

  • 防止变量泄漏
  • 避免变量提升的混乱
  • 减少意外的全局污染

暂时死区(Temporal Dead Zone)

// var:有提升(初始化为 undefined)
console.log(x); // undefined
var x = 1;

// let/const:有提升但未初始化(暂时死区)
console.log(y); // ReferenceError
let y = 1;

// 暂时死区的范围:从块开始到声明处
function test() {
  console.log(z); // ReferenceError(在 let z 声明前)
  let z = 1;
  console.log(z); // 1
}

三、闭包:JavaScript 最强大的特性

什么是闭包?

// 最简单的闭包
function createCounter() {
  let count = 0; // 外层变量

  return function () {
    return ++count; // 内层函数访问外层变量
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

这就是闭包

  • counter 是返回的内层函数
  • 虽然 createCounter 已经执行完毕,但 counter 仍然能访问 count
  • 每次调用 counter,都使用同一个 count 变量(重要!)

闭包能做什么?

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

// ✅ 模拟"私有变量"
const user = (() => {
  let _password = ""; // 私有变量(通常用 _ 前缀)

  return {
    setPassword(pwd) {
      _password = pwd;
    },
    verifyPassword(pwd) {
      return _password === pwd;
    },
  };
})();

user.setPassword("123456");
console.log(user._password); // undefined(访问不到)
console.log(user.verifyPassword("123456")); // true

2. 工厂函数

// 创建多个独立的对象
function createUser(name) {
  let email = ""; // 每个实例都有独立的变量

  return {
    getName() {
      return name;
    },
    setEmail(e) {
      email = e;
    },
    getEmail() {
      return email;
    },
  };
}

const user1 = createUser("Alice");
const user2 = createUser("Bob");

user1.setEmail("alice@ex.com");
user2.setEmail("bob@ex.com");

console.log(user1.getEmail()); // 'alice@ex.com'
console.log(user2.getEmail()); // 'bob@ex.com'

3. 函数柯里化

// 柯里化:转换多参数函数,重点在于闭包和递归
function curry(fn) {
  const arity = fn.length; // 函数形参个数

  return function curried(...args) {
    // fn的参数传够的时候,执行fn
    if (args.length >= arity) {
      return fn(...args);
    }
    // fn的参数没有传够,继续返回接受新参数的函数
    return (...nextArgs) => curried(...args, ...nextArgs);
  };
}

const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6

4. 模块模式

// ✅ 模块化:暴露部分功能,隐藏实现细节
const calculator = (() => {
  let result = 0; // 私有状态

  const add = (n) => (result += n);
  const subtract = (n) => (result -= n);
  const multiply = (n) => (result *= n);
  const getResult = () => result;

  // 只暴露需要的接口
  return { add, subtract, multiply, getResult };
})();

calculator.add(5);
calculator.multiply(2);
console.log(calculator.getResult()); // 10
console.log(calculator.result); // undefined(无法访问)

四、闭包的常见陷阱

❌ 陷阱 1:循环中的闭包

// ❌ 错误:所有函数都引用同一个 i
const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(() => console.log(i));
}

funcs[0](); // 3(不是 0)
funcs[1](); // 3(不是 1)
funcs[2](); // 3(不是 2)

// 原因:循环结束后 i = 3,所有函数都引用这个 i

为什么?

// 执行过程:
// i = 0: 创建函数,闭包保存 i 的引用
// i = 1: 创建函数,闭包保存 i 的引用(同一个 i)
// i = 2: 创建函数,闭包保存 i 的引用(同一个 i)
// i = 3: 循环结束
// 所有函数的 i 都已经是 3

const funcs = [];
for (var i = 0; i < 3; i++) {
  // 此时都指向同一个 i 变量
  funcs.push(() => console.log(i));
}

解决方案 1:用 let 创建块级作用域

// ✅ 最简单的解决
const funcs = [];
for (let i = 0; i < 3; i++) {
  // let 每次创建新的 i
  funcs.push(() => console.log(i));
}

funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2

解决方案 2:立即执行函数(IIFE)

// ✅ 创建独立作用域
const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(
    (
      (j) => () =>
        console.log(j)
    )(i),
  ); // 传参创建新作用域
}

funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2

解决方案 3:工厂函数

// ✅ 工厂函数
const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(createLogger(i));
}

function createLogger(value) {
  return () => console.log(value);
}

funcs[0](); // 0

❌ 陷阱 2:回调中的 this

// ❌ this 指向错误
const obj = {
  name: "obj",
  getName: function () {
    setTimeout(function () {
      // 回调函数的执行者是全局Scope,这里直接访问obj.name也行
      // 但不如箭头函数严谨
      console.log(this.name); // undefined(this 是 window)
    }, 100);
  },
};

obj.getName();

// ✅ 解决 1:箭头函数(继承外层 this)
const obj = {
  name: "obj",
  getName: function () {
    setTimeout(() => {
      console.log(this.name); // 'obj'
    }, 100);
  },
};

// ✅ 解决 2:保存 this
const obj = {
  name: "obj",
  getName: function () {
    const self = this; // 保存 this
    setTimeout(function () {
      console.log(self.name); // 'obj'
    }, 100);
  },
};

❌ 陷阱 3:意外的闭包导致内存泄漏

// ❌ 内存泄漏:大对象被意外引用
function setupListener() {
  const largeData = new Array(1000000).fill("data");

  document.getElementById("btn").addEventListener("click", () => {
    console.log(largeData[0]); // 闭包引用了整个大数组
  });
}

// largeData 永远不会被垃圾回收,因为监听器仍在运行

// ✅ 解决:显式清理
function setupListener() {
  const largeData = new Array(1000000).fill("data");

  const handler = () => {
    console.log(largeData[0]);
  };

  const btn = document.getElementById("btn");
  btn.addEventListener("click", handler);

  // 清理函数
  return () => {
    btn.removeEventListener("click", handler);
  };
}

const cleanup = setupListener();
// cleanup(); // 移除监听器,largeData 可以被垃圾回收

❌ 陷阱 4:过度的闭包复杂性

// ❌ 过度复杂,难以维护
function complex() {
  let a = 0;
  return {
    b: () => {
      return {
        c: () => {
          return {
            d: () => a++,
          };
        },
      };
    },
  };
}

complex().b().c().d(); // 可读性太差

// ✅ 简化
function simple() {
  let a = 0;
  return {
    increment: () => a++,
    get: () => a,
  };
}

simple().increment();

五、this 和作用域

this 的绑定规则

调用方式this 指向示例
直接调用undefined(严格)/ window(非严格)fn()
方法调用对象obj.method()
构造函数新创建的对象new Constructor()
apply/call/bind第一个参数fn.call(obj)
箭头函数外层的 this() => {}
const obj = {
  name: "obj",
  method() {
    console.log(this); // obj
  },
  arrowMethod: () => {
    console.log(this); // window(继承外层)
  },
};

obj.method(); // obj
obj.arrowMethod(); // window

this vs 作用域的区别

// ❌ 常见混淆
const user = {
  name: "Alice",
  info: {
    getName: function () {
      console.log(this.name); // undefined(this 是 info 对象)
      console.log(name); // undefined 或全局 name
    },
  },
};

user.info.getName();

// ✅ 理解区别
const user = {
  name: "Alice",
  getName: function () {
    console.log(this.name); // 'Alice'(this 是 user)

    return () => {
      console.log(this.name); // 'Alice'(继承外层 this)
      console.log(user.name); // 'Alice'(闭包引用外层变量)
    };
  },
};

const fn = user.getName();
fn(); // 'Alice' 'Alice'

六、10 个实战模式

1. 缓存与记忆化(Memoization)

// ✅ 缓存计算结果
function memoize(fn) {
  const cache = new Map();

  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const expensiveAdd = memoize((a, b) => {
  console.log("计算中...");
  return a + b;
});

console.log(expensiveAdd(1, 2)); // "计算中..." 3
console.log(expensiveAdd(1, 2)); // 3(从缓存)

2. 函数节流(Throttle)

// ✅ 限制函数执行频率
function throttle(fn, delay) {
  let lastCall = 0;

  return (...args) => {
    const now = Date.now();
    if (now - lastCall >= delay) {
      fn(...args);
      lastCall = now;
    }
  };
}

const throttledScroll = throttle(() => {
  console.log("滚动处理");
}, 1000);

window.addEventListener("scroll", throttledScroll); // 最多 1 秒触发一次

3. 防抖(Debounce)

// ✅ 延迟执行,避免频繁调用
function debounce(fn, delay) {
  let timeoutId;

  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      fn(...args);
    }, delay);
  };
}

const debouncedSearch = debounce((query) => {
  console.log("搜索:", query);
}, 500);

input.addEventListener("input", (e) => {
  debouncedSearch(e.target.value); // 500ms 无输入后执行
});

4. 部分应用(Partial Application)

// ✅ 固定部分参数
function partial(fn, ...partialArgs) {
  return (...restArgs) => {
    return fn(...partialArgs, ...restArgs);
  };
}

const add = (a, b, c) => a + b + c;
const add5 = partial(add, 5);

console.log(add5(10, 15)); // 30

5. 单例模式

// ✅ 确保只有一个实例
function createSingleton(Constructor) {
  let instance;

  return function (...args) {
    if (!instance) {
      instance = new Constructor(...args);
    }
    return instance;
  };
}

class Database {
  constructor(url) {
    this.url = url;
    console.log("连接到:", url);
  }
}

const getDB = createSingleton(Database);
const db1 = getDB("localhost");
const db2 = getDB("localhost");

console.log(db1 === db2); // true

6. 装饰器模式

// ✅ 增强函数功能
function withLogging(fn) {
  return (...args) => {
    console.log("调用:", fn.name, args);
    const result = fn(...args);
    console.log("返回:", result);
    return result;
  };
}

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);

loggedAdd(1, 2);
// 调用: add [1, 2]
// 返回: 3

7. 观察者模式

// ✅ 发布-订阅
function createEventEmitter() {
  const events = new Map();

  return {
    on(event, handler) {
      if (!events.has(event)) {
        events.set(event, []);
      }
      events.get(event).push(handler);
    },
    off(event, handler) {
      const handlers = events.get(event);
      if (handlers) {
        const index = handlers.indexOf(handler);
        if (index > -1) {
          handlers.splice(index, 1);
        }
      }
    },
    emit(event, ...args) {
      const handlers = events.get(event) || [];
      handlers.forEach((handler) => handler(...args));
    },
  };
}

const emitter = createEventEmitter();
emitter.on("user:login", (user) => {
  console.log("欢迎:", user);
});
emitter.emit("user:login", "Alice"); // "欢迎: Alice"

8. 职责链模式

// ✅ 链式处理请求
function createHandler(condition, action, nextHandler) {
  return (request) => {
    if (condition(request)) {
      return action(request);
    }
    return nextHandler ? nextHandler(request) : null;
  };
}

const isVIP = (user) => user.vip;
const isAdmin = (user) => user.role === "admin";

const vipHandler = createHandler(isVIP, (user) => "优先处理", null);

const adminHandler = createHandler(isAdmin, (user) => "管理员处理", vipHandler);

const defaultHandler = createHandler(
  () => true,
  (user) => "普通处理",
  adminHandler,
);

console.log(defaultHandler({ vip: false, role: "user" })); // "普通处理"
console.log(defaultHandler({ vip: true, role: "user" })); // "优先处理"

9. 惰性初始化(Lazy Initialization)

// ✅ 延迟加载重型资源
function lazy(fn) {
  let result;
  let computed = false;

  return () => {
    if (!computed) {
      result = fn();
      computed = true;
    }
    return result;
  };
}

const getHeavyData = lazy(() => {
  console.log("加载数据...");
  return new Array(1000000).fill("data");
});

console.log("定义函数");
getHeavyData(); // "加载数据..."
getHeavyData(); // (无输出,直接返回)

10. 组合模式

// ✅ 组合多个小函数
function compose(...fns) {
  return (x) => fns.reduceRight((acc, fn) => fn(acc), x);
}

const add1 = (x) => x + 1;
const multiply2 = (x) => x * 2;
const square = (x) => x * x;

const pipeline = compose(square, multiply2, add1);
console.log(pipeline(3)); // ((3 + 1) * 2) ^ 2 = 64

七、闭包与内存管理

何时会导致内存泄漏?

// ❌ 泄漏 1:意外的全局变量
function create() {
  largeArray = new Array(1000000); // 没有 var/let,成为全局变量
}

create();
// largeArray 永远不会被垃圾回收

// ✅ 正确
function create() {
  const largeArray = new Array(1000000);
}
// largeArray 随着函数执行完毕而被回收
// ❌ 泄漏 2:未清理的事件监听
const elements = document.querySelectorAll(".item");
elements.forEach((el) => {
  el.addEventListener("click", () => {
    console.log(el.textContent);
  });
});

// 即使删除 DOM,监听器仍引用元素

// ✅ 正确:显式清理
elements.forEach((el) => {
  const handler = () => console.log(el.textContent);
  el.addEventListener("click", handler);

  el.cleanup = () => {
    el.removeEventListener("click", handler);
  };
});

// 使用完后
elements.forEach((el) => el.cleanup?.());
// ❌ 泄漏 3:循环引用
const obj1 = { name: "obj1" };
const obj2 = { name: "obj2" };

obj1.ref = obj2;
obj2.ref = obj1;

// 即使没有外部引用,也无法被垃圾回收

检测内存泄漏

// 简单的内存监测
function monitorMemory(fn) {
  if (performance.memory) {
    const before = performance.memory.usedJSHeapSize;
    fn();
    const after = performance.memory.usedJSHeapSize;
    console.log(`内存增长: ${(after - before) / 1024 / 1024} MB`);
  }
}

monitorMemory(() => {
  const arr = new Array(1000000).fill("data");
});

八、常见错误与陷阱总结

❌ 错误 1:闭包中修改外层变量

// ❌ 陷阱
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    i++; // 修改外层变量
    console.log(i);
  }, 0);
}
// 输出:1, 2, 3(而不是 0, 1, 2)

// ✅ 使用 let
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 0);
}
// 输出:0, 1, 2

❌ 错误 2:误以为每个闭包都是独立的

// ❌ 错误假设
const handlers = [];
for (let i = 0; i < 3; i++) {
  handlers.push(() => i); // 所有都引用同一个 i
}

console.log(handlers[0]()); // 3(当前 i)
// ❌ 不是 0

❌ 错误 3:忽视性能成本

// ❌ 过度使用闭包
function processLarge(items) {
  const cache = {};

  return items.map((item) => {
    return () => {
      // 每个闭包都保存 cache
      return cache[item.id];
    };
  });
}

// ✅ 只保存必要的数据
function processLarge(items) {
  return items.map((item) => {
    const id = item.id;
    return () => id; // 只保存 id
  });
}

❌ 错误 4:作用域链过长

// ❌ 作用域链太深,查找性能差
function a() {
  const x = 0;
  return function b() {
    return function c() {
      return function d() {
        return function e() {
          return x; // 需要向上查找 4 层
        };
      };
    };
  };
}

// ✅ 尽量扁平
function a() {
  const x = 0;
  return () => x; // 直接访问
}

九、最佳实践

1. 优先用 let/const,避免 var

// ✅ 现代标准
const config = {
  /* ... */
};
let counter = 0;

// ❌ 过时
var oldVar = "deprecated";

2. 闭包最小化

// ❌ 暴露太多
function bad() {
  const state = { a: 1, b: 2, c: 3 };
  return () => state; // 暴露整个对象
}

// ✅ 只暴露必要的
function good() {
  const a = 1;
  return () => a; // 只暴露 a
}

3. 及时清理

// ✅ 显式清理引用
function setup() {
  const data = heavyData();

  const handler = () => process(data);
  element.addEventListener("click", handler);

  return () => {
    element.removeEventListener("click", handler);
    data = null; // 清理
  };
}

4. 避免闭包中的 this 问题

// ✅ 用箭头函数
class MyClass {
  callback = () => {
    console.log(this.name);
  };
}

// ✅ 或显式保存 this
class MyClass {
  setup() {
    const self = this;
    handler.addEventListener("click", () => {
      self.handle();
    });
  }
}

5. 选择合适的模式

// ✅ 根据需求选择
// 需要私有变量 → 模块模式
// 需要延迟计算 → 惰性初始化
// 需要组合逻辑 → 高阶函数
// 需要事件系统 → 观察者模式

十、速查表

作用域类型速查

作用域创建方式访问范围何时销毁
全局全局代码所有地方程序结束
函数函数声明函数内函数返回
块级{}块内块结束
动态函数调用调用位置调用结束

闭包检查清单

// 判断是否是闭包:
// ✅ 有内层函数
// ✅ 内层函数引用外层变量
// ✅ 外层函数已执行完毕
// ✅ 内层函数仍可访问外层变量

常见模式速查

模式用途关键字
缓存避免重复计算Mapcache
节流限制执行频率lastCalldelay
防抖延迟执行clearTimeout
模块数据封装IIFEreturn
工厂创建对象functionreturn
单例唯一实例instance
装饰增强函数wrapper
观察发布订阅eventsMap

十一、性能优化建议

减少闭包的性能影响

// ❌ 每次创建闭包
const arr = [1, 2, 3];
const processors = arr.map((item) => {
  return () => process(item); // 每个都是新闭包
});

// ✅ 共享闭包
const process = (item) => {
  /* ... */
};
const processors = arr.map((item) => process.bind(null, item));

及时释放大对象

// ✅ 使用后及时置 null
function loadData() {
  const largeData = fetch("/api/data");

  setTimeout(() => {
    console.log(largeData);
    largeData = null; // 释放
  }, 100);
}

避免过深的作用域链

// ❌ 查找链太长
const result = deep.scope.chain.to.variable.x;

// ✅ 缓存中间结果
const chain = deep.scope.chain.to.variable;
const result = chain.x;

十二、总结

核心概念

  1. 作用域 = 变量的可访问范围
  2. 作用域链 = 单向向上查找的机制
  3. 闭包 = 函数能记住外层变量
  4. this = 动态指向,不是作用域

关键点

  • ✅ 用 let/const 创建块级作用域
  • ✅ 利用闭包实现数据封装
  • ✅ 避免循环中的闭包陷阱
  • ✅ 及时清理事件监听器
  • ✅ 注意 this 的绑定问题

常见错误 Top 5

  1. 循环中的闭包问题(用 let
  2. 忘记清理事件监听器(内存泄漏)
  3. this 指向混淆(用箭头函数或保存)
  4. 过度使用闭包(性能问题)
  5. 作用域链过深(查找性能差)

学习顺序

  1. 必学:作用域链、闭包基础
  2. 重要:常见陷阱、this 绑定
  3. 实战:10 个设计模式
  4. 📚 深入:性能优化、垃圾回收