Published on

闭包

Authors

这篇我们来关注闭包

什么是闭包?

闭包是由函数和它所在词法环境组合而成的实体,这个环境包含了闭包创建时,所能访问的所有局部变量。所以当外部函数已经执行完后,依然可以通过闭包(内部函数)访问到当时词法环境的变量。

如何创建闭包:

  1. 存在函数嵌套(在外部函数中,再定义一个内部函数)
  2. 内部函数使用了外部函数中的变量
  3. 内部函数被导出,包括三种场景:(1)直接将内部函数作为返回值;(2)作为参数传递给其它变量;(3)被赋予给一个全局变量或对象的属性
// 一个最简单的闭包示例
function createCounter() {
  let count = 0
  function innerFunction(val) {
    count = count + val
    return count
  }
  return innerFunction
}
const counter = createCounter()
counter(1) // 1
counter(2) // 3

闭包有什么用?

  1. 创建私有变量,实现数据封装。即在函数外部无法访问和修改函数内部定义的变量值,只能通过闭包提供的方法操作数据,有利于代码的安全性和模块化
// 示例
function counterFunction() {
  let count = 0

  return {
    plus(v) {
      count = count + v
      return count
    },
    getCount(v) {
      return count
    },
  }
}

const counter = counterFunction()
console.log('counter().getCount()', counter.getCount()) // 0
console.log('counter().getCount()', counter.plus(1)) // 1
console.log('counter().getCount()', counter.getCount()) // 1
console.log('counter().getCount()', counter.plus(2)) // 3
console.log('counter().getCount()', counter.getCount()) // 3
  1. 因为内部函数(闭包)持有对这些变量的引用,导致它们无法被垃圾回收机制回收,从而“延长”了其生命周期,对于需要记住上一次操作状态的场景非常有用,例如计数器,缓存函数
// 1. 计数器--持久化的计数状态
// 1.1 工厂函数的实现
function createCounter() {
  let count = 0

  return {
    plus: (v) => {
      count = count + v
      return count
    },
    getData: () => {
      return count
    },
  }
}

const counterA = createCounter()
console.log('getData', counterA.getData())
console.log('plus', counterA.plus(1))
console.log('getData', counterA.getData())
console.log('plus', counterA.plus(2))
console.log('getData', counterA.getData())

// 1.2 立即执行函数
const counter = (() => {
  let count = 0
  return function () {
    count = count + 1
    return count
  }
})()

console.log(counter()) // 输出: 1
console.log(counter()) // 输出: 2
// 缓存函数
function memoize(fn) {
  const cache = {} // 缓存对象,被闭包“记住”
  return function (...args) {
    // 将参数序列化后作为缓存键
    const key = JSON.stringify(args)
    // 如果缓存中存在该键,直接返回缓存结果
    if (cache[key] !== undefined) {
      console.log('从缓存读取结果')
      return cache[key]
    }
    // 否则,计算新结果并存入缓存
    console.log('执行计算过程')
    const result = fn.apply(this, args)
    cache[key] = result
    return result
  }
}

function memoizeWithMap(fn) {
  const cache = new Map() // 使用 Map 作为缓存容器
  return function (...args) {
    // 直接将参数数组作为键
    if (cache.has(args)) {
      return cache.get(args)
    }
    const result = fn.apply(this, args)
    cache.set(args, result)
    return result
  }
}
  1. 解决异步编程中,避免因变量共享导致的问题。例如在循环中处理异步事件(如setTimeout),使用 var 声明变量时,会出现打印值始终是最后一次的值的问题。当使用 let 或 立即执行函数,在每一次循环都创建独立的作用域后,就可以解决这个问题
// 原始问题
for (var i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i) // 1s 后,输出 10 次 10
  }, 1000)
}

// 利用 let 的块级作用域特性
for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i) // 1s 后,输出 0~9
  }, 1000)
}

// 利用立即执行函数,会创建独立的函数作用域
for (var i = 0; i < 10; i++) {
  ;((num) => {
    setTimeout(() => {
      console.log(num) // 1s 后,输出 0~9
    }, 1000)
  })(i)
}

最佳实践是什么?

  1. 警惕内存泄露:在闭包使用完成后,需要手动解除引用,将引用的值指向 null
  2. 在事件监听,定时器场景下,也要注意及时清理
  3. 闭包捕获的是变量的引用,所以如果在闭包创建后,执行之前值发生了修改,最后闭包访问到的会是最新的值。
// 最后闭包访问到的是最新值(如果在闭包创建后,执行之前发生了变化的话)
function createFunctions() {
  const functions = []
  for (var i = 0; i < 3; i++) {
    // 闭包(箭头函数)捕获的是变量 i 的引用
    functions.push(() => {
      console.log(num)
    })
  }
  return functions // 返回一个包含三个函数的数组
}

// 创建闭包时,循环中的 i 被捕获的是引用
const funcArray = createFunctions()

// 此时循环早已结束,变量 i 的最终值是 3
// console.log("循环结束后 i 的值为:", i); // 输出: 循环结束后 i 的值为: 3

// 执行闭包函数,访问的是同一个 i 的当前值
funcArray[0]() // 输出: 3
funcArray[1]() // 输出: 3
funcArray[2]() // 输出: 3

// 如何解决这一问题
// 1. 换用 let
// 2. 使用立即执行函数

扩展内容

  1. 立即执行函数(IIFE)与闭包的关系​:IIFE是如何通过创建独立作用域来解决循环中的变量共享问题的?
  2. let与闭包的协同作用​:为什么使用let可以避免var的变量提升和函数级作用域问题,从而减少对闭包的依赖。
  3. 现代JavaScript中的闭包应用​:闭包在React Hooks、模块化开发等现代框架中的实际应用。
  4. 引申到内存泄露的排查方法有哪些