JavaScript中闭包的相关总结
目录
1、认识闭包
* 什么是闭包?
- 代码例:
// 什么是闭包
function fn1() { // 定义fn1函数
let a = 2 // fn1函数体内声明变量a,值为2
let b = 'abc' // fn1函数体内声明变量b,值为'abc'
let c = true // fn1函数体内声明变量c,值为true
function fn2() { // fn1函数体内声明函数fn2
console.log(a) // fn2函数体内打印输出变量a
console.log(c) // fn2函数体内打印输出变量c
}
return fn2 // 返回fn2
}
fn1() // 执行fn1函数
这段代码写了个fn1函数,内部声明3个变量和1个函数fn2
最后执行了fn1,却没有执行fn1内部的fn2,但fn2内部却有着2条打印输出变量a和c的语句未执行
闭包是什么?
1、闭包是“包含被引用变量/函数的对象”,上方代码中,fn1函数里的内部函数fn2内的两条语句分别引用了变量a和c,而这两个变量由一个对象所接收,这个对象就是闭包(示例:{a: 2, c: true} )
·
2、闭包存在于嵌套的内部函数中
闭包是存在于嵌套的内部函数中的某个对象
在上方代码例当中,那个对象内容是 { a: 2, c: true },该对象存在于return 返回的fn2函数之中
下方代码例展示了闭包的另一种写法
- 代码例:
// 什么是闭包-2
function fn1() { // 定义fn1函数
let a = 2 // fn1函数体内声明变量a,值为2
let b = 'abc' // fn1函数体内声明变量b,值为'abc'
let c = true // fn1函数体内声明变量c,值为true
function fn2() { // fn1函数体内声明函数fn2
console.log(a) // fn2函数体内打印输出变量a
}
function fn3() {
console.log(c) // fn3函数体内打印输出变量c
}
return fn3 // 返回fn3
}
fn1() // 执行fn1函数
这边的代码例中,定义了2个内部函数,在各自的函数体内分别操作到了变量a和c,最后return 返回了fn3,那么fn3中的闭包对象的内容是什么呢?
fn3中的闭包对象 { a: 2, c: true }
虽然fn3自身的函数体中没有使用到a,但是其他函数体使用到的变量也在这个闭包对象之中。
可以这么理解
- 闭包对象类似于一个公共池,存放所有内部函数所使用到的数据(变量/函数)
- return的那个函数相当于一位代表,把这个公共池带在自己身上并且展示出来
- 当然也可以return多个函数(多位代表),只是每位代表各自的函数体不同
* 如何产生闭包?
- 代码例:
// 如何产生闭包
function fn1() { // 函数fn1
let a = 2
function fn2() { // 函数fn1里的函数fn2
// ① 函数fn1里如果没有任何函数的话,比如把fn2整个删了,那就不存在闭包了
console.log(a) // fn2函数体内打印输出fn1作用域的变量a
// ↑ 如果把上面这行代码删了,也不存在闭包了
// ② 内部函数要引用外部函数的变量/函数
}
return fn2 // 返回fn2
// ↑ ③ 在外部函数的作用域内,return 返回内部函数名称
}
fn1() // 执行fn1函数
// ↑ 如果把上面这行代码删了,也不会存在闭包
// ④ 必须要执行外部函数
产生闭包需要同时满足条件1、2、4
1、函数内存在其他函数(函数有嵌*象)
2、内部的函数要引用外部函数的数据(变量/函数)
3、return 内部函数名称
4、必须要执行外部函数
关于第3点的说明:
即便不return 内部函数名称,但满足1、2、4就可以“产生”闭包了,但这个闭包无法在较新版本的Chorme debug工具内查看到(2017年以前的老版本似乎可以)
* 如何查看闭包对象
如何查看闭包对象:
闭包对象可以通过Chrome浏览器的debug工具内的Sources一栏,进行断点调试时于右侧信息栏查看,比如存在于外部函数fn1里的内部函数fn2里的闭包对象,查看时信息栏内大致是以下结构:
Scope
└
Local
└
a: 2
fn2: {
...,
...,
...,
[[Scopes]]: Scopes[2]
0: Closure (fn1){ ← 这个Closure就是闭包对象,此处的fn1就是执行的外部函数名称
a: 2 ← 内部函数fn2使用外部函数的变量a
}
1: Global {...}
}
2、闭包的使用
* 常见的闭包及闭包作用
- 代码例:
// 常见的闭包及闭包作用
function fn1() { // 函数fn1
let a = 2
function fn2() { // 函数fn1里的函数fn2
a++ // fn2函数体内对fn1作用域的变量a自增
console.log(a) // 打印输出a
}
function fn3() { // 函数fn1里的函数fn3
a-- // fn3函数体内对fn1作用域的变量a自减
console.log(a) // 打印输出a
}
return fn3 // 返回fn3函数对象
}
let f = fn1() // 声明变量f,并将fn1执行结果返回给f
// 由于fn1的执行结果是返回fn3函数对象,因此执行f()相当于执行fn3()
// fn1作用域内的变量a为2,而fn3里将a进行自减,最后还打印输出a
f() // 打印输出a为 1
f() // 打印输出a为 0
以上代码是闭包的常用方式
- 在函数嵌套的同时,外部函数将"内部函数名称"作为返回值返回
声明一个变量并接收该返回值- 由于此变量接收的是一个"函数对象",因此可以直接执行此变量
但是在这边,你是否想过这2个问题:
1、fn1函数执行完毕后,函数体内部都会自动释放,包括这个变量a,但是上方代码最后却能成功让a自减并打印输出,这是为什么呢?
2、f() 是在全局执行的,为何能访问到fn1函数内部的a?
一般情况下,函数执行完都会自动释放,并且是无法在全局访问到函数内部的数据,但是利用闭包,可以解决这些问题
闭包作用
- 1、可以使函数内部的变量,在函数执行完后,不会自动释放,仍然存活在内存中(延长了局部变量的生命周期)
- 2、在函数外部可以操作到函数内部的数据(变量/函数)
我们可以再看一下刚才那段代码
- 代码例:
// 常见的闭包及闭包作用-2
function fn1() { // 函数fn1
let a = 2
function fn2() { // 函数fn1里的函数fn2
a++
console.log(a)
}
function fn3() { // 函数fn1里的函数fn3
a--
console.log(a)
}
return fn3 // 返回fn3函数对象
}
let f = fn1()
// ↑ 这行代码是关键,首先是执行了fn1函数
// fn1函数执行完后,返回fn3函数对象,确切地说,是fn3所对应的某个地址值,包含着fn3函数的一些内容
// 而这个内容之中,包含了变量a的数据
// 这个内容被变量f所引用
// 而其他没有被引用的内容,“函数名fn3,函数名fn2及其对应的地址值”,均被自动释放,成为垃圾对象了
* 闭包的生命周期
闭包是何时产生,又是何时死亡的呢
- 产生:在嵌套内部函数定义执行完时就产生了(不是在调用)
- 死亡:在嵌套的内部函数成为垃圾对象时
- 代码例:
// 闭包的生命周期
function fn1() { // 函数fn1
// 产生:由于函数提升,此时已经产生了闭包
let a = 2
function fn2() { // 函数fn1里的函数fn2
a++
console.log(a)
}
return fn2
}
let f = fn1() // 变量f引用返回值fn2的内容
// ...此处对f进行一系列操作...
f = null // 死亡:变量f赋值null,包含闭包的函数对象成为垃圾对象
* 产生多个闭包
- 外部函数被调用多少次,就产生了多少个闭包
- 每次调用外部函数后的返回值(函数地址),都是不一样的,可以赋值给不同的变量
- 由于赋值后的变量所引用的返回值也是不同的,因此彼此之间操作数据是不会影响到各自的闭包对象内的数据
- 代码例:
// 产生多个闭包
function fn1() { // 函数fn1
let a = 2
function fn2() { // 函数fn1里的函数fn2
a++
console.log(a)
}
return fn2
}
let f = fn1() // 变量f引用返回值fn2的内容(此处产生第一个闭包)
f() // 3
f() // 4
let f2 = fn1() // 变量f1引用返回值fn2的内容(此处产生第二个闭包)
f2() // 3
3、闭包的缺点及解决方案
闭包的优点在于:
1、可以从外部操作函数内部的数据
2、延长相关数据在内存中的占用时间
第2点同时也可能变成缺点,比如这个相关数据本身,非常非常大,而且就一直占用在内存里边,这时候需要去释放它
- 代码例:
// 闭包的缺点及解决方案
function fn1() { // 函数fn1
let arr = new Array[1000000] // arr是一个拥有一百万个空对象的数组
function fn2() { // 函数fn1里的函数fn2
console.log(arr.length) // 计算arr的长度
}
return fn2
}
let f = fn1() // 变量f引用返回值fn2的内容
f() // 第一次计算并输出arr的长度
f() // 第二次计算并输出arr的长度
f() // 第三次计算并输出arr的长度
// ...... 第n次
// 如此一来,这些数据就会变得很多而且很大
// 但是之后已经用不到这些数据了,此时需要释放掉这些数据
f = null // 让内部函数成为垃圾对象-->回收闭包
4、面试题
* 题1
// 请回答以下代码的输出结果
let name = "window"
let object = {
name : "object",
getNameFunc : function(){
return function(){
return this.name
}
}
}
alert(object.getNameFunc()()) // ?
let name2 = "window"
let object2 = {
name : "object2",
getNameFunc : function(){
let that = this
return function(){
return that.name
}
}
}
alert(object2.getNameFunc()()) // ?
* 题1答案及解析
// 请回答以下代码的输出结果
// 1
let name = "window"
let object = {
name : "object",
getNameFunc : function(){
return function(){
return this.name
}
}
}
alert(object.getNameFunc()()) // 'window'
// 首先,getNameFunc()是外部函数,里面return了一个内部函数
// 并且内部函数没有使用到外部函数的任何变量/函数
// object.getNameFunc()先调用了这个方法,返回内部函数
// 然后直接把这个内部函数执行了
// 谁去直接执行的呢?没有指定对象的情况下,都是window
// return 的this就是window,window里找name,当然返回 'window'
// 2
let name2 = "window"
let object2 = {
name : "object2",
getNameFunc : function(){
let that = this
return function(){
return that.name
}
}
}
alert(object2.getNameFunc()()) // 'object2'
// 首先,getNameFunc()是外部函数,里面return了一个内部函数
// 但内部函数使用了外部函数所定义的变量that,这个that的指向对象为 object2
// 之后先执行了object2.getNameFunc()这个外部函数,执行结果返回了内部函数
// 由于满足条件,产生了闭包对象,that被保存了下来
// 之后再执行了返回的内部函数,内部函数返回值为 that.name
// that指向对象是object2,也就是object2.name,最终输出 'object2'
* 题2
// 请回答以下代码的输出结果
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n)
}
}
}
let a = fun(0) // ?
a.fun(1) // ?
a.fun(2) // ?
a.fun(3) // ?
let b = fun(0).fun(1).fun(2).fun(3) // ? ? ? ?
let c = fun(0).fun(1) // ? ?
c.fun(2) // ?
c.fun(3)// ?
* 题2答案及解析
// 请回答以下代码的输出结果
function fun(n,o) { // 外部函数: fun(n, o)
console.log(o) // 外部函数执行后,先打印输出o的值
return { // 之后返回一个对象,里面有个内部函数
fun:function(m){ // 内部函数: fun(m)
return fun(m,n) // 内部函数执行后,返回外部函数并执行 fun(m, n)
}
}
}
// 这道题考验的是 1、形成闭包的条件 2、闭包的生命周期
// 先看这道题是否满足形成闭包的几个条件
// 1、有外部函数和内部函数 √
// 2、内部函数使用了外部函数的数据(变量/函数) √ 就是n
// 3、将内部函数return √
// 4、执行外部函数 ?
// 对于这道题,其实无论是直接执行外部的fun还是内部的fun,都是执行了外部函数
// 内部函数返回的 fun(m, n) 就是在执行外部函数
let a = fun(0) // 输出 undefined
// fun(n, o) -> fun(0, o) 没传o的值,当然输出undefined
// 这边第一次执行后,a就是返回的对象,同时保存了闭包 n = 0
a.fun(1) // 输出 0
// 分析:
// 执行对象里的内部函数 -> fun(m) -> fun(1)
// 内部函数返回又执行了外部函数 fun(m, n) -> fun(1, 0) -> fun(n, o)
// 由于再次执行外部函数,原本赋给内部函数m的值,变成了外部函数n的值
// (上一次执行结果的闭包n的值,同时也在这一次外部函数执行中变成了o的值)
// 此时产生新闭包 n = 1
// fun(n, o) n = 1, o = 0, 执行console.log(o) 当然是0
// - if未保存闭包
// - 由于因为没有任何变量去引用保存这个函数对象,因此闭包对象随函数对象一起自动释放掉了
// - 之后无论执行多少次,由于产生的新闭包马上释放了,输出值永远都是上一次保存的闭包值
// * if保存了闭包
// * 如果有变量引用保存了闭包值,那么根据以上流程,本次保存的闭包值会于下次执行内部函数后输出
a.fun(2) // 输出 0 分析同 a.fun(1)
a.fun(3) // 输出 0 分析同 a.fun(1)
let b = fun(0).fun(1).fun(2).fun(3) // 输出 undefined 0 1 2
// 分析:
// let b = fun(0) 输出 undefined,分析同 let a = fun(0) ,此时闭包 n = 0
// fun(1) 输出 0
// fun(1) 分析: 参考 a.fun(1) ,此时闭包 n = 1,由于有变量b引用,闭包存活
// fun(2) 输出 1
// fun(2) 分析: 参考 a.fun(1) ,此时闭包 n = 2,由于有变量b引用,闭包存活
// fun(3) 输出 2
// fun(3) 分析: 参考 a.fun(1) ,此时闭包 n = 3,由于有变量b引用,闭包存活
let c = fun(0).fun(1)
c.fun(2)
c.fun(3)
// 输出undefined 0 1 1
// 分析:
// let c = fun(0) 输出 undefined,分析同 let a = fun(0) ,此时闭包 n = 0
//
// fun(1) 输出 0
// fun(1) 分析: 参考 a.fun(1) ,此时闭包 n = 1,由于有变量c引用,闭包存活
// c.fun(2) 输出 1
// c.fun(2) 分析: 参考 a.fun(1) ,此时闭包 n = 2,由于没有变量引用保存,闭包随函数对象释放死亡
// c.fun(3) 输出 1
// c.fun(3) 分析: 参考 a.fun(1) ,此时闭包 n = 2,由于没有变量引用保存,闭包随函数对象释放死亡
- Recorded by Scorpio_sky@2020-10-24
本文地址:https://blog.csdn.net/Scorpio_sky/article/details/109198826