JavaScript 闭包:从原理到实践
什么是闭包?
闭包(Closure)是 JavaScript 中一个核心概念,指的是一个函数能够访问并记住其词法作用域中的变量,即使该函数在其词法作用域之外执行。简单来说,闭包让函数可以"记住"它被创建时的环境。
闭包的核心特征:
- 函数嵌套:内部函数定义在外部函数内部
- 变量访问:内部函数访问外部函数的变量
- 外部执行:内部函数在外部函数执行完成后仍被使用
闭包的作用域链原理
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()
关键点解析:
c()
定义在a()
中,其作用域链是:c → a → 全局- 当
c()
执行时,先在自身作用域查找num
(未找到) - 通过作用域链向上在
a()
的作用域查找(未找到) - 最后在全局作用域找到
num=10
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()
风险分析:
bar()
闭包引用了foo()
的所有变量- 即使只使用
myname
,largeData
仍被保留在内存中 - 多次创建此类闭包会导致严重内存泄漏
优化方案:
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()
关键点:
var
声明的变量属于函数作用域let/const
创建块级作用域- 闭包可访问包含它的所有外层作用域
- 块级作用域结束后内部变量被销毁(除非被闭包引用)
总结
- 理解作用域链 :
- 明确变量的查找路径
- 记住函数定义位置决定变量访问权
- 结合块级作用域 :
- 使用
let/const
管理变量生命周期 - 避免意外的变量提升问题
- 使用
- 性能优化 :
- 避免在闭包中保留大型数据结构
- 使用记忆化优化重复计算
核心原则:函数定义的位置决定了它能访问哪些变量,而不是函数被调用的位置。掌握这一原则,就能编写出高效、健壮的 JavaScript 代码。