Skip to content

JavaScript 闭包详解

什么是闭包

闭包(Closure)是 JavaScript 中一个非常重要的概念,它指的是能够访问其词法作用域之外变量的函数。即使创建函数的上下文已经销毁,闭包仍然能够访问并操作这些变量。

闭包的工作原理

当一个函数在另一个函数内部定义时,内部函数会形成一个闭包。这个闭包包含了内部函数的代码以及创建它时的环境(词法作用域)。

基本示例

javascript
function outer() {
  let count = 0

  function inner() {
    count++
    console.log(count)
  }

  return inner
}

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

在这个例子中:

  • outer 函数定义了一个局部变量 count
  • outer 返回了内部函数 inner
  • outer 执行完毕后,其执行上下文被销毁
  • 但由于 inner 函数引用了 count 变量,count 变量不会被垃圾回收
  • 每次调用 counter() 都会访问和修改同一个 count 变量

闭包的优缺点

优点

  1. 数据封装:可以创建私有变量,避免全局命名空间污染
  2. 状态保持:能够在多次调用之间保持状态
  3. 模块化:可以创建独立的模块,暴露有限的接口
  4. 函数工厂:可以根据不同参数创建不同行为的函数
  5. 回调函数:在异步操作中保持上下文

缺点

  1. 内存占用:闭包会引用外部函数的变量,导致这些变量不会被垃圾回收
  2. 性能影响:闭包的创建和执行比普通函数稍慢
  3. 内存泄漏风险:如果闭包引用了大对象或 DOM 元素,可能导致内存泄漏
  4. 调试困难:闭包的作用域链可能较复杂,增加调试难度

闭包的 10 个使用场景

1. 计数器

javascript
function createCounter() {
  let count = 0

  return {
    increment: function () {
      count++
      return count
    },
    decrement: function () {
      count--
      return count
    },
    getCount: function () {
      return count
    }
  }
}

const counter = createCounter()
console.log(counter.increment()) // 1
console.log(counter.increment()) // 2
console.log(counter.decrement()) // 1
console.log(counter.getCount()) // 1

2. 防抖函数

javascript
function debounce(func, wait) {
  let timeout

  return function () {
    const context = this
    const args = arguments

    clearTimeout(timeout)
    timeout = setTimeout(function () {
      func.apply(context, args)
    }, wait)
  }
}

const debouncedSearch = debounce(function (query) {
  console.log('Searching for:', query)
  // 实际的搜索逻辑
}, 300)

// 输入框事件监听
document.getElementById('search').addEventListener('input', function (e) {
  debouncedSearch(e.target.value)
})

3. 节流函数

javascript
function throttle(func, limit) {
  let inThrottle

  return function () {
    const context = this
    const args = arguments

    if (!inThrottle) {
      func.apply(context, args)
      inThrottle = true
      setTimeout(function () {
        inThrottle = false
      }, limit)
    }
  }
}

const throttledScroll = throttle(function () {
  console.log('Scroll event fired')
  // 处理滚动事件
}, 100)

window.addEventListener('scroll', throttledScroll)

4. 模块模式

javascript
const calculator = (function () {
  let result = 0

  function validateNumber(num) {
    return typeof num === 'number' && !isNaN(num)
  }

  return {
    add: function (a, b) {
      if (validateNumber(a) && validateNumber(b)) {
        result = a + b
        return result
      }
      return 'Invalid input'
    },
    subtract: function (a, b) {
      if (validateNumber(a) && validateNumber(b)) {
        result = a - b
        return result
      }
      return 'Invalid input'
    },
    getResult: function () {
      return result
    }
  }
})()

console.log(calculator.add(5, 3)) // 8
console.log(calculator.subtract(10, 4)) // 6
console.log(calculator.getResult()) // 6

5. 函数工厂

javascript
function createMultiplier(multiplier) {
  return function (num) {
    return num * multiplier
  }
}

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

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

6. 事件处理

javascript
function setupEventHandlers() {
  let count = 0

  document.getElementById('btn').addEventListener('click', function () {
    count++
    console.log(`Button clicked ${count} times`)
  })
}

setupEventHandlers()

7. 私有方法和属性

javascript
function Person(name) {
  let _name = name
  let _age = 0

  function validateAge(age) {
    return typeof age === 'number' && age >= 0
  }

  return {
    getName: function () {
      return _name
    },
    setAge: function (age) {
      if (validateAge(age)) {
        _age = age
        return true
      }
      return false
    },
    getAge: function () {
      return _age
    }
  }
}

const person = Person('John')
console.log(person.getName()) // John
console.log(person.setAge(30)) // true
console.log(person.getAge()) // 30
console.log(person.setAge(-5)) // false

8. 缓存函数

javascript
function memoize(func) {
  const cache = {}

  return function (...args) {
    const key = JSON.stringify(args)

    if (cache[key]) {
      console.log('Cache hit')
      return cache[key]
    }

    console.log('Cache miss')
    const result = func.apply(this, args)
    cache[key] = result
    return result
  }
}

const fibonacci = memoize(function (n) {
  if (n <= 1) return n
  return fibonacci(n - 1) + fibonacci(n - 2)
})

console.log(fibonacci(10)) // 55
console.log(fibonacci(10)) // 55 (from cache)

9. 异步操作中的上下文保持

javascript
function fetchData(url) {
  return function (callback) {
    fetch(url)
      .then(response => response.json())
      .then(data => callback(null, data))
      .catch(error => callback(error))
  }
}

const fetchUser = fetchData('https://api.example.com/user/1')
const fetchPosts = fetchData('https://api.example.com/posts')

fetchUser(function (err, user) {
  if (err) console.error(err)
  else console.log('User:', user)
})

fetchPosts(function (err, posts) {
  if (err) console.error(err)
  else console.log('Posts:', posts)
})

10. 柯里化函数

javascript
function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args)
    }

    return function (...nextArgs) {
      return curried.apply(this, args.concat(nextArgs))
    }
  }
}

function add(a, b, c) {
  return a + b + c
}

const curriedAdd = curry(add)
console.log(curriedAdd(1)(2)(3)) // 6
console.log(curriedAdd(1, 2)(3)) // 6
console.log(curriedAdd(1)(2, 3)) // 6

闭包的最佳实践

  1. 合理使用闭包:只在必要时使用闭包,避免过度使用导致内存问题
  2. 及时释放:当闭包不再需要时,将引用设置为 null,以便垃圾回收
  3. 避免循环引用:特别是在 DOM 元素和闭包之间
  4. 注意性能:在性能敏感的场景中,要考虑闭包的开销
  5. 保持简洁:闭包逻辑应该简单明了,避免复杂的嵌套

总结

闭包是 JavaScript 中一个强大的特性,它允许函数访问其词法作用域之外的变量。通过合理使用闭包,我们可以实现数据封装、状态管理、模块化等高级功能。然而,闭包也有其缺点,主要是内存占用和性能影响。

在实际开发中,我们应该根据具体场景权衡闭包的优缺点,合理使用这一特性,以写出更加优雅、高效的 JavaScript 代码。

基于 VitePress 的本地知识库