JS–闭包
闭包详解
闭包,在MDN(闭包 - JavaScript | MDN (mozilla.org))中的解释是
一个函数和对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。
也就是说,闭包可以让你在一个内层函数中访问到其外层函数的作用域。在JavaScript中,每当创建一个函数,闭包就在函数创建的同时被创建出来。
在讨论闭包前,我们先看下下面的代码,并且用文字描述出代码的工作流程
1.function add(x) {
2. let sum = x + 1
3. return sum
4.}
5.let myadd = add(1)
6.console.log(myadd)
-
在第1行到第4行,我们在全局上下文中声明了一个名为add的变量,并且给这个变量分配了一个函数定义。而第2行和第3行是函数的内容。需要注意的是,这个分配的操作并不会导致函数的内容被执行到,只是将函数定义的内容的引用给到了add变量。题外话–如果并不想把函数定义的内容的引用给到了add变量,而是直接执行函数定义的内容的话,可以用以下代码
(function add(x) { let sum = x + 1 console.log('成功执行,值为:' + sum) return sum }(1)) // 控制台输出【成功执行,值为:2】
在JS中,(function( ) { } ( ))是一个表达式,会强制理解成函数直接量方式,即表达式方式创建函数;而(function( ) { })则返回函数对象的引用,可用小括号直接调用,比如(function ( ) { } ) ( )
-
第5行会稍微复杂一些,首先,声明了一个名为myadd的变量,变量一经声明,其值为undefined(var变量类型)/ uninitialize(let\const变量类型)。
-
还是在第5行,进行了一个赋值操作,给变量myadd赋一个新值。而且被赋的这个值是一个被调用的函数,需要注意的是如果只是函数的话,那么myadd被赋的值就是函数的内存地址的引用;但如果是被调用的函数的话,则myadd变量存的是函数执行后的返回值。
-
但是在执行前,js会在全局执行上下文内存中查找名为add的变量。于是,js就在第1行到第4行找到了它。需要注意的是,我们这边是直接传了一个值作为参数,但是如果传的是变量的话,则要在全局上下文中找到这个变量并传参过去。之后就可以准备执行函数了。
-
在执行函数时,会先进行执行上下文的切换,js会创建一个新的本地上下文,叫“add执行上下文”,执行上下文被推送到调用堆栈中。
-
有意思的来了,切换上下文后,我们做的第一件事是什么?是在“add执行上下文“中声明了新变量sum吗?不是,我们需要先看函数的参数。在“add执行上下文”中声明了一个变量x并将作为参数传参的值1赋值给它。
-
下一步,就是在”add的执行上下文“中声明一个新的变量sum,其值为undefined(var变量类型)/ uninitialize(let\const变量类型)。在第2行中,执行了一个加法操作,首先我们要先找到x的值,在"add的执行上下文"中找到值为1,第二个操作数为1,两个进行加法可得2,之后被赋值到变量sum。
-
第3行,我们返回变量sum的值,在”add的执行上下文“中找到为2,返回,到此函数结束。
-
当函数结束后,”add的执行上下文“会被销毁,变量x和sum都找不到了。返回值2被赋值到了myadd中,最后在控制台中输出2。
这是JS在运行以上代码时的文本描述。六行代码,运行的细节等却极为繁多。但是,我们对整个运行流程多少有了点认识。
接下来,我们开始进入闭包的世界。首先,我们要先知道什么是词法作用域。在MDN(闭包 - JavaScript | MDN (mozilla.org))中,词法作用域是根据源代码中变量的位置来确定该变量在何处可用。
换句话说,词法作用域是JS查找变量的解决方案。当全局执行上下文存在变量A,函数执行上下问存在变量A的话,那么JS会先在执行流所处的函数执行上下文中寻找变量A(即你需要寻找变量时所处的上下文环境,有可能是函数执行上下文、也可能是全局执行上下文),如果找不到,就到上一层的执行上下文中找,一直往上一层找,直到在全局执行上下文中查找为止。如果到最后还是找不到,那么它就是undefined。
我们改动下之前举例说明的代码demo
1.let num = 1
2.function add(x) {
3. let sum = x + num
4. return sum
5.}
6.let myadd = add(1)
7.console.log(myadd)
- 在全局执行上下文中,声明了名为num的变量,并赋值为1
- 第2行到第5行,声明了名为add的变量,并赋值了一个函数定义
- 第6行,则是声明了名为myadd的变量。之后从全局执行上下文中寻找名为add的变量,并将其作为一个函数执行,将1的值作为参数传递。
- 在函数被执行前,js会创建一个函数执行上下文,名为”add的执行上下文“。之后声明一个名为x的变量并将参数传递过来的值赋值给它。
- 第3行,声明了一个名为sum的变量,之后在”add的执行上下文“中寻找名为x的变量,找到x的值为1。再在”add的执行上下文“中寻找名为num的变量,找不到,于是向上一层寻找,上一层为全局执行上下文,找到num的值为1。执行加法操作,将值2赋值给sum。
- 到此,函数执行结束,”add的执行上下文“被销毁,x的变量被销毁,但是num的变量还存在,因为它是全局执行上下文的一部分。myadd变量被赋值为2。之后在控制台输出2。
一个函数可以访问在它的调用上下文中定义的变量,这个就是词法作用域。即是在全局执行上下文中调用的函数,可以访问到全局执行上下文中的声明定义的变量。
好了,重头戏来了。让我们再把那个代码demo例子改一下
1.function add() {
2. let sum = 0
3. return function (a, b) {
4. sum += a + b
5. return sum
6. }
7.}
8.let myadd = add()
9.const add1 = myadd(1,1)
10.const add2 = myadd(2,2)
11.const add3 = myadd(3,3)
12.console.log(add1, add2, add3)
- 在第1行到第7行,我们在全局执行上下文中声明了一个名为add的变量,并赋值一个定义好的函数。
- 在第8行,于全局执行上下文中声明了名为myadd的变量,之后在全局执行上下文中找名为add的变量,并将其作为函数调用。
- 在执行函数前,会先创建”add的执行上下文“,之后查看是否有参数传入到函数,没有传入则直接执行函数。接下来,声明了名为sum的变量,并赋值0。最后返回一个函数给到myadd变量。
- 需要注意的是控制权交给全局执行上下文时,add函数返回了函数定义和它的闭包。闭包中包括了创建它时在作用域内的变量。
- 第9行,声明一个变量add1,并调用myadd变量,会在全局执行上下文中找myadd变量,将其作为函数调用。这时候进入到函数,会先创建变量a和b,并将参数赋值过去,之后在第4行,会先在函数执行上下文中寻找变量sum,找不到后会在闭包中寻找,可以得到变量sum的值为0。之后再在函数执行上下文中寻找a和b变量的值,可得公式0 + 1 + 1 = 2,进行赋值后sum变量的值为2,最后再返回sum变量的值,变量add1被赋值为2。
- 第10、11行都和第9行的过程基本一致。
- 第12行,会在控制台输出2,6,12。
关于闭包的还有个点需要注意一下----
如果闭包函数返回的是值(包括字符串、数值、数组等),而没有把闭包函数直接返回或者将闭包函数放在对象中返回(或者说返回了但是并不进行调用),则第一次调用闭包函数之后返回的闭包会被一直使用。
举个例子–
1.function add() {
2. let sum = 0
3. return function () {
4. sum++
5. return sum
6. }
7.}
8.let myadd = add()
9.console.log(myadd())//输出1
10.console.log(myadd())//输出2
11.console.log(myadd())//输出3
上面的例子的不把闭包函数返回并调用的,可以看出,每一次调用myadd函数,其中的sum变量都指向同一个值(或者说同一个内存地址),每一次调用该函数,都会使sum变量的值加1。
下面的例子则是返回了闭包函数并进行了调用----
1.//这是一个闭包函数,从第2行到第10行
2.function fun(n, o) {
3. console.log(o)
4. //返回一个对象,其中的fun1属性的值是一个匿名函数,返回闭包函数
5. return {
6. fun1: function (m) {
7. return fun(m, n);
8. }
9. };
10.}
11.var a = fun(0).fun1(1); // 输出undefined、0
12.a.fun1(2); // 输出1
13.a.fun1(3); // 输出1
从上面的结果我们可以看出,如果每一次调用fun1函数,都是访问同一个闭包里的变量o的话,那么我们期望的值会是这样的
a.fun1(2); // 输出1
a.fun1(3); // 输出2
a.fun1(4); // 输出3
但实际上并不是。这是因为第一次调用闭包函数之后返回了一个对象,且对象中的fun1的值是被调用的闭包函数。在调用fun1函数时,会先调用闭包函数并将其返回值(即包含闭包函数的对象以及闭包)返回。每一次调用闭包函数都会创建一个新的闭包并和返回值一块返回。所以fun1(3)中变量o的值是在新创建的闭包中发生改变,而旧的闭包中o的值还是0,而不是1。如下图所示
闭包形成的条件以及优缺点
以下内容(闭包形成的条件以及优缺点)来自于(17条消息) 闭包的形成,闭包的优点和缺点,闭包有哪些作用?_Mr_qinyivue的博客-CSDN博客
1.函数A里面直接或间接返回函数B
2.函数B中使用了函数A的变量
3.函数A外部有一个变量接受函数B,形成一个不会销毁的函数空间
闭包优缺点:
1.延长了变量的生命周期
优点:变量能够一直存在并被使用。在实际开发中,如果需要对数据进行频繁操作,或者有长期保留数据的需要,并且不希望对外开放的话,那么可以通过闭包实现。
缺点:因为执行空间不会销毁,变量一直存在,会一直占用内存空间。
2.可以访问到函数内部的私有变量
优点:闭包能够访问到函数内部的私有变量,可用于封装变量、限制权限等需求。
缺点:因为执行空间不会销毁,变量一直存在,会一直占用内存空间。
需要特别注意的是,因为闭包的执行空间会在内存中一直存在,如果闭包占用过多时,会导致内存溢出,结果就是内存泄漏,所以如果有别的方式实现你延长变量的生命周期或者访问函数内部的私有变量的需要,就尽量不使用闭包。请慎用!
闭包的经典应用–防抖、节流函数
以下内容来自于(17条消息) JS–防抖函数、节流函数_上班汪的博客-CSDN博客
//防抖函数
function debounce(fun, wait = 200, immediate = false) {
let timeout = null
return function () {
const that = this
const args = arguments
timeout ? clearTimeout(timeout) : (immediate && fun.apply(that,args))
timeout = setTimeout(()=>{
!immediate && fun.apply(that,args)
timeObj = null
},time)
}
}
//节流函数
function throttle(fun, wait = 200, immediate = false) {
let timeout = null
return function () {
const that = this
const args = arguments
if(!timeout) {
immediate && fun.apply(that,args)
timeout = setTimeout(()=>{
!immediate && fun.apply(that,args)
timeout = null
},wait)
}
}
}
其它经典应用请看这位大佬的https://www.jianshu.com/p/9eb30b6af3a1(JS 闭包的 9 大经典使用场景)
我看了之后才知道,原来迭代器可以用闭包来实现,感觉很有意思,决定了,任务清单加一!!!