04.闭包(Closure)

[toc]

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

下面就是我的学习笔记,对于Javascript初学者应该是很有用的。

一.引入闭包

需求: 点击某个按钮, 提示"点击的是第n个按钮"

  <button>测试1</button>
    <button>测试2</button>
    <button>测试3</button>
    <!--
需求: 点击某个按钮, 提示"点击的是第n个按钮"
-->
<script type="text/javascript">
    var btns = document.getElementsByTagName('button')
    //遍历加监听

    for (var i = 0, length = btns.length; i < length; i++) {
        var btn = btns[i]
        btn.onclick = function () {
            alert('第' + (i + 1) + '个')
        }
    }
</script> 

为什么会出现点击的不是依次对应的button,而是最后一个?

GIF 2021-6-21 10-11-48.gif

1.1 解决办法一:将btn所对应的下标保存在btn上

for (var i = 0, length = btns.length; i < length; i++) {
        var btn = btns[i]
        //将btn所对应的下标保存在btn上
        btn.index = i
        btn.onclick = function () {
          alert('第' + (this.index + 1) + '个')
        }
}

1.2 解决办法二:==利用闭包==

 for (var i = 0, length = btns.length; i < length; i++) {
        //立即执行函数
        (function (j) {
          var btn = btns[j]
          btn.onclick = function () {
            alert('第' + (j + 1) + '个')
          }
        })(i)
}

1.3 解决办法三:ES6新语法:let关键字加上代码块

for (let i = 0, length = btns.length; i < length; i++) {
        let btn = btns[i];
        btn.onclick = () => {
          alert(`第${i+1}个`);
        }
}

二.闭包的概念

2.1 MDN的官方解释

闭包

说人话就是只要一个函数内部引用了外部的数据,就形成了闭包。如:

function f1(){
  const a = 10
  function f2(){
    console.log(a) // f2引用了f1的数据a, 形成了闭包
  }
}

闭包的一个好处就是可以把数据安全地缓存起来,方便在一定的时候使用它。

2.2 闭包理解的总结

1.如何产生闭包?

==当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包==

2. 闭包到底是什么?

使用chrome调试查看

理解一: 闭包是嵌套的内部函数(绝大部分人)

理解二: 包含被引用变量(函数)的对象(极少数人)

注意: 闭包存在于嵌套的内部函数中

3. 产生闭包的条件?

函数嵌套

内部函数引用了外部函数的数据(变量/函数)

function fn1() {
    var a = 2
    var b = 'abc'

    function fn2() { //执行函数定义就会产生闭包(不用调用内部函数)
        console.log(a)
    }
    // fn2()
}
fn1()
function fun1() {
    var a = 3
    var fun2 = function () {
        console.log(a)
    }
}
fun1()

三.闭包的作用

闭包作用:

  1. ==使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)==
  2. ==让函数外部可以操作(读写)到函数内部的数据(变量/函数)==

闭包问题:

  1. 函数执行完后, 函数内部声明的局部变量是否还存在? 一般是不存在, 存在于闭中的变量才可能存在

  2. 在函数外部能直接访问函数内部的局部变量吗? 不能, 但我们可以通过闭包让外部操作它

四.闭包的生命周期

  1. 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)

  2. 死亡: 在嵌套的内部函数成为垃圾对象时

function fn1() {
    //此时闭包就已经产生了(函数提升, 内部函数对象已经创建了)
    var a = 2

    function fn2() {
        a++
        console.log(a)
    }
    return fn2
}
var f = fn1()
f() // 3
f() // 4
f = null //闭包死亡(包含闭包的函数对象成为垃圾对象)

五.闭包的原理

5.1 作用域和作用域链

为什么闭包能实现这样的效果呢,其原理在于js中存在作用域和作用域链。

1.作用域

变量或者函数的有效访问范围,一个变量或者函数只能在一定的范围内使用,我们称这个范围为作用域。

2.作用域链

多个作用域之间互相嵌套,就会形成一定的访问规则,这个规则我们称为作用域链

  • 当访问一个数据(变量或者函数)时,如果当前作用域存在这个数据,优先使用这个当前作用域内的数据
  • 如果当前作用域不存在这个数据,就会往上级作用域依次查找
  • 如果直到全局使用域都没找到,则会报错

