7 分钟学会 JavaScript 闭包

2025-06-04

7 分钟学会 JavaScript 闭包

要学习闭包,您需要了解范围的工作原理。

在 JavaScript 中,我们有全局和本地范围。

在主程序中声明的变量称为全局变量。它们属于全局对象,可以在代码中的任何位置访问。

在函数中声明的变量称为局部作用域。它们属于函数体(包括其嵌套函数),并且可以访问全局作用域中的任何变量。

如果一个函数定义在另一个函数内部,则父函数将无法访问子函数中声明的变量。但子函数可以访问父函数中的变量。

因此基本上任何代码块都可以访问其外部范围的变量。

这是一个例子

const x = 'someone';
function incrementFrom(count) {
  // has access to x
  return count++;
}

const firstCall = incrementFrom(0);
const secondCall = incrementFrom(5);
console.log(firstCall);
console.log(secondCall);

// does not have access to count i.e console.log(count) will throw an error
Enter fullscreen mode Exit fullscreen mode

请记住,该参数是局部变量,因为它位于局部作用域内。每次调用该incrementFrom函数时,它都会重新创建。
它基本上等同于

function incrementNum() {
  let count = 5;
  return count++;
}
// all the calls
Enter fullscreen mode Exit fullscreen mode

因此好消息是,局部变量在函数调用时不会相互践踏。

但坏消息是,这种标准调用incrementFrom(5)几次并不会使其增加。它只会继续记录 5,因为“每个局部变量在每次调用时都会重新创建”。

那么,如果我们想在每次调用函数时都使用传入(或创建)的值,该怎么办呢?就像在 的情况下incrementFrom(),我们只想获取一个初始值,并在每次调用时递增它。

因此,当我调用incrementFrom(3)3 次时,它会神奇地从 3 增加到 4,然后增加到 5,再增加到 6。这可以通过闭包实现。

另一个例子可能是将用户的 传递firstName给函数,然后将 添加lastName到其中。例如

function printName(firstName, lastName) {
  return `${firstName} ${lastName}`;
}
Enter fullscreen mode Exit fullscreen mode

由于某些原因,lastName尚未提供,因此您使用现有资源进行第一次调用

console.log(printName('John', "I don't have it yet"));
Enter fullscreen mode Exit fullscreen mode

最后,lastName获取被处理了,现在内存中没有地方了firstName,所以你最终会这样做来使用进行第二次调用lastName

console.log(printName('I lost it', 'Doe'));
Enter fullscreen mode Exit fullscreen mode

我知道这个例子有点傻,但其核心思想是进行两次函数调用,并将它们的局部变量关联起来。这可以通过闭包实现。

那么,什么是闭包?

在 Eloquent JavaScript 中它说

...能够在封闭范围内引用本地绑定的特定实例称为闭包。

简而言之,闭包是即使外部函数已经关闭(不再活动),仍然可以访问外部函数范围的函数。

这意味着子函数可以随时使用父函数中声明的任何局部变量,即使父函数已被调用并且不再处于活动状态。

它的工作方式是这样的,当我们创建一个带有任何局部变量的函数时,该函数将返回另一个函数(即子函数),并且如上所述,子函数可以访问父函数中的变量。

所以当函数被调用时,其值是一个函数,可以被调用。即

function callMe() {
  return () => 'Hello world';
}

const funcVal = callMe();
console.log(funcVal());
Enter fullscreen mode Exit fullscreen mode

这只是“函数就是它们返回的内容”的一种表达,或者更好地表达为“函数作为值”。

因此,当调用返回字符串的函数时,字符串的属性和方法可以在该函数调用中使用。数字、数组、对象和函数也一样。

在这种情况下,我们的函数返回一个函数,这意味着callMe()可以调用该函数的值,因为它是一个函数(您可以添加 params 和 args)。

这就是事情变得更加有趣的地方……

function callMe(val) {
  return (newVal) => val + newVal;
}

const funcVal = callMe(2);
console.log(funcVal(2)); // 4
Enter fullscreen mode Exit fullscreen mode

