返回首页

JS 闭包:词法环境和示例

JavaScript 中的闭包在外层函数执行完毕后,内层函数仍能访问外层变量。该材料涵盖从兰姆达演算的理论、循环中的实践、OOP 仿真以及引擎优化。中高级开发者的代码示例。

JS 中闭包如何工作:代码和循环陷阱
Advertisement 728x90

JavaScript 闭包:词法环境机制与实际应用

JavaScript 中的闭包是一个函数与其词法环境的组合,其中内部函数在外部函数执行完成后仍能访问外部作用域的变量。这使得在异步上下文中实现状态封装和可预测行为成为可能。

这一概念源于阿隆佐·丘奇的 lambda 演算。高阶函数返回或接受其他函数,捕获自由变量——即内部使用但外部声明的变量。

一个基本的计数器示例展示了其原理:

Google AdInline article slot
function counter() {
  let count = 0
  return function(step) {
    count = count + step
    return count
  }
}

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

内部函数闭包了 count,防止其被垃圾回收。这是一个实时引用:更改会实时反映。

闭包作为面向对象编程的类比

闭包模拟了对象的私有字段和公共 API。一个带有方法的计数器:

function Counter(initialValue = 0) {
  let count = initialValue
  return {
    increment: function(step = 1) {
      return count += step
    },
    decrement: function(step = 1) {
      return count -= step
    },
    getValue: function() {
      return count
    },
    reset: function() {
      return count = initialValue
    }
  }
}

const myCounter = Counter(10)
console.log(myCounter.getValue()) // 10
console.log(myCounter.increment(5)) // 15

使用带有私有字段(#count)的类实现等价逻辑。在 Pinia/Vuex 存储中,闭包封装了响应式状态:

Google AdInline article slot
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const initValue = 0
  function increment(step) {
    count.value += step
  }
  return { count, increment }
})

循环和迭代器中的细微差别

闭包在循环中的行为取决于变量提升和块作用域。

使用 var——所有迭代共享一个变量:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i) // 3, 3, 3
  }, 100)
}

使用 let——每次迭代创建一个独立的词法环境:

Google AdInline article slot
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i) // 0, 1, 2
  }, 100)
}

迭代器如 forEachmapfilter 自动创建闭包:

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

实践:回调和事件

在异步代码中,闭包解决了上下文问题。

带有唯一 ID 的 Ajax 请求:

function ajaxRequest(url) {
  let requestId = Math.random().toString(36).substring(2)
  fetch(url).then(data => {
    console.log(`请求 ${requestId} 完成:`, data)
  }).catch(error => {
    console.log(`请求 ${requestId} 失败:`, error.message)
  })  
}

事件处理程序:

function clickSetup(elemId, message) {
  const elem = document.getElementById(elemId)
  elem.addEventListener('click', function() {
    console.log(message)
  })
}

箭头函数与常规函数工作方式相同。

优化和调试

JS 引擎(如 V8)从闭包中排除未使用的变量以节省内存。在 DevTools(作用域 → 闭包)中,仅显示相关变量。

关键实践:

  • 在循环中使用 let/const 实现块作用域。
  • 优先使用闭包而非类来实现私有状态。
  • 在异步回调中,尽早固定上下文。
  • 监控长生命周期闭包的内存使用。

关键要点

  • 闭包 = 函数 + 词法环境,包含对变量的实时引用。
  • 循环中的 let 创建每次迭代的闭包,var 则不会。
  • 模拟面向对象编程的私有字段,用于 Vuex/Pinia 存储。
  • 引擎优化:未使用的变量被排除。
  • 对异步回调和事件至关重要。

— Editorial Team

Advertisement 728x90

继续阅读