欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JavaScript中闭包的相关总结

程序员文章站 2022-03-27 12:48:20
目录1、认识闭包* 什么是闭包?* 如何产生闭包?* 如何查看闭包对象2、闭包的使用* 常见的闭包及闭包作用* 闭包的生命周期* 产生多个闭包3、闭包的缺点及解决方案4、面试题* 题1* 题1答案及解析* 题2* 题2答案及解析1、认识闭包* 什么是闭包? - 代码例:// 什么是闭包function fn1() { // 定义fn1函数 let a = 2 // fn1函数体内声明变量a,值为2 let b = 'abc' // fn1函数体内声明变量b,值为'abc' let c...


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