奋斗这种事是需要动机的
一是对贫穷的恐惧
二是对美好生活的向往
前言
无论你是前端老鸟还是即将遭受社会“毒打”的前端萌新,闭包(Closures)都是一个在你获得心仪offer路上的拦路虎。
在前面的几篇文章中,我们从不同的角度来分析了何为闭包等。
文章链接 | 查看顺序 |
再谈JS闭包 | 基础篇 |
兄台:JS闭包了解一下 | 进阶篇 |
然而,纸上得来终觉浅,绝知此事要躬行。So,为了避免成为那种眼高手低的人。这篇文章,通过几个常见的闭包应用场景评估一下,是否真正的懂闭包。
一图胜千言
直接拿来主义了,这是前几篇中,关于闭包的一些简单汇总。
文章概要
- Q1:慧眼认“包”
- Q2: 参数为何凭空消失
- Q3:作用域嵌套惹的祸
- Q4:迟到的兑现,错误的值
- Q5:变量的从一而终
- Q6:公私分明
- Q7:柯里化
1:慧眼认“包”
仔细分析下面的各个方法clickHandler
/immediate
和delayedReload
,哪些方法属于闭包。
clickHandler
let countClicks = 0; button.addEventListener('click', function clickHandler() { countClicks++; }); 复制代码
immediate
const result = (function immediate(number) { const message = `number is: ${number}`; return message; })(100); 复制代码
delayedReload
setTimeout(function delayedReload() { location.reload(); }, 1000); 复制代码
答疑解惑:
代码解析 | 结果 |
clickHandler 访问了外部作用域内的变量countClicks |
clickHandler是一个闭包 |
immediate 没有访问任何的外部作用域变量 |
未形成闭包 |
delayedReload 访问了全局作用域的全局变量:location |
delayedReload 是一个闭包 |
Q2: 参数为何凭空消失
(function immediateA(a) { return (function immediateB(b) { console.log(a); })(1); })(0); 复制代码
答疑解惑:
代码运行后,控制台打印了
0
immediateA
被调用时,参数的值为0,也就是说 a
参数的值为 0。
immediateB
内嵌在immediateA
函数内部,并且访问了其外部作用域(immediateA
作用域)的变量a
,所以immediateB
是一个闭包。在函数调用时, 变量a
为0,所以在控制台输出结果也是0。
Q3:作用域嵌套惹的祸
let count = 0; // ① (function immediate() { if (count === 0) { let count = 1; console.log(count); // 输出结果? } console.log(count); // 输出结果? })(); 复制代码
答疑解惑:
代码运行后,控制台依次输出
1
0
在①行出,通过let count =0
声明了变量 count
且值为0。
immediate
是一个闭包:它访问了位于它外部作用域内的变量count
。 在immediate
函数作用域内部,count
的值一直都是0。
但是,在条件判断语句下形成的块级作用域里面,另一个let count =1
的语句,声明了一个局部变量count
,该变量重写(覆盖)了外部作用域的count
。所以在局部作用域内部,count
的值为1,也就是第一个打印结果是1。
而第二个打印结果是0。因为,第二个打印语句的所能访问到的范围是immediate作用域
-->全局作用域
。 它没有权限访问位于其头上的块级作用域(子作用域)。
这里需要简单强调下:作用域是可以嵌套的,详情请参考再谈JS闭包
Q4:迟到的兑现,错误的值
for (var i = 0; i < 3; i++) { setTimeout(function log() { console.log(i); // 输出结果? }, 1000); } 复制代码
答疑解惑:
代码运行后,控制台依次输出
3
3
3
该代码执行过程,可以分成2个阶段
阶段一
for
迭代了3次。每次迭代,都新建函数(log()
),而在函数中,都访问了变量i
。setTimeout()
将log()
函数延迟1000ms触发- 当
for()
循序执行完时,变量i
已经变成3
阶段二
该阶段发生在1000ms之后
setTimeout()
执行被延后处理的log()
函数。而log()
访问的变量i
已经变成3了。 所以,三次打印都是3
解决方案
这里多说一嘴,知道病因是啥,还需要能够对症下药。
ES5解决方案:利用IIFE
IIFE:英文名是Immediately Invoked Function Expression
中文名是立即调用函数表达式,简称为 立即执行函数
---请用小沈阳语言包阅读以上内容
for (var i = 0; i < 3; i++) { (function (a){ setTimeout(function(){ console.log(a); },1000) })(i); } 复制代码
其实,这种处理方式,也是在利用闭包特性。
在setTimeout
外层又包了一层,此时 IIFE中的参数a
的值就是当时i
的值。而log
中又引用了变量a
。 也就是说,通过IIFE,让某个时刻的变量i
和log
产生了关联。也就是每次打印的时候,都是当时绑定的值。
ES6解决方案: 使用Let声明变量
for (let i = 0; i < 3; i++) { setTimeout(function log() { console.log(i); }, 1000); } 复制代码
Q5:变量的从一而终
function createIncrement() { let count = 0; function increment() { count++; } let message = `Count is ${count}`; function log() { console.log(message); } return [increment, log]; } const [increment, log] = createIncrement(); increment(); increment(); increment(); log(); // 输出结果 复制代码
答疑解惑:
代码运行后,控制台输出
0
increment()
函数被调用了三次,变量count
的值更新为3
。
message
位于createIncrement
函数作用域内。它的初始值为Count is 0
。即使 Count 变量增加了几次,message
总是保持初始值。
log()
函数是一个闭包,访问了createIncrement
函数作用域内的值message
。 而message一直没有发生变化。所以,无论increment()
被调用任意次,count
被增加到任意数值。message
的引用count
的值,都是其初始值。
Q6:公私分明
构建了一个用于生成栈(stack)数据结构的函数。
function createStack() { return { items: [], push(item) { this.items.push(item); }, pop() { return this.items.pop(); } }; } const stack = createStack(); stack.push(10); stack.push(5); stack.pop(); // => 5 stack.items; // => [10] stack.items = [10, 100, 1000]; // 封装性被破坏了 复制代码
对实例进行push/pop
处理,都符合stack的操作特性。但是,存在一个小小的瑕疵。如果有人蓄意对items
进行直接赋值处理,那么原先的操作都会被抹杀,对于一个成熟的数据结构来讲,这是不允许的。
所以,我们需要将针对items
的操作,进行过滤处理。或者说将其对外隐藏。
利用闭包,使得items
的属性对外不可见。
function createStack() { const items = []; return { // ① push(item) { items.push(item); }, pop() { return items.pop(); } }; } const stack = createStack(); stack.push(10); stack.push(5); stack.pop(); // => 5 stack.items; // => undefined 复制代码
变量items
被移动createStack
函数作用域内部。也就是说items
是私有变量,而在return
对象(①行所在位置)的各个方法(push
/pop
)是公共变量。
对外开放的方法中,访问了createStack
作用域内的变量items
,从而形成了闭包。
Q7:柯里化
现在有一个需求,让你写一个用于计算两数乘积的函数multiply
function multiply(num1, num2) { // bala bala } 复制代码
如果multiply(num1, numb2)
在调用的时候,有两个满足要求的参数(都是Number类型),那么就返回两数乘积(num1 * numb2
)
如果调用multiply
的时候,只传入了一个参数,那需要返回另外一个函数(const anotherFunc = multiply(num1)
),而返回的函数,又可以接受另外一个参数(num2
)。被调用时,返回的是num1 * num2
的乘积。
需要满足下面的各种情况。
multiply(4, 5); // => 20 multiply(3, 3); // => 9 const double = multiply(2); double(5); // => 10 double(11); // => 22 复制代码
话不多说,直接上代码。
function multiply(number1, number2) { if (number2 !== undefined) { return number1 * number2; } return function doMultiply(number2) { return number1 * number2; }; } 复制代码
当number2 !== undefined
时,说明multiply
调用时,传入了两个参数。
当number2 == undefined
时,说明multiply
调用时,只传入了一个参数,此时函数返回了接收另外一个参数的函数doMultiply
。 而在返回的函数中,访问了multiply
作用域的变量number1
,形成了闭包。
也就会出现,虽然multiply(number1)
被调用执行后,在返回的doMultiply
函数中,依然能够访问到变量number1
。
--
后记
参考资料: