在了解闭包以前,咱们要清楚一点。咱们了解闭包,不是为了去有意的建立闭包,实际上咱们在写代码的过程当中,就会无心的建立不少闭包,咱们要作的只是了解和熟悉,在写代码的时候知道写出来的是闭包,而后在出现一些奇怪的bug的时候能正确找到它们。segmentfault
在开始前,先看一个小栗子数组
问题:每一个一秒分别打印 0、一、2闭包
解法1:异步
(function foo(){ for (var i = 0; i< 3; i++) { setTimeout(function fn1 (){ console.log(i) }, 1000 * i); } })()
预期结果: 0、一、2
实际结果: 三、三、3
是否是很奇怪,前面这段代码看起来是没什么问题啊,输出结果怎么会不对?
这个疑问先放一放,咱们先来看一下今天的主角,“闭包”同窗函数
直接上代码:spa
function fn1() { var a = 2; function fn2() { console.log(a); } return fn2; } var fn3 = fn1(); fn3();
上面这段代码中作了三件事情:
1⃣️ 函数 fn1 执行
2⃣️ fn1 的返回值(执行结果)被赋值给fn3
3⃣️ fn3(也就是fn2) 执行,打印变量 aprototype
fn1执行前,引擎先为fn1建立了一个活动对象,而后塞进内存中。咱们知道,js引擎有垃圾回收机制,会释放再也不使用的内存空间,等fn1执行完以后,按理说前面建立的活动对象已经没用了,这个时候引擎会将该活动对象回收。code
可是这里并不会,由于fn2内部引用的变量a是存活在fn1活动对象中的,也就是说fn2引用了fn1活动对象中的a,这也就使得fn1活动对象不会被销毁,仍然存活在内存中。( 能够理解为:引擎准备清理fn1活动对象的时候,发现还被别的对象引用着,说明它还有用,就放弃回收它了 )对象
由于fn1活动对象不会被销毁,等到fn3执行的时候,须要获取a的值并打印,就能正常获取和打印了。blog
总的来讲就是,fn2持有了对fn1的引用,致使fn1执行完以后活动对象没有被销毁,这个现象就叫作闭包。
了解完闭包,如今咱们来分析一下文章开头那段输出结果不对的代码:
// 原代码: (function foo(){ for (var i = 0; i< 3; i++) { setTimeout(function fn1 (){ console.log(i) }, 1000 * i); } })()
对上面的代码作个拆解:
// 拆解后: (function foo(){ var i i = 0 // 第一次循环 此时 i === 0 if(i < 3){ // setTimeout 执行,定时器开启 // 可是因为fn1是异步代码,因此fn1会等到全部同步代码执行完成后再执行 setTimeout(function fn1 (){ console.log(i) }, 0); // 1000 * 0 } // i 自增 i++ // 第二次循环 此时 i === 1 if(i < 3){ setTimeout(function fn1 (){ console.log(i) }, 1000); // 1000 * 1 } i++ // 第三次循环 此时 i === 2 if(i < 3){ setTimeout(function fn1 (){ console.log(i) }, 2000); // 1000 * 2 } i++ // 这里 i === 3 ,不知足判断条件 i < 3 ,才会跳出循环 })()
如今咱们再去看,for循环建立了三个定时器,每一个定时器分别有一个回调函数fn1。
仔细看这里的每一个fn1函数中输出的变量i都是引用的外层函数foo的,根据咱们讨论闭包得出的结论,因为函数fn1引用了外层函数foo的变量i,因此fn1持有了对外层函数foo的引用,致使了foo函数的活动对象不会被销毁。
因此这段代码中会产生3个闭包,关系以下图:
三个fn1函数虽然分别产生了三个闭包,可是引用的是同一个外层函数foo的值,因此咱们能够理解为三个闭包都是共享的。三个函数使用的是同一个父级做用域下的变量 i ,因此异步函数fn1 执行时,获取到的是同一个i值(i === 3)
这个栗子拆解是为了讲闭包,同时为了方便后面其余代码的讲解,因此用闭包的思路去分析。可是实际关键节点仍是异步问题,小伙伴们不要钻牛角尖。
说到这里,眼尖的小伙伴可能已经发现了,那既然是取的时候已是3了,那我每次进循环的时候,都把当前的i值存一下行不行?行啊,固然行,这就是咱们接下来要说的。
如今再回去看一下最开始的代码快,而后对代码作个改造。
for (var i = 0; i< 3; i++) { setTimeout(function fn1 (){ console.log(i) }, 1000 * i); }
经过前面的讨论,咱们能够肯定,由于这里的fn1函数和相同的父级做用域造成了共享闭包。因此为了解决这个问题,咱们能够给每一个fn1函数外面再包一层做用域,拆分红三个独立小闭包。
改造:
for (var i = 0; i< 3; i++) { (function foo (i) { // 这里加一层当即执行函数 setTimeout(function fn1 (){ console.log(i) }, 1000 * i); })(i) // 每次循环的时候,都给 foo 的 i 赋值 }
拆解:
var i i = 0 if(i < 3){ (function foo (i) { setTimeout(function fn1 (){ console.log(i) }, 0); // 1000 * 0 })(i) // i === 0 (⚠️:当即执行函数,因此代码执行到这里的时候,就已经把foo内部的i给赋值成0了) } i++ if(i < 3){ (function foo (i) { setTimeout(function fn1 (){ console.log(i) }, 1000); // 1000 * 1 })(i) // i === 1 } i++ if(i < 3){ (function foo (i) { setTimeout(function fn1 (){ console.log(i) }, 2000); // 1000 * 2 })(i) // i === 2 } i++ // 异步函数执行
关系图:
下面的代码和前面用自调用函数拆分闭包的道理是同样的,区别只是把函数做用域变成了块级做用域。
⚠️:使用let关键字,会隐式的建立块级做用域
改造:
for (let i = 0; i< 3; i++) { // 这里的 var 改为 let setTimeout(function fn2 (){ console.log(i) }, 100); }
拆解:
var i i = 0 if(i < 3){ let j = i setTimeout(function fn1 (){ console.log(j) }, 0); } i++ if(i < 3){ let j = i setTimeout(function fn1 (){ console.log(j) }, 1000); // 1000 * 1 } i++ if(i < 3){ let j = i~~~~ setTimeout(function fn1 (){ console.log(j) }, 2000); // 1000 * 2 } i++ // 异步函数执行
关系图:
简单的聊一下函数柯里化,柯里化其实就是闭包的一种利用。
好比说咱们要实现这样的一个效果:
实现一个函数,能够不停的往里传string,直到传入句号,结束并返回全部string拼接的结果。
例子:
strConcat('H') strConcat('e', 'll') strConcat('o', ' ', 'W') strConcat('o') strConcat('rl') strConcat('d','.') // 输出 Hello Word.
实现:
function fn1() { let arr = [] // ** 重要:返回函数 concat ** return function concat() { // 拿到参数数组 const arg = Array.prototype.slice.call(arguments) // 将参数存到外层做用域下的arr中 // 因为闭包的缘由,每次concat执行时,arr都会保持上一次操做结果 arr = arr.concat(arg) // 接到终止参数,则返回拼接字符串 if(~arg.indexOf('.')){ const result = arr.join('') console.log(result) return result } } } // ** 重要:这里 strConcat === concat ** const strConcat = fn1() strConcat('H') strConcat('e', 'll') strConcat('o', ' ', 'W') strConcat('o') strConcat('rl') strConcat('d','.') // 输出 Hello Word.
柯里化其实就是利用闭包的原理,实现的一个相似于一个小仓库的效果。
咱们用包子工厂举个栗子:
能够看到,实际就是利用了 fn1 活动对象不会被销毁的特色,把fn1当成了一个临时仓库,等全部包子原料sring加工完以后(偷懒了,个人贴的代码里没有加工的过程,可是道理是同样的),再统一输出。
定义:词法做用域就是定义在词法阶段的做用域。
解释:词法阶段也就是词法分析阶段(预编译阶段)。换句话说,词法做用域是在代码执行前就已经肯定的做用域。
也就是说,词法做用域在代码执行前就被肯定了,因此词法做用域是不会由于代码的执行而改变的。(其实仍是有办法改变的,好比用eval搞一些奇怪的事情,可是这不在咱们讨论范围内了,咱们就当它不变的就行了)
上面的图中颜色深浅不一的三个地方就是三个词法做用域,它们是彻底包含的关系,1⃣️ > 2⃣️ > 3⃣️
1⃣️ 包含foo函数所在的做用域,也就是全局做用域2⃣️ 包含foo函数所建立的做用域,也就是bar函数所在的做用域3⃣️ 包含bar函数建立的做用域