JavaScript 闭包

40 阅读4分钟

一个秘密的技巧袋

JavaScript闭包的概念可能会让你有点难以理解。通常情况下,当你有一个函数返回另一个函数时,就会用到闭包。它们背后的魔力在于,即使一个函数被返回,从而消除了调用函数和它的局部变量,返回的函数仍然保留着它所删除的环境的 "记忆",包括被破坏的局部变量,并且可以使用它们,就像它们仍然存在一样🤯。

让我们通过分解下面的代码来揭开闭包的神秘面纱。

1:  var add = (function () {2:    var counter = 0;3:    return function () {counter += 1; return counter}4:  })();5:  const increment1 = add();6:  const increment2 = add();7:  const increment3 = add();8:  console.log('increments: ', increment1, increment2, increment3); // output -> increments:  1 2 3

代码取自 w3schools的JavaScript闭包 页面,并进行了修改,添加了一个 console.log

在任何脚本运行之前,全局执行上下文(GEC)被推入调用栈

脚本运行后,变量add 和它的函数表达式一起在GEC中被创建。请注意,分配给add 的函数是一个立即调用的函数表达式(IIFE),返回一个匿名函数。function() {counter += 1; return counter}

有趣的是......为什么要这样写函数?嗯......为什么不把 add等于一个有定义的函数counter += 1; return counter而不是让它等于一个返回该函数的IIFE。事实上,你可以在函数之外定义 counter在函数之外,像这样重写函数,输出结果也是一样的。

var counter = 0;var add = function() {  counter += 1;  return counter;};

敏锐的观察侦探。让我们继续沿着黄砖路走下去,看看这场冒险还能把我们带到哪里。

现在,在这一点上,更多的JavaScript魔法出现了。IIFE's,正如它的名字所暗示的那样,一经定义就会被调用。这意味着,var add 现在确实等于function() {counter =+ 1; return counter} 。噗,就像这样,IIFE现在消失了......哦,有趣......但现在var counter = 0; 也消失了。

那么,当我们可以像前面提到的那样直接写出函数时,我们为什么还要跳过这些障碍呢?

var counter = 0;var add = function() {  counter += 1;  return counter;};

这就简单多了,也容易理解。另外,有了IIFE。 *counter*看起来好像从来没有被声明过。它如何在不引起错误的情况下执行呢?

这两个问题都很好。是的,把代码改写成这样会更简单易懂,而且它仍然会产生预期的结果。但是counter 的声明方式使它成为一个全局变量。我们只希望它能被add函数访问。这样的重写方式污染了全局范围,使它没有受到保护而被其他函数访问并可能改变。我们正试图避免这两种情况。

好吧,这样如何?

var add = function() {  var counter = 0;  counter += 1;  return counter;}

哦,等等,这将重置 counter 0 每次被调用时都会重置。好吧,我看到了缺点,所以告诉我更多关于这里发生的事情......

var add = function() {  counter += 1;  return counter};

很好,所以IIFE执行并返回一个函数,该函数将counter ,然后返回1 。IIFE不再存在,它的局部变量也不再存在,似乎是这样。返回函数的神奇之处在于,即使在它被销毁之后,它还能记住它的环境,包括局部变量,并能继续使用它们。所以,在我们的例子中,counter 是在IIFE的主体中声明的,一旦执行,声明就消失了。然而,返回的函数记住了counter的声明,即使它不再是全局执行上下文中的。通常,当一个函数启动并需要找到它所操作的变量的值时,它会首先检查它的局部作用域,然后是它的父级作用域,然后是祖级作用域...一直到它到达全局作用域。这就是所谓的作用域链。然而,当add ,在它检查它的本地作用域之前,它会首先检查它的秘密技巧袋。当然,它将在其中找到counter

add 在第5行被调用时,它将看到counter 最初被分配给0 。然后它将通过1 来增加counter的值,并在设置increment1 = 1 的同时返回其值。

当它在第6行被调用时,它将再次检查它的技巧包,看到counter 现在是1 ,再次增量并返回,并分配给increment2 = 2......。

......以此类推,直到下一行。所有的add 函数都已执行并从堆栈中弹出。console.log 运行,最后输出increments: 1 2 3

你可以通过使用Chrome浏览器开发工具中的控制台来查看你的锦囊妙计,也就是所谓的闭包范围。只需将你的闭包传入console.dir()并展开[[Scopes]]属性。

眼见为实

闭包函数有一种神奇的能力。它们能够封装从堆栈中弹出的数据,将它们保持在比普通函数的作用域链更深的层次上,并且专门使用它们,而不必担心它们被其他函数触及✨💫✨