闭包是什么

0 阅读4分钟

1. 核心概念

闭包是指一个函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。

简单说:函数 + 它创建时的作用域环境 = 闭包

function outer() {
    const name = '张三'  // 局部变量
    
    function inner() {
        console.log(name)  // 内部函数访问外部函数的变量
    }
    
    return inner  // 返回内部函数
}

const closureFunc = outer()  // outer执行完毕,name应该被销毁
closureFunc()  // 但这里仍然可以访问name,输出:张三

2. 工作原理

function createCounter() {
    let count = 0  // 这个变量会被闭包"记住"
    
    return function() {
        count++  // 每次调用都会修改这个"记住"的变量
        return count
    }
}

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

const counter2 = createCounter()  // 新的闭包,独立的count
console.log(counter2())  // 1
console.log(counter2())  // 2

3. 闭包的典型应用场景

3.1 私有变量封装

function createPerson(name) {
    let privateAge = 0  // 私有变量
    
    return {
        getName() {
            return name
        },
        getAge() {
            return privateAge
        },
        setAge(newAge) {
            if (newAge > 0) privateAge = newAge
        },
        haveBirthday() {
            privateAge++
        }
    }
}

const person = createPerson('李四')
console.log(person.getName())  // 李四
console.log(person.privateAge) // undefined,无法直接访问
person.haveBirthday()
console.log(person.getAge())   // 1

3.2 模块模式

const calculator = (function() {
    let memory = 0  // 私有变量
    
    return {
        add(x, y) {
            const result = x + y
            memory = result
            return result
        },
        multiply(x, y) {
            const result = x * y
            memory = result
            return result
        },
        getMemory() {
            return memory
        },
        clearMemory() {
            memory = 0
        }
    }
})()

console.log(calculator.add(2, 3))      // 5
console.log(calculator.getMemory())    // 5
console.log(calculator.multiply(4, 5)) // 20
console.log(calculator.getMemory())    // 20

3.3 函数工厂

function createMultiplier(factor) {
    return function(x) {
        return x * factor
    }
}

const double = createMultiplier(2)
const triple = createMultiplier(3)

console.log(double(5))  // 10
console.log(triple(5))  // 15

3.4 事件处理与回调

function setupButtons() {
    const buttons = ['按钮1', '按钮2', '按钮3']
    
    for (let i = 0; i < buttons.length; i++) {
        const button = document.createElement('button')
        button.textContent = buttons[i]
        
        // 使用闭包保存每个按钮的索引
        button.addEventListener('click', (function(index) {
            return function() {
                console.log(`点击了第${index + 1}个按钮:${buttons[index]}`)
            }
        })(i))
        
        document.body.appendChild(button)
    }
}

// 不使用闭包会有问题的版本:
function setupButtonsWrong() {
    for (var i = 0; i < 3; i++) {  // 注意:使用var
        const button = document.createElement('button')
        button.textContent = `按钮${i + 1}`
        
        button.addEventListener('click', function() {
            console.log(`点击了第${i + 1}个按钮`)  // 所有按钮都会输出"第4个按钮"
        })
    }
}

4. 闭包的优点

4.1 数据封装与私有化

const bankAccount = (function() {
    let balance = 1000  // 私有变量
    
    return {
        deposit(amount) {
            if (amount > 0) {
                balance += amount
                return true
            }
            return false
        },
        withdraw(amount) {
            if (amount > 0 && amount <= balance) {
                balance -= amount
                return amount
            }
            return 0
        },
        getBalance() {
            return balance
        }
    }
})()

// 外部无法直接访问balance
console.log(bankAccount.balance)      // undefined
console.log(bankAccount.getBalance()) // 1000
bankAccount.deposit(500)
console.log(bankAccount.getBalance()) // 1500

4.2 状态保持

function createLogger(prefix) {
    let callCount = 0
    
    return function(message) {
        callCount++
        console.log(`[${prefix}] 调用次数:${callCount},消息:${message}`)
    }
}

const log1 = createLogger('系统')
log1('启动成功')  // [系统] 调用次数:1,消息:启动成功
log1('运行正常')  // [系统] 调用次数:2,消息:运行正常