我们已经调用了callMe()一次函数并传入了一个值。现在,当我们调用它返回的函数时,可以使用这个值。这就是闭包。

我现在可以调用funcVal()不同的时间,它仍然可以访问val父函数的局部变量()( callMe)

console.log(funcVal(3)); // 5 i.e 2 + 3
console.log(funcVal(10)); // 12 i.e 2 + 10
// we can go on and on
Enter fullscreen mode Exit fullscreen mode

现在,函数局部变量在不同调用中不践踏自身的规则仍然有效,我们只对父函数进行了一次调用callMe,让我们尝试再调用一次

const funcVal = callMe(2);
const funcVal2 = callMe(100); // local variable (val) will be created anew here with a value of 100.

console.log(funcVal(2)); // 4 i.e 2 + 2
console.log(funcVal2(10)); // 110 i.e 100 + 10
Enter fullscreen mode Exit fullscreen mode

所以基本上,它们返回的函数才是真正的魔法。即便如此,它们的局部变量在不同的调用中仍然不会互相干扰。

console.log(funcVal(3)); // 5 i.e 2 + 3
console.log(funcVal(10)); // local variable (newVal) will be created anew here, but it still has access to the local variables in the outer function. so we get 12 i.e 2 + 10
Enter fullscreen mode Exit fullscreen mode

现在让我们回到最初的例子或问题。我们先解决名称问题。

回想一下,我们之前有一个函数printName可以打印用户的名字和姓氏,但由于某种原因,姓氏会被延迟(我们知道这一点),所以我们一开始只能先不打印它。等它最终打印出来后,我们应该打印全名。我们会这样做:

function printName(firstName) {
  return (lastName) => `${firstName} ${lastName}`;
}
Enter fullscreen mode Exit fullscreen mode

现在功能有一点改变

  • 该函数printName现在只接受一个参数(firstName-我们知道那个人不会被延迟)
  • it( printName) 现在返回一个函数而不是返回一个字符串。
  • 子函数接受lastName(我们知道将会延迟的人)然后返回全名字符串。

如果我们尝试记录,现在会更有意义

// first name comes
const user = printName('John');
// after a while, last name comes
console.log(user('Doe')); // John Doe
Enter fullscreen mode Exit fullscreen mode

瞧!问题用闭包解决了。我们来添加另一个用户吧。

// first name comes
const user = printName('John');
// after a while, last name comes
console.log(user('Doe')); // John Doe

// new user
const user2 = printName('Sarah');
console.log(user2('Michelle')); // Sarah Michelle
Enter fullscreen mode Exit fullscreen mode

我知道有很多其他方法可以解决这个问题,但这是另一种方法。

现在,在我们结束本文之前,最后一个例子是我们的计数器。

回想一下,我们有一个函数incrementFrom,它没有任何递增。我们该如何解决这个问题?

function incrementFrom(count) {
  return () => count++;
}
Enter fullscreen mode Exit fullscreen mode

只有一件事发生了变化,我们返回了一个返回的函数,count + 1而不是仅仅返回count + 1

现在让我们看看它是否有效

const addOne = incrementFrom(5);
console.log(addOne()); // 5
console.log(addOne()); // 6
console.log(addOne()); // 7
console.log(addOne()); // 8
// and on and on
Enter fullscreen mode Exit fullscreen mode

令人高兴且毫不意外的是,它有效!!

这就是闭包在编程中非常有用的原因。

结论

如果你是第一次学习闭包,那么这部分内容可能有点难理解。但随着练习的深入,你就会逐渐明白。

感谢您读到最后,希望您和我一样享受其中并从中学习。下次再见。与此同时,您可以给我留言,分享您的想法。您也可以点击“赞”和“分享”按钮,以便我们联系到更多开发者。

让我们联系吧,在 Twitter 上联系我@elijahtrillionz

文章来源:https://dev.to/elijahtrillionz/learn-javascript-closures-in-7-mins-324n
PREV
编写更好的 React 组件的 3 个技巧
NEXT
如何在 React 中向 API 发出异步请求