发布于 2026-01-05 4 阅读
0

JavaScript Closure 简明解释

JavaScript Closure 简明解释

 

闭包是一个有状态的函数,它由另一个函数返回。它充当容器,即使父函数已经执行完毕,它仍然会保留父函数作用域中的变量和参数。考虑以下简单示例。

function sayHello() {
  const greeting = "Hello World";

  return function() { // anonymous function/nameless function
    console.log(greeting)
  }
}


const hello = sayHello(); // hello holds the returned function
hello(); // -> Hello World

看!我们有一个返回函数的函数!返回的函数会被保存到一个变量中,并在下面的代码行中被调用。

编写相同代码的方法有很多种!

现在你已经对闭包的基本概念有所了解,下面介绍几种编写与上面相同代码的方法。

// original
function sayHello() {
  const greeting = "Hello World";

  return function() { // anonymous function
    console.log(greeting)
  }
}


// #1
function sayHello() {
  const greeting = "Hello World";

  return function hello() { // named function
    console.log(greeting)
  }
}


// #2
function sayHello() {
  const greeting = "Hello World";

  function hello() { // named function
    console.log(greeting)
  }

  return hello; // return inner function on a different line
}


// #3
function sayHello() {
  const greeting = "Hello World";
  const hello = () => { // arrow function
    console.log(greeting)
  }

  return hello;
}

选择你最喜欢的样式并坚持使用,因为以上所有变体都会打印出相同的结果!

const hello = sayHello();
hello(); // -> Hello World

 

关闭的好处及其实际应用

 

私有命名空间

内部函数能够记住创建时的环境固然很棒,但它有什么用呢?有两个用途。首先,它可以保护你的变量是私有的。这里有一个经典的计数器示例。

function counter() {
  let count = 0;
  return function() {
    count += 1;
    return count;
  }
}


const increment = counter();
console.log(increment()); // 1
console.log(increment()); // 2
console.log(count) // Reference error: count is not defined

尝试访问计数变量会导致引用错误,因为它没有暴露给全局环境。这有助于我们减少错误,因为我们的状态由特定方法更严格地控​​制。

可重用状态

因为“count”是私有作用域的,所以我们可以创建不同的计数器函数实例,而它们的“count”变量不会重叠!

function counter() {
  let count = 0;
  return function() {
    count += 1;
    return count;
  }
}

const incrementBananaCount = counter();
const incrementAppleCount = counter();
console.log(incrementBananaCount()); // 1
console.log(incrementBananaCount()); // 2
console.log(incrementAppleCount()); // 1

模块设计模式

模块设计模式是构建 JavaScript 应用时常用的一种约定。它利用立即调用函数表达式 (IIFE) 返回对象,并且只公开你想要公开的变量和方法。

let Dog1 = (function() {
  let name = "Suzy";

  const getName = () => {
    return name;
  }

  const changeName = (newName) => {
    name = newName;
  }

  return {
    getName: getName,
    changeName: changeName
  }
}())

console.log(name); // undefined
Dog1.getName() // Suzy
Dog1.changeName("Pink")
Dog1.getName() // Pink

这段代码一旦运行,函数就会执行并返回一个对象,该对象会被保存到 Dog1 中。这种模式回归到保持命名空间私有,仅通过对象的形式公开我们需要的内容,即公共方法和变量。状态被完全封装了!

 

著名的面试问题

运行以下函数会得到什么结果?

for(var i=0; i<5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}

为什么这是个热门面试题?因为它考察你对函数作用域/代码块作用域、闭包、setTimeout 和匿名函数的理解!答案会在 1 秒后打印出五个 5。

5
5
5
5
5

怎么做到的呢?简单来说,`setTimeout` 函数会在循环中每隔 1 秒执行 5 次。超时后,循环内部的函数会执行,这些函数会简单地输出 `i` 的值。1 秒过后,循环已经结束,`i` 的值变成了 5。于是,循环输出了五个 5。这跟你预想的不一样?你可能想看到的是 `i` 的值依次从 1 到 5。

解决方案

有一些解决方法,但我们先重点关注使用闭包!

for(var i=0; i<5; i++) {
  setTimeout((function(index) {
    return function() {
      console.log(index);
    }
  }(i)), 1000)
}

我们有一个闭包,它由一个匿名函数返回,该函数接收当前的“i”作为参数,并将其作为“index”输出。这样做可以将当前变量 i 传递给每个函数。结果是:

0 (...1000ms have passed)
1 (...1000ms have passed)
2 (...1000ms have passed)
3 (...1000ms have passed)
4 (...1000ms have passed)
5 (loop exits)

 

恭喜!🎉🎉 现在你为下一次面试做好了更充分的准备!😉 记住,闭包是一个可以访问包含该函数的作用域的函数。

 

文章来源:https://dev.to/shimphillip/javascript-closure-simply-explained-1f79