闭包由两级至多级作用域组成。在一个局部作用域中声明了一个数据,于是只有这个作用域内部可以访问。所以我们在这个使用域内部再声明函数,此时内部的函数是能访问外层函数的数据的。如果我们想操作这个局部数据,就把一个函数返回出去,在外部接收到这个函数,于是就可以在外部通过内部函数操作内部数据了。

六.闭包的应用

  • 循环遍历加监听: 给多个li加点击监听, 读取当前下标
  • 模块化: 封装一些数据以及操作数据的函数, 向外暴露一些行为
  • JS框架(jQuery)大量使用了闭包

6.1 闭包的应用一:定义JS模块(写法一)

闭包的应用2 : 定义JS模块
  * 具有特定功能的js文件
  * 将所有的数据和功能都封装在一个函数内部(私有的)
  * 只向外暴露一个包信n个方法的对象或函数
  * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
function myModule() {
  //私有数据
  var msg = 'My atguigu'
  //操作数据的函数
  function doSomething() {
    console.log('doSomething() '+msg.toUpperCase())
  }
  function doOtherthing () {
    console.log('doOtherthing() '+msg.toLowerCase())
  }

  //向外暴露对象(给外部使用的方法)
  return {
    doSomething: doSomething,
    doOtherthing: doOtherthing
  }
}
var module = myModule()
module.doSomething()
module.doOtherthing()

6.2 闭包的应用二:定义JS模块(写法二)

(function () {
  //私有数据
  var msg = 'My atguigu'
  //操作数据的函数
  function doSomething() {
    console.log('doSomething() '+msg.toUpperCase())
  }
  function doOtherthing () {
    console.log('doOtherthing() '+msg.toLowerCase())
  }

  //向外暴露对象(给外部使用的方法)
  window.myModule2 = {
    doSomething: doSomething,
    doOtherthing: doOtherthing
  }
})()
myModule2.doSomething()
myModule2.doOtherthing()

6.3 模拟银行存取钱案例

比如我们要做一个银行里面的存取钱的效果,如果直接把数据暴露出来,会很危险

// 你存在在银行的钱
let money = 0
// 存钱
function add(n){
  money += n
}
// 取钱
function reduce(n){
  money -= n
}

此时是我们在全局使用域里面实现了一个数据,我们可以随意在控制台里面操作它

image-20210312172159919

我们就要想办法把这个数据"私有化",使用一个函数把它包起来

function f(){
  let money = 0
  function add(n){
    money += n
  }
  function reduce(n){
    money -= n
  }
}

此时money是一个局部变量,只有内部能操作了,而这时add和reduce函数已经形成了闭包。

但是通常写成这们是没有意义的,因为我们没法操作money了,所以我们希望在函数f之外操作money时,我们可以把add和reduce作为f函数的返回值,就可以在外面对money进行操作

又为了我们能看到money的当前值,我们给返回的数据加上一个用户用于查看余额的函数

function f(){
  let money = 0
  function add(n){
    money += n
  }
  function reduce(n){
    money -= n
  }
  function query(){
    console.log(money)
  }
  return {add,reduce,query}
}

此时只要我们调用一下,就能得到一个对象是,这个对象的方法可以操作money,别的方式操作不了了

let bank = f()
bank.add(100) // money:100
bank.reduce(100) // money: 0

沿着这个思路,我们把银行使用闭包进行模拟:

function f(){
  let vault = {} // 一个金库,存在所以人的钱,使用名字为键,金额为值
  // name 是用户的名字,value是要存的钱
  function add(name,money){
        if(typeof money !== 'number' && Object.is(money,NaN) && money <=0){
      return console.error("请输入正确的金额")
    }
    if (!vault[name]) vault[name] = 0
    vault[name] += money
  }
  function reduce(name,money){
    if(!(name in vault)) return console.error('没有该用户')
    if(typeof money !== 'number' && Object.is(money,NaN) && money <=0)
      return cosole.error('请输入正确的金额')
        if(money > vault[name]) return console.error('你没有这么多钱')
    vault[name] -= money
  }
  function query(name){
    console.log(vault[name])
  }
  return {add,reduce,query}
}

七.闭包的缺点及解决

7.1 内存溢出与内存泄露

