Skip to content

JavaScript 闭包

什么是闭包

闭包(Closure)是指一个函数能够访问并操作其外部作用域中变量的函数。即使外部函数已经执行完毕,内部函数仍然可以访问外部函数的变量。

闭包的基本概念

1. 作用域

在理解闭包之前,我们需要先了解作用域的概念:

  • 全局作用域:在整个程序中都可以访问的变量
  • 局部作用域:只能在函数内部访问的变量
  • 块级作用域:ES6 引入的,只能在块(如 {})内部访问的变量(使用 letconst 声明)

2. 词法作用域

JavaScript 采用词法作用域(Lexical Scoping),即函数的作用域在函数定义时就已经确定,而不是在函数调用时确定。

3. 闭包的形成

闭包的形成需要满足以下条件:

  1. 存在嵌套函数(内部函数)
  2. 内部函数引用了外部函数的变量
  3. 内部函数在外部函数执行完毕后仍然可以被访问

闭包的示例

1. 基本示例

javascript
// 基本闭包示例
function outerFunction() {
  let outerVariable = "我是外部变量";

  function innerFunction() {
    console.log(outerVariable); // 内部函数访问外部变量
  }

  return innerFunction;
}

const closure = outerFunction(); // outerFunction 执行完毕,但 innerFunction 仍然可以访问 outerVariable
closure(); // 输出: 我是外部变量

2. 计数器示例

javascript
// 计数器示例
function createCounter() {
  let count = 0;

  return {
    increment: function () {
      count++;
      return count;
    },
    decrement: function () {
      count--;
      return count;
    },
    getCount: function () {
      return count;
    },
  };
}

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

3. 模块化示例

javascript
// 模块化示例
const module = (function () {
  // 私有变量
  let privateVariable = "私有变量";

  // 私有方法
  function privateMethod() {
    console.log("这是私有方法");
  }

  // 公共接口
  return {
    publicVariable: "公共变量",
    publicMethod: function () {
      console.log("这是公共方法");
      console.log("访问私有变量:", privateVariable);
      privateMethod();
    },
  };
})();

console.log(module.publicVariable); // 公共变量
module.publicMethod(); // 这是公共方法, 访问私有变量: 私有变量, 这是私有方法
console.log(module.privateVariable); // undefined (无法访问私有变量)
module.privateMethod(); // TypeError: module.privateMethod is not a function (无法访问私有方法)

4. 回调函数中的闭包

javascript
// 回调函数中的闭包
function fetchData(url, callback) {
  // 模拟网络请求
  setTimeout(() => {
    const data = `从 ${url} 获取的数据`;
    callback(data);
  }, 1000);
}

function processData() {
  let processingId = 123;

  fetchData("https://api.example.com/data", function (data) {
    console.log("处理数据:", data);
    console.log("处理 ID:", processingId); // 闭包访问外部变量
  });
}

processData(); // 1秒后输出: 处理数据: 从 https://api.example.com/data 获取的数据, 处理 ID: 123

5. 事件监听器中的闭包

javascript
// 事件监听器中的闭包
function setupButton() {
  let clickCount = 0;

  const button = document.getElementById("myButton");

  button.addEventListener("click", function () {
    clickCount++;
    console.log("按钮点击次数:", clickCount); // 闭包访问外部变量
  });
}

setupButton(); // 每次点击按钮都会递增 clickCount

6. 函数工厂