const log2 = createLogger('用户')
log2('登录成功')  // [用户] 调用次数:1,消息:登录成功

5. 闭包的缺点与注意事项

5.1 内存泄漏风险

function createLeakyClosure() {
    const largeArray = new Array(1000000).fill('data')  // 大数组
    
    return function() {
        // 即使外部不需要largeArray,闭包仍然保持引用
        console.log('闭包执行')
    }
}

const leakyFunc = createLeakyClosure()
// largeArray不会被垃圾回收,因为闭包保持着引用

// 解决方法:不再使用时解除引用
leakyFunc = null

5.2 性能考虑

// 避免在循环中创建不必要的闭包
function processItems(items) {
    // 不好的做法:循环内创建闭包
    for (let i = 0; i < items.length; i++) {
        setTimeout(function() {
            console.log(items[i])
        }, 1000)
    }
    
    // 改进做法:使用IIFE或箭头函数
    for (let i = 0; i < items.length; i++) {
        (function(index) {
            setTimeout(function() {
                console.log(items[index])
            }, 1000)
        })(i)
    }
    
    // 更好的做法:使用let的块级作用域
    for (let i = 0; i < items.length; i++) {
        setTimeout(function() {
            console.log(items[i])
        }, 1000)
    }
}

6. 闭包面试题解析

6.1 经典循环问题

for (var i = 1; i <= 5; i++) {
    setTimeout(function() {
        console.log(i)  // 输出6个6
    }, i * 1000)
}

// 解决方案1:使用IIFE创建闭包
for (var i = 1; i <= 5; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j)  // 输出1,2,3,4,5
        }, j * 1000)
    })(i)
}

// 解决方案2:使用let的块级作用域
for (let i = 1; i <= 5; i++) {
    setTimeout(function() {
        console.log(i)  // 输出1,2,3,4,5
    }, i * 1000)
}

// 解决方案3:setTimeout的第三个参数
for (var i = 1; i <= 5; i++) {
    setTimeout(function(j) {
        console.log(j)  // 输出1,2,3,4,5
    }, i * 1000, i)
}

6.2 多个闭包共享变量

function createClosures() {
    let shared = 0
    
    return [
        function() {
            shared++
            return shared
        },
        function() {
            shared *= 2
            return shared
        },
        function() {
            return shared
        }
    ]
}

const [inc, double, get] = createClosures()
console.log(inc())   // 1
console.log(double()) // 2
console.log(get())    // 2
console.log(inc())    // 3

7. 闭包的内存管理

// 正确管理闭包内存
function createDataProcessor() {
    const data = new Array(10000).fill('important data')
    
    const processor = {
        process() {
            // 处理数据
            console.log('Processing data...')
        },
        getData() {
            return data[0]
        },
        cleanup() {
            // 清理函数,断开对大数据的引用
            data.length = 0
            processor.process = null
            processor.getData = null
        }
    }
    
    return processor
}

const processor = createDataProcessor()
processor.process()

// 不再使用时调用cleanup
processor.cleanup()
processor = null  // 帮助垃圾回收

8. 闭包与作用域链

function outer() {
    const a = 1
    
    function middle() {
        const b = 2
        
        function inner() {
            const c = 3
            console.log(a, b, c)  // 可以访问所有外部变量
        }
        
        return inner
    }
    
    return middle
}

const middleFunc = outer()
const innerFunc = middleFunc()
innerFunc()  // 输出:1 2 3

总结

闭包的本质:函数在执行时,会创建一个包含其变量对象的作用域链。闭包使得函数可以访问这个作用域链上的所有变量,即使外部函数已经执行完毕。

关键要点

  1. 闭包是函数和其词法环境的组合
  2. 内部函数可以访问外部函数的作用域
  3. 闭包在创建时"记住"了当时的环境
  4. 每个闭包都是独立的,有自己的状态
  5. 合理使用闭包,注意内存管理

闭包是JavaScript的核心概念之一,理解闭包对于掌握JavaScript的高级特性至关重要。