前提摘要:
一千个读者,一千个哈姆雷特,每个人的理解不同,用我的话来讲述一下高频的问题,说不定同频的人更容易理解相应知识点,但是水平有限,也有很多是参考其他博主的总结出来的,如果有错误的地方,感谢指出!
1.什么是闭包:
背景(问题产生):
闭包其实不是什么很难理解的东西,听名字可能有些许抽象,但是弄懂之后会觉得这个名字取的也算精髓,首先肯定先产生问题再有解决方法,闭包同样也是解决问题的一个方法
我们知道全局变量的好处是会常驻,缺点是会污染环境,一旦一个全局变量被定义,那么其他人(功能模块)就不能使用了,当一个变量被不同的功能模块引用就会面临很多问题,尤其是被不同功能模块使用很可能会导致严重bug,例如var money = 500,我们将张三余额定义成一个全局变量,那么会面临那些问题呢?(为了方便理解,其他功能模块或方法可以理解成这里的人)首先,其他人都能拿到全局变量,也就是人人都知道张三有500余额,显然是不希望发生的,还有如果有人想坏心思,拿了张三200后将money改为money=300,因为全局变量,大家都能修改,如果今后这里不仅要记录张三的余额还要记录李四的,显然不可能也var money = 800,因为变量名已经被使用了,面临类似上述等等问题,我们知道了全局变量命名会污染环境,而且并不安全,虽然能够长期驻留
那么肯定有人会说,简单啊,将其封装到一个函数内不就好了,如下图代码,这样确实解决
// less 为张三每次去银行取钱数量
function zs(less){
var money = 500
money -= less
console.log('张三的余额',money)
return money
}
// less 为李四每次去银行取钱数量
function ls(less){
var money = 800
money -= less
console.log('李四的余额',money)
return money
}
try{
console.log(money)
}catch(err){
console.log('外部不能访问他人余额',err)
}
zs(0)
ls(0)
了变量命名的问题,由于函数作用域的问题,都可以命名成money了, 并且我们通过try catch语句知道了外部也不能随便获得他们的余额了,但是又有接下来一个问题了,张三和李四分别去银行取钱,代码和运行结果如下,得,张三和李四变成有钱人了,存进去的钱取不完,这显然是不希望
// less 为张三每次去银行取钱数量
function zs(less){
var money = 500
money -= less
console.log('张三的余额',money)
return money
}
// less 为李四每次去银行取钱数量
function ls(less){
var money = 800
money -= less
console.log('李四的余额',money)
return money
}
// 张三
zs(0)
// 第一次取钱
console.log('第一次取钱',zs(100))
console.log('----------------------------')
// 第二次取钱
console.log('第二次取钱',zs(100))
console.log('----------------------------')
// 第三次取钱
console.log('第三次取钱',zs(100))
console.log('----------------------------')
console.log('----------------------------')
// 李四
ls(0)
// 第一次取钱
console.log('第一次取钱',ls(200))
console.log('----------------------------')
// 第二次取钱
console.log('第二次取钱',ls(200))
console.log('----------------------------')
// 第三次取钱
console.log('第三次取钱',ls(200))
看到的,这是为什么呢?因为声明在函数中的变量,在函数执行完成后会被销毁,再次调用函数的时候又是新的函数了,换而言之就是变量不能够常驻,不像定义在全局函数那般,函数内部定义的变量在函数执行完成后会被销毁。由此我们可以看出,函数内定义变量虽然不会污染全局,并且不易被外界访问到,但是变量无法常驻。
闭包实例:
那么,我们既希望变量不污染全局环境,不容易被外界访问的同时还希望能够使得变量常驻,于是闭包就出现了,他就能够解决这个问题,代码示例如下,通过查看输出我们可以知道每次减少都是在上次的基础之上,所以可以得出变量是得以常驻了,通过try catch语句我们可以知道,变量也是无法在外部进行访问的,同样也不会污染全局,这样就达到了我们预期的结果。并且我们通过查看图3chrome调试工具可以知道确实money是作为闭包变量存在了。
function zs(){
var money = 500
function lessmoney(){
money -= 100
console.log('张三的余额',money)
}
return lessmoney
}
function ls(){
var money = 800
function lessmoney(){
money -= 200
console.log('李四的余额',money)
}
return lessmoney
}
// 张三
let zhangsan = zs()
try{
console.log('让我看看张三的余额',money)
}catch(err){
console.log('无法查看张三的余额',err)
}
// 第一次取钱
console.log('第一次取钱')
zhangsan()
console.log('----------------------------')
// 第二次取钱
console.log('第二次取钱')
zhangsan()
console.log('----------------------------')
// 第三次取钱
console.log('第三次取钱')
zhangsan()
console.log('----------------------------')
console.log('----------------------------')
// 李四
let lisi = ls()
// 第一次取钱
console.log('第一次取钱')
lisi()
console.log('----------------------------')
// 第二次取钱
console.log('第二次取钱')
lisi()
console.log('----------------------------')
// 第三次取钱
console.log('第三次取钱')
lisi()
闭包的定义:
前置:
在此之前先了解一下JavaScript中的GC(垃圾回收)机制,在 JavaScript 中,如果一个对象不再被引用,那么这个对象就会被 GC 回收,否则这个对象会一直保存在内存中
闭包:
严格意义上来讲闭包需要满足以下四个条件:
①有函数嵌套
②内部函数引用外部作用域的变量参数
③返回值是内部函数
④创建一个对象函数,让其长期驻留
①为什么需要函数嵌套呢?因为单层函数实例的时候,我们发现了函数每次执行都是一次新的函数,因为JavaScript中GC机制问题,因为没有人继续引用他,他会被销毁掉,所以每次执行取钱都会重新计算,但是当他的内部嵌套一个函数,并且②函数内引用了外部函数变量,且③返回值是内部函数,根据JavaScript的GC机制我们知道这个时候由于内部函数引用了外部函数变量,并且返回值是内部函数,④我们又创建了一个全局变量let zhangsan = zs(),也就是说zhangsan引用了内部函数,内部函数又引用了外部函数的变量,zhangsan是一个常驻的全局变量,所以根据GC机制,外部函数的变量不会被销毁,这是变量就得以保存下来了,所以④很关键,我们需要声明一个let zhangsan = zs()保持变量的常驻。
总结:
闭包顾名思义就是一个闭合的包,每一个包其实就相当于一个新的环境,这就像地球一样,每一个国家就是一个闭包,每个国家对于同一个东西可以有不同的定义,比如中国,龙是代表吉祥神圣,而欧美等会觉得龙是贪婪和暴虐,对于JavaScript来说,龙就是一个变量,只不过在中国这个闭包函数里面他是吉祥的,在美国或其他这个函数里面他是罪恶的。代码编程讲究模块化和解耦,闭包很大程度上和这种思想很像。闭包可以理解成一个更小的环境,当然闭包内的函数也非完全不可访问,依旧可以通过向外暴露或者return将数据传给外部环境。
2.什么是回调函数:
与闭包不同,我想先把回调讲明白再来说他的场景和用途,回调函数又叫callback函数,又是经典的顾名思义,callback意思就是打回来
示例1:
假设你要为你的老婆张三准备一个惊喜,所以你去玩具店定制一下物品,如同带有张三名字的气球、玩具熊、氛围灯等等,但是定制物品需要等待时间,并不能立马交到你的手上,当我们还有其他事情要做,比如买蛋糕,收拾卫生等等前置工作,也就是说你不会在这里等,于是你留下了你的电话号码,叫他好了callback就好,所以等他callback的时候,你才能拿定制物品去做剩下的操作,否则都是空谈,东西都没有,你拿到物品后如何处理和摆放,就是回调函数的内容,比如放到凳子底下,灯挂在顶上等等一系列操作都是回调函数的内容,但是这些的前提的定制物品到手,所以店员callback回来这个操作就是调用函数,时机到了,这就是callback函数。
实例2:
当你发送一个axios请求(axios是结合promise处理异步请求的),你会传入resolve, reject两种回调函数,分别是成功和失败需要执行什么?但是在这个请求没有完成之前你并不知道成功或者失败,需要请求返回才知道,这和定制物品一样,是异步操作,所以需要等待结果才能知道执行什么?if(res.status==200) 就执行resolve成功的回调
小总结:
回调函数常常和异步操作结合使用,但是并不意味这回调函数是异步操作,回调函数本身是同步的,和普通函数没有区别,回调函数就是将函数B作为参数传入到函数A中,这个时候就能说B是A的回调函数,与普通函数其实并没有太大差异,只是他的执行时机不同,回调函数同样也需要调用执行,而他的执行时机是当异步操作完成的时候执行,但是这样写法是非常利于编程的,就像定制物品一样,其实我们已经构思好了如何摆设,但是由于没有物品无法操作,有了回调函数,我们就可以将操作事先定义好,等到结果出来的时候,执行即可。
之所以需要回调函数,还有一个重要原因就是JavaScript是一门事件驱动语言,这意味他不会继续等待,当一个操作必须要等待上一个完成时,才能执行的时候,为了保证顺序执行,就需要用到回调函数,比如需要先找到钥匙才能开门出去,不能找钥匙是耗时操作就直接出门了,显然是不合理的,因为必须有钥匙才能开门,为了保证执行顺序,所以常常用到回调函数。
注意点:
1.函数一般考虑复用都会有名称,命名规则基本与功能相关,如下图1,当然如果不考虑复用性,便于书写可以省略名称,这种就叫做匿名回调函数。
function add(a,b,callback){
var c = a+b
callback(c)
}
function print(n){
console.log(n)
}
add(1,2,print)
图1
function add(a,b,callback){
var c = a+b
callback(c)
}
add(1,2,function(n){
console.log(n)
})
图2
2.为了方便书写,ES6提供了更为简便的写法,那就是箭头函数,不过最终他也会通过babel解析成普通函数的类型,这种就是为了便于编写,代码如下图3,但是这里就引出了一个知识点,那就是
function add(a,b,callback){
var c = a+b
callback(c)
}
add(1,2,n=>{
console.log(n)
})
图3
函数this的指向问题,箭头函数的this指向是从书写完成的时候就已经决定了,是距离他外层最近的一层的this指向,而图1和2的代码书写方式,this的指向就是他的调用者