JavaScript 闭包:从原理到实践

JavaScript 闭包:从原理到实践

什么是闭包?

闭包(Closure)是 JavaScript 中一个核心概念,指的是一个函数能够访问并记住其词法作用域中的变量,即使该函数在其词法作用域之外执行。简单来说,闭包让函数可以"记住"它被创建时的环境。

闭包的核心特征:

  1. 函数嵌套:内部函数定义在外部函数内部
  2. 变量访问:内部函数访问外部函数的变量
  3. 外部执行:内部函数在外部函数执行完成后仍被使用

闭包的作用域链原理

JavaScript 的作用域链决定了变量访问的顺序:

  • 当前作用域 → 外部作用域 → 全局作用域
  • 作用域链关系由 outer 指针维护
javascript 复制代码
// 文档4示例:作用域链解析
var num = 10
function a() {
  function b() {
    var num = 20
    c() // 输出10,而非20!
  }
  function c() {
    console.log(num) // 访问a作用域的num
  }
  b()
}
a()

关键点解析

  1. c() 定义在 a() 中,其作用域链是:c → a → 全局
  2. c() 执行时,先在自身作用域查找 num(未找到)
  3. 通过作用域链向上在 a() 的作用域查找(未找到)
  4. 最后在全局作用域找到 num=10
  5. b() 中的 num=20 在另一个作用域,不在 c() 的访问链上

闭包的实际应用场景

1. 创建私有变量(模块模式)

javascript 复制代码
// 闭包封装实现私有变量
function createCounter() {
  let count = 0 // 私有变量
  
  return {
    increment: function() {
      count++
      return count
    },
    get: function() {
      return count
    }
  }
}

const counter = createCounter()
console.log(counter.increment()) // 1
console.log(counter.get())      // 1
console.log(counter.count)      // undefined (无法直接访问)

2. 解决循环中的变量捕获问题

scss 复制代码
// 文档5示例:经典循环问题解决方案
var arr = []
for (var i = 1; i <= 5; i++) {
  (function(j) { // IIFE创建独立作用域
    arr.push(function() {
      console.log(j) // 捕获当前j值
    })
  })(i) // 立即执行并传入i值
}

// 执行数组中的函数
for (var j = 0; j < arr.length; j++) {
  arr[j]() // 正确输出1,2,3,4,5
}

方案优势

  • 使用 IIFE(立即执行函数)为每次循环创建独立作用域
  • 将循环变量 i 作为参数 j 传递给闭包
  • 每个闭包捕获自己的 j 值,解决共享变量问题

3. 记忆化和高阶函数

javascript 复制代码
// 记忆化示例:缓存计算结果
function memoize(fn) {
  const cache = {}
  return function(...args) {
    const key = JSON.stringify(args)
    return cache[key] || (cache[key] = fn.apply(this, args))
  }
}

// 使用示例
const factorial = memoize(n => {
  return n <= 1 ? 1 : n * factorial(n - 1)
})

console.log(factorial(5)) // 首次计算
console.log(factorial(5)) // 从缓存读取(性能优化)

闭包的内存管理

闭包的内存泄漏风险

javascript 复制代码
// 文档6示例:闭包保留外部变量引用
function foo() {
  var myname = '刘洋'
  var largeData = new Array(1000000).fill('数据') // 大数据
  
  return function bar() {
    console.log(myname) // 闭包引用myname和largeData!
  }
}

var baz = foo()
baz()

风险分析

  1. bar() 闭包引用了 foo() 的所有变量
  2. 即使只使用 mynamelargeData 仍被保留在内存中
  3. 多次创建此类闭包会导致严重内存泄漏

优化方案:

javascript 复制代码
// 优化1:最小化引用
function optimizedFoo() {
  const largeData = new Array(1000000).fill('数据')
  const name = '刘洋'
  
  return {
    getName: function() {
      return name // 只暴露需要的name
    }
  }
}

// 优化2:及时释放
let baz = foo()
baz()
baz = null // 解除引用,释放内存

闭包与词法环境的关系

JavaScript 引擎通过词法环境管理闭包:

javascript 复制代码
// 文档3示例:块级作用域与闭包
function foo() {
  var a = 1
  let b = 2
  {
    let b = 3 // 新的块级作用域
    var c = 4 // var属于函数作用域
    let d = 5
    console.log(a) // 1(访问函数作用域)
    console.log(b) // 3(访问当前块级作用域)
  }
  console.log(b) // 2(外部作用域)
  console.log(c) // 4(函数作用域)
  console.log(d) // ReferenceError(块级作用域已销毁)
}
foo()

关键点

  1. var 声明的变量属于函数作用域
  2. let/const 创建块级作用域
  3. 闭包可访问包含它的所有外层作用域
  4. 块级作用域结束后内部变量被销毁(除非被闭包引用)

总结

  1. 理解作用域链
    • 明确变量的查找路径
    • 记住函数定义位置决定变量访问权
  2. 结合块级作用域
    • 使用 let/const 管理变量生命周期
    • 避免意外的变量提升问题
  3. 性能优化
    • 避免在闭包中保留大型数据结构
    • 使用记忆化优化重复计算

核心原则:函数定义的位置决定了它能访问哪些变量,而不是函数被调用的位置。掌握这一原则,就能编写出高效、健壮的 JavaScript 代码。

相关推荐
passerby606136 分钟前
完成前端时间处理的另一块版图
前端·github·web components
掘了44 分钟前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc