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 代码。

相关推荐
小小小小宇2 小时前
前端 Service Worker
前端
只喜欢赚钱的棉花没有糖2 小时前
http的缓存问题
前端·javascript·http
小小小小宇2 小时前
请求竞态问题统一封装
前端
loriloy2 小时前
前端资源帖
前端
源码超级联盟2 小时前
display的block和inline-block有什么区别
前端
GISer_Jing3 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js
让梦想疯狂3 小时前
开源、免费、美观的 Vue 后台管理系统模板
前端·javascript·vue.js
海云前端3 小时前
前端写简历有个很大的误区,就是夸张自己做过的东西。
前端
葡萄糖o_o3 小时前
ResizeObserver的错误
前端·javascript·html
AntBlack3 小时前
Python : AI 太牛了 ,撸了两个 Markdown 阅读器 ,谈谈使用感受
前端·人工智能·后端