7.1.1 内存溢出

一种程序运行出现的错误
当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误

7.1.2 内存泄露

占用的内存没有及时释放
内存泄露积累多了就容易导致内存溢出
常见的内存泄露:
    意外的全局变量
    没有及时清理的计时器或回调函数
    闭包

7.2 闭包的缺点

  1. 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
  2. 闭包容易造成内存泄露

7.3 闭包问题解决

  1. 能不用闭包就不用
  2. 解决: 及时释放 : f = null; //让内部函数对象成为垃圾对象
function fn1() {
    var arr = new Array[100000]

    function fn2() {
        console.log(arr.length)
    }
    return fn2
}
var f = fn1()
f()

f = null //让内部函数成为垃圾对象-->回收闭包

7.4 内存泄漏实例分析

闭包虽然可以把安全的把数据缓存下来,但是同样带来了一些问题。因为闭包引用和外部函数的数据,导致外部函数在运行结束时没法对里面的数据回收,会一直占用程序的内存,导致程序运行变慢。———— 我们管无法被回收的数据称为内存泄漏

至于为什么会内存泄漏,我们需要大概了解一下 垃圾回收机制

简单来说,每个数据会有一个被使用的标记,每当有一个地方使用这个数据,这个标记的数量就会+1,当这个标记为0时,就会进入等待回收的状态。

function fn(){
  var a = 10
  console.log(a)
}
fn()

上面这段代码,我们在调用fn时,会在第3行使用到了a,引用数量+1,使用过后,引用数量-1,这个时候就是0了,于是当fn调用完成,a这个数据就会进入回收流程。

但是如果我们这样写代码

function fn(){
  let a = 10
  function f2(){
    console.log(f2)
  }
}
let f = fn()

当fn执行完成后,发现在f2里面有使用到a,此时引用+1,本来在fn结束后,f2也要被回收的,但是发现在外面有一个f保存了f2的引用,f2的引用+1,所以导致f2不能被回收,也就导致了a不能被回收,于是造成了内存泄漏

如果当我们确实不需要使用这个闭包了,我们可以手动的把引用释放

f = null

此时f就不再对f2进行引用,f2的引用-1,f2的引用为0,当f2被回收后,就没有别的地方用到a了,a的引用-1,于是a的引用也变为0,最终a也被回收。

所以大家在使用闭包的时候,一定要注意:==因为不是所有时候我们都能注意到什么时候这个闭包不用了,手动释放的。因此能不用闭包,就尽量不要使用了。==

八.闭包面试题

8.1 面试题一:

//代码片段一
 var name = "The Window";
 var object = {
   name: "My Object",
   getNameFunc: function () {
     return function () {
       return this.name;
     };
   }
 };
 alert(object.getNameFunc()()); //?  the window


 //代码片段二
 var name2 = "The Window";
 var object2 = {
   name2: "My Object",
   getNameFunc: function () {
     var that = this;
     return function () {
       return that.name2;
     };
   }
 };
 alert(object2.getNameFunc()()); //?  my object

8.2 面试题二:

function fun(n, o) {
  console.log(o)
  return {
    fun: function (m) {
      return fun(m, n)
    }
  }
}
var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3) //undefined,0,0,0

var b = fun(0).fun(1).fun(2).fun(3) //undefined,0,1,2

var c = fun(0).fun(1)
c.fun(2)
c.fun(3) //undefined,0,1,1
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • ● 闭包基础 ● 闭包作用 ● 闭包经典例子 ● 闭包应用 ● 闭包缺点 ● 参考资料 1、闭包基础 作用域和作...
    lzyuan阅读 916评论 0 0
  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    lueyoo阅读 192评论 0 0
  • 什么是闭包? 闭包是指那些能够访问自由变量的函数。自由变量:指在函数中使用的,但既不是函数参数也不是函数的局部变量...
    BubbleM阅读 292评论 0 1
  • 引子 我们先来看一个例子: 上面代码中,函数f1可以读取全局变量n。但是,函数外部无法读取函数内部声明的变量。 上...
    oWSQo阅读 280评论 0 1
  • 一、引子 闭包(closure)是 Javascript 语言的一个难点,面试时常被问及,也是它的特色,很多高级应...
    1CC4阅读 112评论 0 1