正如《你不知道的 JavaScript》书中所言:
闭包就好像从 JavaScript 中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能到达那里!
在实际的开发工作中也确实如此,除了在面试的场景下,或其它几个少数特定的场景下(如“防抖节流”函数),咱意识到了 —— 这就是“闭包”!其它时候基本不用,或者是用了却不知道。
多么可惜!!这么好的东西用了却不自知。
本篇借助 medium 上的五万赞好文 I never understood JavaScript closures 带你一次吃透“闭包”!(吃不透找我,找耶稣也没用,我说的 QAQ)
看完本篇,你会惊奇的发现,竟然连以下这段代码都存在着闭包?!
let a = 1
function b(){
return a
}
console.log(b())
“我永远不懂 JS 闭包,因为它无处不在......”
认知“闭包”前,咱得了解一个重要的概念 —— “执行上下文”!
执行上下文分为:
比如我们在全局执行上下文中调用一个函数的时候,JS 解析的流程大概会是这样的:
return
或 }
时,判定为执行结束。关于执行栈的操作,可见下面的动图理解:
我们再结合下面这段代码举例解析,来看看 JS 一步一步到底是怎么做的:
1: let a = 3
2: function addTwo(x) {
3: let ret = x + 2
4: return ret
5: }
6: let b = addTwo(a)
7: console.log(b)
这里虽然是一段非常冗长的解释,且还并未涉及到“闭包”,但是却是我们必须要写在前面并且理解透彻的。
接着“执行上下文”,还有一个概念我们需要理解,它就是 “词法作用域”!
来看下面这段代码示例:
1: let val1 = 2
2: function multiplyThis(n) {
3: let ret = n * val1
4: return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)
你能参照上一节的解释对这段代码进行描述吗?
这段描述中,置灰的步骤就是和上一节的描述基本一致,未置灰的是
最重要的是:JS 在当前执行上下文寻找变量的时候,如果找不到,就会去调用这个执行上下文的上一执行上下文去寻找。
这并不难理解,这样链式查找变量的过程,就是 JS 的【作用域链】。
函数可以返回任何东西,当然也就包括返回另一个函数了。
让我们再按照以上解析一遍以下代码:
1: let val = 7
2: function createAdder() {
3: function addNumbers(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return addNumbers
8: }
9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)
看了以上三段的具体步骤详细分析,我相信再给你一段类似的调用代码,你也一定能通晓其中端倪,作出类似的解析!
1:let counter
2: function createCounter() {
3: let counter = 0
4: const myFunction = function() {
5: counter = counter + 1
6: return counter
7: }
8: return myFunction
9: }
10: const increment = createCounter()
11: const c1 = increment()
12: const c2 = increment()
13: const c3 = increment()
14: console.log('example increment', c1, c2, c3)
来试试吧?
counter = counter + 1
,counter 变量在新创建的函数执行上下文找的到吗?找不到!只得回到调用它的全局执行上下文去寻找,在全局执行上下文 counter 为 undefined,所以执行 counter = undefined + 1
;
究竟是这样的吗? 我们在控制台打印:
结果竟然与我们分析的期望打印相悖!
发生了什么?
一定还有一个神秘的东西在作用!没错,它就是 “闭包”!the missing piece !
它的原理是这样的:
当我们声明一个函数时,存储以供调用,存储的不仅仅是这个函数的定义,同时还有这个函数的“闭包”,闭包包括了这个函数执行上下文所有变量的词法作用域。这些在函数被创建时就已经确定下来了。
做个不太恰当的比喻,把函数理解为一个人,当这人生下来的时候(函数创建时),也附赠了一个背包(闭包),这个背包包括了家庭环境(词法作用域)。
所以我们上一段的逐步分析是错的!
我们再来进行一次正确的分析!
1:let counter
2: function createCounter() {
3: let counter = 0
4: const myFunction = function() {
5: counter = counter + 1
6: return counter
7: }
8: return myFunction
9: }
10: const increment = createCounter()
11: const c1 = increment()
12: const c2 = increment()
13: const c3 = increment()
14: console.log('example increment', c1, c2, c3)
counter = counter + 1
,counter 变量在新创建的函数执行上下文找的到吗?找不到!我们要去它的“闭包”里面找一找!,竟然是有的!它的值为 0 ,那么就可以执行 counter = 0 + 1,等于 1;
噢~原来是这样!
当一个函数声明的时候不单单只做了声明,后面还带着个“闭包”呢!闭包里装的是这个函数执行上下文所有变量的词法作用域!
原来这就是闭包!
你可能会疑问:只要一个函数进行了声明,就包含了闭包,那全局函数是不是也有闭包呢?
答案是:YES !
全局函数声明时,也有闭包!只不过它的变量词法作用域就是全局的,所以不是“闭”的不是很明显。
当一个函数返回另外一个函数时,“闭包”是最明显的!返回的函数的变量词法作用域可以访问非全局范围的变量,它们仅放在其闭包中!
这里再放几道闭包题目,供大家自查测验:
// 题目一
let c = 4
function addX(x) {
return function(n) {
return n + x
}
}
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
// 题目二
function multiply(number1, number2) {
if (number2 !== undefined) {
return number1 * number2;
}
return function doMultiply(number2) {
return number1 * number2;
};
}
multiply(4, 5);
const double = multiply(2);
double(5);
// 题目三
function showBiBao() {
for (var i = 0; i < 5; i++) {
setTimeout( timer => () {
console.log(i);
}, 1000 );
}
console.log(i)
}
showBiBao()
function showListNumber() {
for(var i = 0; i < 5; i++) {
let ret = function(i) {
setTimeout(function timerr() {
console.log(i)
}, 1000)
}
ret(i)
}
console.log(i)
}
showListNumber()
我相信如果你从头至尾认真跟着分析步骤来了,这一定不算啥大问题~
欢迎讨论 ~
通过本篇我们知道了:只要有函数申明的地方就有闭包!只不过有时候“闭”的不那么明显。
为什么题目说《我永远不懂 JS 闭包》呢?其实你也看到了,开发工作中,即使你没有用到闭包或者根本不认识闭包也一样摸鱼打卡上下班。但问题的关键是闭包吗?
不!
你以为本篇是在讲闭包?错!本篇是在讲执行上下文!
你以为本篇是在讲执行上下文?错!本篇是在讲词法作用域!
你以为本篇是在讲词法作用域?错!本篇是在讲 JS 的动态语言特性!
你一问本篇是在讲 JS 的动态语言?错!本篇是在讲 JS 运行的单线程特性!
......
没错,哎,就是玩儿~
撰文不易✍,点赞鼓励👍,你的反馈💬,我的动力🚀
我是掘金安东尼,关注公众号【掘金安东尼】,关注前端技术,也关注生活,持续输出ing!