#JavaScript 闭包问题详解 #打倒心魔
闭包
网络上很多资料,都有描述闭包,解释的又繁琐又复杂。 这里尝试用自己的理解去解释一下,什么是闭包。对我个人来讲,也是谈闭包色变,看了很多遍,每次都觉得自己看明白了,几天不见,又懵逼了。 而且每次看别人的文章都感觉理解起来很困难。 大家惯用的做法是将一个已经很抽象的问题,进一步抽象成一个看似好理解的模型,实际上真的很痛苦啊喂!! 所以在参考了众多相关问题后,我打算自己总结一下,下次忘记就可以很快的回溯了。
关于闭包的问题,我觉得,要先从广义上去理解,然后落地到代码实现上, 此外,比起什么是闭包这个概念,明白它有哪些用处,以及为什么需要用到它,才是最重要。 毕竟,往往不好理解的东西通常用着用着就理解了。
1. 到底,什么是闭包?
在JavaScript 中,有作用域链的概念, 总结的讲,其实就是,子作用域能够向上访问父作用域中的变量,反之则不行。
let str = 'hello world!';
function func(){
let name='henrry'
console.log(str);
}
func();//hello world!
console.log(name);// name is not defined
那么,有没有一种办法,能够让我们,能够从父级作用域去访问子作用域中的变量呢?我们把能够从父作用域成功访问到子作用域的这种情况,称之为 “闭包” 。
它其实,是一种间接访问子作用域中变量的方式。以下代码,是一个基本的实现:
function outer() {
let str = "hello world!"
function inner() {
return str;
}
return inner;
}
let recOuter = outer();
console.log(recOuter());//hello world
代码说明: 在这段代码中, 有三个作用域我们一定要先明确。 第一个就是最外层的作用域(记作S1), 即outer
函数被定义的这一层; 第二个就是outer
函数体内,也是inner
函数被定义的那一层(记作S2); 第三个就是inner
函数体内(记作S3)。 我们希望在 S1 访问到 S2 中的 str
这个变量, 如果直接访问是不被允许的, 因此,我们在S2所在层,创建 了一个新的函数 inner
这个函数什么事情也不做,只是如果被执行将会把 str
返回,紧接着,我们又返回了inner
这个定义好的函数,等待被触发。 现在,如果outer
函数触发,我们将会得到 inner
函数,如果我们再触发返回的inner
函数, 就会获取到str
的值。 在最外层S1, 我们创建了recOuter
变量用以接收 outer
函数的返回值,即 inner
, 然后我们执行了inner
函数(recOuter()
) , 输出了 "hello world"。
这个过程其实为了尽可能的详细,描述的有些繁琐。 其实,如果从代码上看,反而简单的多。 就是在outer
函数内部定义了一个函数专用于返回outer
函数体中定义的变量。 这整个过程其实就做了一件事,把outer
函数内的 str
返回到S1 作用域 ,(换句话说,就是:在S1作用域内访问到S2中的str变量)。 这就是闭包。
2. 其实你每天的代码都在闭包
为什么这么说呢?类似以下的代码,应该是每天都有写的吧?
function calcGap(a, b) {
if (a > b) {
return a - b;
} else {
return b - a;
}
}
let num1 = 2;
let num2 = 9;
let result = calcGap(num1, num2)
console.log("the gap is:", result);//7
那么重新审视一下这段及其普通的代码, 如果不直观,稍作改动:
function calcGap(a, b) {
if (a > b) {
let gap = a - b;
return gap;
} else {
let gap = b - a
return gap;
}
}
let num1 = 2;
let num2 = 9;
let result = calcGap(num1, num2)
console.log("the gap is:", result);
且不用管if...else作用域的问题,他们是块级作用域,就当作和calcGap
函数体在同一个作用域就好。
我们最终需要的,是这个gap
值,而这个值定义在 calcGap
函数内部,我们在外部想要打印它,不就是在尝试“在外层作用域去访问子作用域中的变量"吗? 这不就是闭包吗。 这就是闭包!所以,其实我们每天都在写。
3. 利用闭包可以有哪些实现?
其实这个问题,远比 ”闭包究竟是什么“ 重要的多, 因为概念容易忘记, 通过不断的去实现,去实践,从能真正的反过来去理解”闭包“。 作为初学者,其实最困难的就是对它一知半解,也不是完全不知道,能说出个大概,可就是感觉哪里没理解透彻,正是这个原因。 缺少实践。
阮一峰对闭包的用处是这样描述的:
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
https://ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
这两个应用方向,是从本质上去描述的,它能做什么。
- 读取子作用域变量
- 让变量保持在内存中
关于第二点,有一个很好的示例,一个是个日常场景 —— 计数器
这个例子也更够帮助我们更进一步的理解闭包的特点。
实现1 : 计数器
假设我们需要执行一个函数,counter就加一, 如果不考虑闭包,一般会是这样:
let counter = 0;
function count() {
console.log(counter++);
}
count();//0
count();//1
count();//2
这样虽然简单,但是也存在一些问题,或者说有值得优化的地方。 在我们的代码逻辑中,可能会有很多类似 counter
这样的变量,它没别的什么作用,只是维护了一个计数值。 如果逻辑代码很多的时候,这种类似的变量可能会有很多个,在同一个作用域中,如果有类似功能的变量,甚至还需要为同样功能的变量取不同的变量名,例如 counter1, counter2... 这显然,不够酷 。
那么,我们能不能让整个计数逻辑成为一个整体呢? 这时候就需要依靠闭包的实现了:
function count() {
let counter = 0;
console.log(counter++);
}
count();//0
count();//0
count();//0
我们首先希望,要是能把这个变量放在函数体内部,就不会和其他变量相互影响了。 但是直接放进去肯定是不行的,我们发现了问题,因为函数一执行,counter
就被重置为了0 , 这样以来就达不到计数的效果了。 怎么办 ? 要是能够保存这个counter
值的状态就好了! 且看:
function count() {
let counter = 0;
function closureFunc() {
return counter++;
}
return closureFunc;
}
let closureF = count();
console.log(closureF());//0
console.log(closureF());//1
console.log(closureF());//2
这就是一个计数器的实现,最值得我们去刨析的是,它为什么会保存状态 ?
let closureF = count()
, closureF
变量 就接收到了count
函数的执行返回值 ——函数 closureFunc
。
因为在最外层,我们创建了 closureF 这一个变量,它会保持对函数 closureFunc 的引用, 因此只要变量closureF 没有被销毁, 函数closureFunc
也不会被销毁, 连带着, counter
也不会被销毁。它们将会始终保存在内存中, 所以就保存了状态。 这属于JavaScript 垃圾回收机制内容。
也正是因为这个原因,所以在调用的使用需要先定义一个变量closureF
保持对count()
返回值的引用,这样就返回值就不会被回收掉。就能实现保存状态的目的。 不要想当然地这样去调用:
console.log(count()());//0
console.log(count()());//0
console.log(count()());//0
这样做并不会保存状态,因为执行完毕就会被回收机制回收掉。
怎么主动断开引用,让它被回收呢? 在退出逻辑块的时候,将其置为null
以释放掉内存。
function count() {
let counter = 0;
function closureFunc() {
return counter++;
}
return closureFunc;
}
let closureF = count();
console.log(closureF());//0
console.log(closureF());//1
console.log(closureF());//2
closureF = null;//释放当前的变量, 使得`closureFunc`被回收掉
这也引出了闭包的优点和缺点的讨论:
4. 闭包的优缺点
优点: 不污染变量,让逻辑代码更整体。
缺点: 如果闭包函数所定义的上下文中(作用域)存在着很多定义的变量,那么这些变量都会被持续保存状态,而不会触发JavaScript回收机制,白白占用内存,在ie中还有可能造成内存溢出。 所以在闭包的使用上,需要花费程序设计成本以尽量减少不必要的内存占用。
注:对于缺点的说明,“闭包函数所定义的上下文中(作用域)存在着很多定义的变量”, 这个在以上示例中,就值得是
let counter = 0;
, 在实际开发中,可能这里有一堆变量。
5. 闭包的更多应用实现
相比较,什么是闭包,闭包有哪些实现才是最重要的。
关闭闭包的更过应用实现,见:https://www.bilibili.com/video/BV1z7411v7T1?p=2&spm_id_from=pageDriver