
你不知道的JavaScript 中的一段话:回忆我前几年的时光,大量使用JavaScript但却完全不理解闭包是什么。总是感觉言有其隐的一面,如果能够掌握将 会功力大涨,但其讽刺的是我始终无法掌握其中的门道。还记得我曾经大量阅读早期框架的源码,试图能够理解闭包的工作原理,现在还能回忆起我的脑海中第一次浮现出关于 "模块模式" 相关概念时的激动心情。
闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合LSH查询;LHS查询会在当前作用域链进行查询,如果当前作用域查询不到,就会沿着作用域链一层一层,找到的话就会将值赋值给这个变量,如果到达作用域顶端仍然找不到,就会在作用域链顶端创建这个变量。RHS查询;RHS查询会在当前作用域链进行查询,如果当前作用域查询不到,就会沿着作用域链一层一层,,找到的话就会取得这个值并返回,如果到达作用域顶端仍然找不到,就会抛出错误(比如TypeError、ReferenceError)。- function foo() {
- var moment = 18;
- var test = 111;
-
- function bar() {
- const may = moment + 777;
- return may;
- }
-
- console.log(test);
-
- return bar;
- }
-
- var baz = foo();
-
- baz(); // 嗨,朋友,这就是闭包
-
- 复制代码
通过观察上面的代码,我们在foo函数中定义了bar函数,并返回bar函数,同时在bar函数中访问了foo函数中的变量moment。
上面的代码大概的执行流程为:
foo 函数时,foo 函数会将它的内部函数 bar 返回给全局变量 baz;foo 函数执行结束时,执行上下文会被 V8 销毁;moment 已经被 V8 销毁了,因为我们知道 V8 引擎有垃圾回收期用来释放不再使用的内存空间,但是由于存活的函数 bar 依然引用了 foo 函数作用域中的变量 moment,这样就会带来两个问题:foo 执行结束时,变量 moment 该不该被销毁?如果不应该被销毁,那么他应该在什么时候销毁,而又应该采用什么策略?foo 函数时, V8 只会解析 foo 函数,并不会解析内部的 bar函数,仅仅知识对 bar函数进行了标记,在这时 V8 引擎并不知道 bar 函数中是否引用了 外层函数作用域中的变量 moment;JavaScript 是一门基于堆和栈的语言。在执行全局代码时当执行,V8 会将全局执行上下文压入到调用栈中,然后进入执行 foo 函数的调用过程。V8 引擎会为 foo 函数创建执行上下文,执行上下文中包括了变量 moment,然后将 foo 函数的执行上下文压入栈中,foo 函数执行结束之后,foo 函数执行上下文从栈中弹出,这时候 foo 执行上下文中的变量 moment 也随之被销毁。foo 函数的执行上下文被销毁了,但是 bar 函数引用的 foo 函数中的变量却不能被销毁。foo 函数的阶段,虽然采取了惰性解析,不会解析和执行 foo 函数中的的 bar 函数中的 bar 函数,但是 V8还是需要判断 bar 函数是否引用了 foo 函数中的变量。V8 引擎引入了预解析器,当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该书做一次快速的预解析,其中主要的目的主要有两个:foo 函数是否有被 bar 函数引用的变量,如果有,就会把该变量复制一份到堆内存中,同时 bar 函数本身也是一个对象,也会被存放到内存当中,这样即使 foo 函数即使执行完成,内存被释放以后,bar 函数在执行的时候,依然可以从堆内存中访问复制过来的变量;Closure (foo)),但是此函数只有被 bar 函数引用的值,foo 函数中的 test 变量并没有被复制过去,如下图所示:
- function foo() {
- var moment = 777;
-
- function baz() {
- console.log(moment);
- }
-
- bar(baz);
- }
-
- function bar(fn) {
- fn(); // 这也是一个闭包
- }
-
- foo();
- 复制代码
baz 传递给 bar 函数,当调用这个内部函数时(这个时候叫作 fn),它涵盖的 foo()内部作用域的闭包就可以观察到了,因为他能够访问。- function wait(message) {
- setTimeout(function timer() {
- console.log(message);
- }, 1000);
- }
-
- wait("hello world");
- ```js
- function wait(message) {
- setTimeout(function timer() {
- console.log(message);
- }, 1000);
- }
- wait("hello world"); // 这也是一个闭包
- 复制代码
timer 传递给settimeout(...)。timer函数依然保存有wait(...)作用域的闭包。settimeout(...)持有对一个参数的引用,这个参数也许叫作fn或者func,又或者其他类型的名字。引擎会调用这个函数,在这个例子中就是内部的timer函数,而词法作用域在这个过程中保持完整。
- for (var i = 0; i <= 5; i++) {
- setTimeout(() => {
- console.log(i);
- }, i * 10000);
- }
- 复制代码
正常情况下,我们对这段代码行为的预期分别是输出1~5,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次输出的频率输出五次6。因为这个循环的终止条件是 i 不再<=5,条件时 i 的值为6,因此输出显示的是循环结束时 i 的最终值。
这是因为 setTimeout 是异步的,而for循环是同步的,延迟函数的回调会在循环结束时才执行,当循环结束时 i 已经是 6了,所有的回调函数才会开始执行,因此会每次输出一个 6 来。
那么有什么办法可以让这个循环一次输出数字呢? 用 let 关键字代替 var? 答案当然是可以的 , 你会看到 0 1 2 3 4 5 成功输出。
- for (let i = 0; i <= 5; i++) {
- setTimeout(() => {
- console.log(i); // 0 1 2 3 4 5 成功输出
- }, 1000);
- }
- 复制代码
let,用立即执行函数(IIFE)呢?- for (var i = 0; i <= 5; i++) {
- (function () {
- setTimeout(() => {
- console.log(i); // 输出 6 次 6
- }, 1000);
- })();
- }
- 复制代码
IIFE 在每次迭代中创建的作用域封闭起来。但是该错用域是空的,所以 IIFE只是一个什么都没有的空作用域。- for (var i = 0; i <= 5; i++) {
- (function (j) {
- setTimeout(() => {
- console.log(j); // 0 1 2 3 4 5 成功输出
- }, 1000);
- })(i);
- }
- 复制代码
i 作为参数传递给 立即执行函数 ,j 就是传进来的参数,这个时候 立即执行函数 就有自己的作用域变量 j 了,问题就迎刃而解了。这就是闭包的力量。- function Person() {
- var friends = 0;
-
- this.getFriends = function () {
- return friends;
- };
-
- this.friend = function () {
- friends++;
- };
- }
-
- const student = new Person();
-
- student.friend();
- student.friend();
- console.log(student.getFriends());
- 复制代码
Person ,定义了一个变量 friends 用于保存状态。由于JavaScript的作用域规则的限制,因此只能在构造函数内部访问该变量。friends 变量直接进行读写,这就实现了私有变量了。JavaScript 也能支持私有变量;虽然闭包带给我们一定的好处,但是处理不好,很可能给我们带来严重的灾害。如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。。所以在开发中应尽量避免使用闭包。正所谓代码千万条,规范第一条,代码不规范,开发两行泪。
闭包就好像从 JavaScript 中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够到达那里。