你不知道的js之闭包
闭包
闭包一直是一个困扰初学者的一个问题,网上对闭包的各种定义和解释都大不相同,有的人说闭包很简单,也有的人说闭包很复杂,甚至在评论区 battle,这下可苦坏了初学者,该信哪个啊😢
其实我们可以看看《你不知道的 JavaScript》的作者对于闭包的理解:
对于那些有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看
作是某种意义上的重生,但是需要付出非常多的努力和牺牲才能理解这个概念。
回忆我前几年的时光,大量使用 JavaScript 但却完全不理解闭包是什么。总是感觉这门语 言有其隐蔽的一面,如果能够掌握将会功力大涨,但讽刺的是我始终无法掌握其中的门 道。还记得我曾经大量阅读早期框架的源码,试图能够理解闭包的工作原理。现在还能回 忆起我的脑海中第一次浮现出关于“模块模式”相关概念时的激动心情。
那时我无法理解并且倾尽数年心血来探索的,也就是我马上要传授给你的秘诀:JavaScript中闭包无处不在,你只需要能够识别并拥抱它。闭包并不是一个需要学习新的语法或模式才能使用的工具。
闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意 识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿 来识别、拥抱和影响闭包的思维环境。
最后你恍然大悟:原来在我的代码中已经到处都是闭包了,现在我终于能理解它们了。
闭包的定义
下面是直接了当的定义,你需要掌握它才能理解和识别闭包:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
注意:⚠️ 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定
下面来看一段代码:
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
这段代码看起来和嵌套作用域中的示例代码很相似。基于词法作用域的查找规则,函数 bar() 可以访问外部作用域中的变量 a (这个例子中的是一个 RHS 引用查询)。
这是闭包吗?
技术上来讲,也许是。但根据前面的定义,确切地说并不是。我认为最准确地用来解释 bar() 对 a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却是非常重要的一部分!)
👀这段你不知道的 JavaScript 原话说明了闭包应该是有严格意义上的闭包和广义的闭包。可能也就是这么多人争论的原因。所以上面的例子并不算是严格意义上的闭包,因为 bar(…) 这个函数没有在当前词法作用域之外执行。
下面我们来看一段代码,清晰地展示了闭包:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。
在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(), 际上只是通过不同的标识符引用调用了内部的函数 bar()。
bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。
在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。
拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
总结上面这段话:就是函数 foo() 内定义一个变量和一个函数 bar() ,函数 bar() 里面使用了函数 foo() 内的变量 a,也就是函数 bar() 访问了函数 foo() 的词法作用域,所以函数 bar() 拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活。最后函数 bar() 以值传递的方式到 baz,然后函数 baz 在函数 bar 定义时的词法作用域之外执行,所以这两个条件就构成了闭包。
感慨:原书的作者对比闭包讲的非常清楚了,果然还是看书效率最高,看网上不同的说法都要把自己看出人格分裂了。
闭包总结:本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一 级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!还有防抖节流保持状态和 React 里面的函数组件保持状态的原理都是闭包。