javascript
// 函数工厂
function createMultiplier(multiplier) {
  return function (number) {
    return number * multiplier; // 闭包访问外部变量 multiplier
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

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

7. 延迟执行

javascript
// 延迟执行
function createDelayedFunction(message, delay) {
  return function () {
    setTimeout(() => {
      console.log(message); // 闭包访问外部变量
    }, delay);
  };
}

const sayHello = createDelayedFunction("Hello!", 2000);
const sayGoodbye = createDelayedFunction("Goodbye!", 1000);

sayHello(); // 2秒后输出: Hello!
sayGoodbye(); // 1秒后输出: Goodbye!

闭包的工作原理

1. 执行上下文

当函数执行时,会创建一个执行上下文(Execution Context),包含:

  • 变量对象(Variable Object):存储函数的变量、参数和函数声明
  • 作用域链(Scope Chain):用于查找变量的链式结构
  • this 指向

2. 作用域链

作用域链是由当前执行上下文的变量对象和所有外部执行上下文的变量对象组成的链式结构。当查找变量时,JavaScript 引擎会从当前执行上下文的变量对象开始查找,如果找不到,就沿着作用域链向上查找,直到找到变量或到达全局执行上下文。

3. 闭包的内存结构

当外部函数执行完毕后,其执行上下文会被销毁,但是由于内部函数仍然引用着外部函数的变量对象,所以外部函数的变量对象不会被垃圾回收机制回收,从而形成了闭包。

javascript
// 闭包的内存结构示例
function outer() {
  let outerVar = "outer";

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

  return inner;
}

const closure = outer();
/*
当 outer() 执行完毕后:
1. outer 的执行上下文被销毁
2. 但 outer 的变量对象仍然存在于内存中
3. 因为 inner 函数的作用域链中仍然引用着 outer 的变量对象
4. 当 closure() 被调用时,inner 函数会通过作用域链访问 outer 的变量对象
*/

闭包的应用场景

1. 数据私有化

闭包可以用于创建私有变量和方法,实现数据的封装和保护。

javascript
// 数据私有化
const person = (function () {
  // 私有变量
  let name = "John";
  let age = 30;

  // 私有方法
  function validateAge(newAge) {
    return newAge >= 0 && newAge <= 150;
  }

  // 公共接口
  return {
    getName: function () {
      return name;
    },
    setName: function (newName) {
      name = newName;
    },
    getAge: function () {
      return age;
    },
    setAge: function (newAge) {
      if (validateAge(newAge)) {
        age = newAge;
      } else {
        console.error("无效的年龄");
      }
    },
  };
})();

console.log(person.getName()); // John
person.setName("Jane");
console.log(person.getName()); // Jane

console.log(person.getAge()); // 30
person.setAge(35);
console.log(person.getAge()); // 35

person.setAge(200); // 无效的年龄
console.log(person.getAge()); // 35

// 无法直接访问私有变量
console.log(person.name); // undefined
console.log(person.age); // undefined

2. 函数工厂

闭包可以用于创建具有特定配置的函数。

javascript
// 函数工厂
function createLogger(prefix) {
  return function (message) {
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] [${prefix}] ${message}`);
  };
}

const errorLogger = createLogger("ERROR");
const infoLogger = createLogger("INFO");
const debugLogger = createLogger("DEBUG");

errorLogger("系统错误"); // [2023-12-01T00:00:00.000Z] [ERROR] 系统错误
infoLogger("用户登录"); // [2023-12-01T00:00:00.000Z] [INFO] 用户登录
debugLogger("调试信息"); // [2023-12-01T00:00:00.000Z] [DEBUG] 调试信息

3. 缓存

闭包可以用于创建缓存,避免重复计算。

javascript
// 缓存示例
function createCache() {
  const cache = {};

  return {
    get: function (key) {
      return cache[key];
    },
    set: function (key, value) {
      cache[key] = value;
    },
    has: function (key) {
      return key in cache;
    },
    clear: function () {
      Object.keys(cache).forEach((key) => delete cache[key]);
    },
  };
}

const fibCache = createCache();

function fibonacci(n) {
  if (n <= 1) return n;

  if (fibCache.has(n)) {
    console.log(`从缓存中获取 fib(${n})`);
    return fibCache.get(n);
  }

  const result = fibonacci(n - 1) + fibonacci(n - 2);
  fibCache.set(n, result);
  return result;
}

console.log(fibonacci(10)); // 55
console.log(fibonacci(10)); // 从缓存中获取 fib(10), 55
console.log(fibonacci(15)); // 610
console.log(fibonacci(15)); // 从缓存中获取 fib(15), 610

4. 事件处理

闭包可以用于在事件处理函数中访问外部变量。

javascript
// 事件处理示例
function setupEventHandlers() {
  const items = ["item1", "item2", "item3"];

  items.forEach((item, index) => {
    const button = document.createElement("button");
    button.textContent = `点击我 ${item}`;
    document.body.appendChild(button);

    button.addEventListener("click", function () {
      console.log(`点击了 ${item},索引为 ${index}`); // 闭包访问外部变量 item 和 index
    });
  });
}

setupEventHandlers(); // 每个按钮点击时都会输出对应的 item 和 index

5. 模块模式

闭包是模块模式的基础,用于创建模块化的代码。

javascript
// 模块模式
const calculator = (function () {
  // 私有变量
  let result = 0;

  // 私有方法
  function validateNumber(num) {
    return typeof num === "number" && !isNaN(num);
  }

  // 公共接口
  return {
    add: function (num) {
      if (validateNumber(num)) {
        result += num;
        return result;
      }
      throw new Error("请输入有效的数字");
    },
    subtract: function (num) {
      if (validateNumber(num)) {
        result -= num;
        return result;
      }
      throw new Error("请输入有效的数字");
    },
    multiply: function (num) {
      if (validateNumber(num)) {
        result *= num;
        return result;
      }
      throw new Error("请输入有效的数字");
    },
    divide: function (num) {
      if (validateNumber(num)) {
        if (num === 0) {
          throw new Error("除数不能为 0");
        }
        result /= num;
        return result;
      }
      throw new Error("请输入有效的数字");
    },
    clear: function () {
      result = 0;
      return result;
    },
    getResult: function () {
      return result;
    },
  };
})();

console.log(calculator.add(5)); // 5
console.log(calculator.subtract(2)); // 3
console.log(calculator.multiply(4)); // 12
console.log(calculator.divide(2)); // 6
console.log(calculator.clear()); // 0

6. 柯里化

闭包是柯里化(Currying)的基础,柯里化是将一个接受多个参数的函数转换为一系列接受单个参数的函数的过程。

javascript
// 柯里化示例
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return function (...nextArgs) {
      return curried.apply(this, [...args, ...nextArgs]);
    };
  };
}

// 普通函数
function add(a, b, c) {
  return 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
console.log(curriedAdd(1, 2, 3)); // 6

闭包的优缺点

优点

  1. 数据私有化:可以创建私有变量和方法,实现数据的封装和保护
  2. 函数工厂:可以创建具有特定配置的函数
  3. 缓存:可以用于缓存数据,提高性能
  4. 模块化:是模块模式的基础,用于创建模块化的代码
  5. 事件处理:在事件处理函数中访问外部变量

缺点

  1. 内存泄漏:闭包会引用外部函数的变量对象,导致这些变量无法被垃圾回收,可能会造成内存泄漏
  2. 性能影响:闭包的使用会增加内存使用,可能会影响性能
  3. 代码复杂度:过度使用闭包会增加代码的复杂度,降低可读性

闭包的内存管理

1. 内存泄漏的原因

当闭包引用了外部函数的变量,而这些变量又引用了大量的内存(如 DOM 元素),如果闭包没有被正确释放,就会导致内存泄漏。

2. 避免内存泄漏的方法

  • 及时释放闭包:当不再需要闭包时,将其设置为 null
  • 减少闭包中引用的变量:只引用必要的变量
  • 避免循环引用:避免闭包引用的对象又引用闭包本身
  • 使用弱引用:在支持的环境中使用 WeakMap 或 WeakSet 存储引用
javascript
// 避免内存泄漏的示例
function setupEventHandler() {
  const element = document.getElementById("myElement");

  function handleClick() {
    console.log("点击了元素");
  }

  element.addEventListener("click", handleClick);

  // 当不再需要事件监听器时,移除它
  return function cleanup() {
    element.removeEventListener("click", handleClick);
    // 释放引用
    element = null;
  };
}

const cleanup = setupEventHandler();
// 当不再需要时
cleanup();

闭包的常见问题

1. 循环中的闭包问题

在 ES5 之前,使用 var 声明变量时,在循环中创建的闭包会共享同一个变量,导致所有闭包都引用相同的值。

javascript
// 循环中的闭包问题(ES5)
function createFunctions() {
  var functions = [];

  for (var i = 0; i < 3; i++) {
    functions.push(function () {
      console.log(i); // 所有函数都引用同一个 i
    });
  }

  return functions;
}

const funcs = createFunctions();
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3
// 原因:var 声明的变量是函数作用域,在循环结束后 i 的值为 3,所有闭包都引用这个值

2. 解决方案

  • 使用立即执行函数表达式(IIFE):为每个循环创建一个新的作用域
  • 使用 let 声明变量let 声明的变量是块级作用域,在每次循环中都会创建一个新的变量
javascript
// 解决方案 1:使用 IIFE
function createFunctions() {
  var functions = [];

  for (var i = 0; i < 3; i++) {
    functions.push(
      (function (j) {
        return function () {
          console.log(j); // 每个函数引用不同的 j
        };
      })(i)
    );
  }

  return functions;
}

const funcs1 = createFunctions();
funcs1[0](); // 0
funcs1[1](); // 1
funcs1[2](); // 2

// 解决方案 2:使用 let
function createFunctions() {
  var functions = [];

  for (let i = 0; i < 3; i++) {
    functions.push(function () {
      console.log(i); // 每个函数引用不同的 i
    });
  }

  return functions;
}

const funcs2 = createFunctions();
funcs2[0](); // 0
funcs2[1](); // 1
funcs2[2](); // 2

3. 闭包中的 this 指向问题

在闭包中,this 的指向可能会与预期不符,因为闭包中的 this 通常指向全局对象(在非严格模式下)或 undefined(在严格模式下)。

javascript
// 闭包中的 this 指向问题
const obj = {
  value: 42,
  getValue: function () {
    function inner() {
      console.log(this.value); // this 指向全局对象,不是 obj
    }
    inner();
  },
};

obj.getValue(); // undefined (在浏览器中) 或 TypeError (在严格模式下)

4. 解决方案

  • 使用箭头函数:箭头函数没有自己的 this,它会捕获外层作用域的 this
  • 使用变量保存 this:在外部函数中使用变量保存 this,然后在闭包中使用这个变量
javascript
// 解决方案 1:使用箭头函数
const obj1 = {
  value: 42,
  getValue: function () {
    const inner = () => {
      console.log(this.value); // 箭头函数捕获外层作用域的 this
    };
    inner();
  },
};

obj1.getValue(); // 42

// 解决方案 2:使用变量保存 this
const obj2 = {
  value: 42,
  getValue: function () {
    const self = this; // 保存 this
    function inner() {
      console.log(self.value); // 使用保存的 this
    }
    inner();
  },
};

obj2.getValue(); // 42

闭包的最佳实践

1. 合理使用闭包

  • 只在必要时使用闭包:闭包虽然强大,但也会增加内存使用和代码复杂度
  • 保持闭包简洁:闭包应该只包含必要的逻辑
  • 避免过度嵌套:过度嵌套的闭包会降低代码的可读性

2. 内存管理

  • 及时释放闭包:当不再需要闭包时,将其设置为 null
  • 减少闭包中引用的变量:只引用必要的变量
  • 避免循环引用:避免闭包引用的对象又引用闭包本身

3. 代码风格

  • 命名规范:给闭包函数起有意义的名字
  • 注释:为复杂的闭包添加注释,说明其用途和工作原理
  • 模块化:使用模块模式组织代码,提高代码的可维护性

4. 性能优化

  • 缓存频繁使用的变量:在闭包中缓存频繁使用的变量,减少查找次数
  • 避免在闭包中创建大型对象:大型对象会增加内存使用
  • 使用防抖和节流:对于频繁触发的事件处理函数,使用防抖和节流优化性能

闭包的面试问题

1. 什么是闭包?

答案:闭包是指一个函数能够访问并操作其外部作用域中变量的函数。即使外部函数已经执行完毕,内部函数仍然可以访问外部函数的变量。

2. 闭包的工作原理是什么?

答案:当外部函数执行时,会创建一个执行上下文,包含变量对象、作用域链和 this 指向。当外部函数执行完毕后,其执行上下文会被销毁,但是由于内部函数仍然引用着外部函数的变量对象,所以外部函数的变量对象不会被垃圾回收机制回收,从而形成了闭包。当内部函数被调用时,它会通过作用域链访问外部函数的变量对象。

3. 闭包有哪些应用场景?

答案:闭包的应用场景包括:

  • 数据私有化
  • 函数工厂
  • 缓存
  • 事件处理
  • 模块模式
  • 柯里化

4. 闭包可能会导致什么问题?如何避免?

答案:闭包可能会导致内存泄漏,因为闭包会引用外部函数的变量对象,导致这些变量无法被垃圾回收。避免内存泄漏的方法包括:

  • 及时释放闭包
  • 减少闭包中引用的变量
  • 避免循环引用
  • 使用弱引用

5. 解释下面代码的输出结果

javascript
function outer() {
  var i = 0;

  function inner() {
    i++;
    console.log(i);
  }

  return inner;
}

var f1 = outer();
var f2 = outer();
f1(); // 1
f1(); // 2
f2(); // 1

答案:输出结果是 1, 2, 1。因为 f1f2 是两个不同的闭包,它们分别引用着不同的外部函数执行上下文的变量对象,所以它们的 i 是独立的。当调用 f1() 时,i 递增为 1,再次调用 f1() 时,i 递增为 2。当调用 f2() 时,它引用的 i 初始值为 0,递增为 1。

6. 如何使用闭包创建一个计数器?

答案

javascript
function createCounter() {
  let count = 0;

  return {
    increment: function () {
      count++;
      return count;
    },
    decrement: function () {
      count--;
      return count;
    },
    getCount: function () {
      return count;
    },
  };
}

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

总结

闭包是 JavaScript 中一个强大的特性,它允许函数访问并操作其外部作用域中的变量,即使外部函数已经执行完毕。闭包的形成需要满足三个条件:存在嵌套函数、内部函数引用了外部函数的变量、内部函数在外部函数执行完毕后仍然可以被访问。

闭包的主要应用场景包括:数据私有化、函数工厂、缓存、事件处理、模块模式和柯里化。然而,闭包也可能会导致内存泄漏,因为它会引用外部函数的变量对象,导致这些变量无法被垃圾回收。

为了合理使用闭包,我们应该:

  • 只在必要时使用闭包
  • 及时释放闭包
  • 减少闭包中引用的变量
  • 避免循环引用
  • 保持闭包简洁
  • 给闭包函数起有意义的名字
  • 为复杂的闭包添加注释

通过理解和合理使用闭包,我们可以编写更加灵活、模块化和高效的 JavaScript 代码。