第九章 这次把JS闭包给你讲的明明白白?

1.什么是环境与作用域

  1. 当一个环境不被需要的时候,它就会被回收
  2. 全局环境不会被自动回收,除非关闭浏览器等操作(人为回收)

2.函数的环境与作用域原理

所谓环境,可以理解为一块内存数据。

所以全局环境就是可以辐射全局的内存数据。

函数环境:

  1. 当我们声明一个函数的时候,计算机就会给这个函数开辟一块新的内存空间(环境),在这个内存空间中定义的数据,只能在这个内存空间使用。
  2. 我们每次调用函数的时候,计算机都会开辟新的内存空间。

3.延伸函数环境生命周期

当我们声明一个函数“show”时,这时候系统并不会给它分配内存空间,当我们调用“show”函数的时候,这时候系统就会给她分配内存空间了。

那我们对此调用这个函数时,他的“n”会累加吗?明显是不会的,因为每次调用都是一块新的内存空间,想多与开辟了多块土地,每一块土地上都有一个长得一样,但是却并非为同一个的“n”。

并且这些函数在执行完成后就会被销毁。

相当于我们玩王者荣耀,可能每一局都选李白,但是他们并不是同一个李白~~~

  function show(){
    let n = 1
    function sum(){
      console.log(++n)
    }
    sum()
  }
  show()// 2
  show()// 2

如果我们不希望这个函数被销毁,希望函数中的“n”可以一直累加,那我们怎么办呢?我们可以定义一个对象接收这个函数的返回值,这时候系统就会给“a”开辟出一块新的内存空间,无论我们调用多少次“a”,都是在这一块内存空间中执行的,所以它永远不会被销毁。

类似的,我们再定义一个“b”将“show”赋值给他,此时也会给“b”开辟出来一个新的内存空间,所以此时调用“b”他就是“2”了。

那我们在定义一个“c”,并且将“a”赋值给“c”会怎么样呢?此时,系统并不会为“c”单独开辟一块新的内存空间,这是因为我们只是将“a”的内存空间的地址赋值给了“c”,所以“a”和“c”指向的是同一块内存地址,所以我们调用“c”的时候就会返回“4”了。

  function show(){
    let n = 1
    return function(){
      console.log(++n)
    }
  }
  let a = show()
  a()// 2
  a()// 3
  let b = show()
  b()// 2
  let c = a
  c()// 4

那么更复杂的情况呢?

我在“sum”函数内部又定义了一个“sumInner”函数,显然,我们直接调用“show”函数,“m”的值只能返回“2”,这里不多说了。

  function show(){
    function sum(){
      let m = 1
      function sumInner(){
        console.log(++m)
      }
      sumInner()
    }
    sum()
  }
  show()// 2
  show()// 2

当我们声明一个对象的情况呢?根据我们上面的描述,我们可以知道“a”的两次调用均是在同一块内存地址,那么为什么我们的结果还是没有累加呢?原因在于,我们的“show”函数虽然在同一个内存地址调用了两次,但是在这调用的两次中,“sum”函数并没有被外部引用,所以在两次“show”函数的调用中,系统都为“sum”函数分配了新的内存空间,所以此时“m”的数值并没有发生变化(或者说这个本不是同一个“m”)。

  function show(){
    return function sum(){
      let m = 1
      function sumInner(){
        console.log(++m)
      }
      sumInner()
    }
  }
  let a = show()
  a()// 2
  a()// 2

那如果我们希望在调用“show”函数的时候只给“sum”函数分配一次内存空间呢?此时我们将“sum”和“sumInner”函数都返回出去,并且调用两次“show”函数,这样就能解决这个问题了。

  function show(){
    return function sum(){
      let m = 1
      return function sumInner(){
        console.log(++m)
      }
    }
  }
  let a = show()()
  a()// 2
  a()// 3

4.构造函数中的作用域的使用形态

构造函数中的作用域与普通函数相同,当我们声明一个新的对象的时候,系统就会给这个新的对象开辟一块新的内存地址,并且每一个对象都有自己的内存地址。

  function show(){
    let n = 1
    this.sum = function(){
      console.log(++n)
    }
  }
  let a = new show()
  a.sum()// 2
  a.sum()// 3
  let b = new show()
  b.sum()// 2
  b.sum()// 3

5.什么是块作用域

ES6中新推出了两种声明变量的方式:“let”和“const”,这两种方式声明的变量只能在自身的块作用域中使用,而“var”则没有这个限制。

  {
    let a = 1
    const b = 2
    var c = 3
    console.log(a)// 1
    console.log(b)// 2
    console.log(c)// 3
  }
  console.log(a) // a is not defined
  console.log(b) // b is not defined
  console.log(c) // 3

6.let-var在for循环中执行原理

 当我们使用“var”声明“i”

for(var i = 0;i <= 3;i++){
  setTimeout(function(){
    console.log(i)// 4 4 4 4
  },1000)
}

 当我们使用“let”声明“i”

for(let i = 0;i <= 3;i++){
  setTimeout(function(){
    console.log(i)//0 1 2 3
  },1000)
}

我们发现两次循环看似相同,但是输出的结果却截然不同,这是因为使用“var”声明的“i”定义在了全局,使用全局的内存地址存储,所以当一秒钟后打印“i”时,全局作用域下的“i”已经变成“4”了,所以打印结果就是四个“4”了(这里为什么不是“3”而是“4”,是因为for循环是线进行i++再进行判断,所以此时全局的“i”就变成“4”了);而当使用“let”定义时,存在块作用域的概念,每一次执行循环中的函数,都会重新生成一块新的作用域,将“i”放置进去,当一秒钟后进行打印时,会在每一个作用域分别打印“i”,所以就输出了“0 1 2 3”。

7.模拟处var的伪块级作用域

前面说过,我们可以用语言的特性来解决问题。这里我们可以定义一个匿名函数,这个函数有一个形参,函数内部输出这个形参,我们在每次循环的时候调用这个函数,并且将“i”的值传递给这形参,这样在每次调用这个函数的时候都相当于传递了不同的“i”值,以此来达到伪块级作用域的效果。

for(var i = 0;i <= 3;i++){
  (function(a){
    setTimeout(function(){
      console.log(a)
    },1000)
  })(i)
}

PS:这里和函数作用域有关系吗???不太清楚,但是我认为我描述那种理解方式是没有问题的,但是可能浅显了一些,还是得向底部挖掘。

8.块级作用域嵌套详解

和上面一样,我们使用“let”声明的时候“i”在“for”循环的块级作用域中使用。

  let arr = []
  for(let i = 0;i <= 3;i++){
    arr.push(function(){
      return i
    })
  }
  console.log(arr[2]())

 我们使用“var”声明的时候“i”在全局作用域中使用。

  let arr = []
  for(var i = 0;i <= 3;i++){
    (function(a){
      arr.push(function(){
      return a
    })
    })(i)
  }
  console.log(arr[2]())

值得一提的是,使用函数回调的方式将内容push到数组中,在这个循环结束后这个块仍不会被销毁,因为这个块中的内容仍在被“arr”所使用。

9.什么是闭包

闭包:我们的函数可以访问到其他函数作用域当中的数据,称为闭包。

10.使用闭包获取数字区间

利用的特性,在between执行完成后,拿到filter方法的默认参数,返回的函数利用这个参数进行判断,从而完成区间选取。

  let arr = [1,22,13,44,51,62,79,89,96]
  function between(a,b){
    return function(v){
      return v >= a && v <= b
    }
  }
  console.log(arr.filter(between(10,50)))// [22, 13, 44]

11.移动动画的闭包使用

利用这个案例,我对于闭包顿悟了。在“setInterval”定时器内定义的函数中,“item”是没有被定义的,系统回向上级寻找这个变量,在添加事件的方法中没有找到,所以就再向上到forEach方法中寻找,此时“item”被找到,“setInterval”中使用的“item”正是来源于此。

<body>
  <button message="魏主任">weizhuren</button>
  <button message="耳鼻喉科">erbihouke</button>
</body>
<script>
  let btns = document.querySelectorAll("button")
  btns.forEach(function(item){
    item.addEventListener("click",function(){
      let left = 1
      setInterval(function(){
        item.style.left = left++ + "px"
      }, 5);
    })
  })
</script>

12.动画为什么会抖动呢?

在上面我们做了一个移动button按钮位置的小案例,在这个案例中有一个问题,就是反复(两次及以上)点击按钮时,按钮就会从移动位置和当前位置之间来回抖动,那是因为什么呢?

原因是每当我们点击一次按钮,都会重新执行一次函数,此时就会开辟出一块新的内存空间,这块新的内存空间中的“left”为“1”,所以按钮就会不断从不同内存空间读取“left”,造成抖动。

所以我们可以将“left”定义在forEach作用域下,“setInterval”作用域下的“left”在自己作用域找不到,就会向上寻找,找到forEach作用域下的left,因为forEach只调用了一次,所以只开辟了一个内存空间,每次调用点击事件时,拿到的都是同一个“left”,这样就不会造成抖动的情况了。

  let btns = document.querySelectorAll("button")
  btns.forEach(function(item){
    let left = 1
    item.addEventListener("click",function(){
      setInterval(function(){
        item.style.left = left++ + "px"
      }, 5);
    })
  })

13.动画加速的原因

抖动的问题解决了,但是我们会发现又出现了一个问题,那就是多次点击按钮后,按钮的移动速度会增加,这是因为多次点击后,定时器就会被多次调用,内部的函数也会多次调用,造成移动速度越来越快。

我们可以使用一个标记(interval )进行判断,当这个标记为false时,点击事件才会绑定定时器中的方法,一旦调用一次以后,标为变为真,这个定时器就再也不会被调用了。

  let btns = document.querySelectorAll("button")
  btns.forEach(function(item){
    let left = 1
    let interval = false
    item.addEventListener("click",function(){
      if(!interval){
        interval = true
        setInterval(function(){
        item.style.left = left++ + "px"
      }, 5);
      }
    })
  })

如果考虑到这个方法,并且为了避免污染环境中的数据池,可以定义一个标记,并且在点击事件中定义“left”。

  let btns = document.querySelectorAll("button")
  btns.forEach(function(item){
    let bind = false
    item.addEventListener("click",function(){
      if(!bind){
        bind = true
        let left = 1
        setInterval(function(){
        item.style.left = left++ + "px"
      }, 5);
      }
    })
  })

PS:这个思想就是在第一次点击时分配一个内存地址并且完成所有操作,在之后的点击事件中只分配了内存地址,并没有进行任何的操作。

14.利用闭包根据字段排序商品

利用闭包向上寻找数据的特性,在定义排序函数后,放置到sort方法中,sort方法有两个参数,这两个参数传递到排序方法中,完成排序。

直接调用排序函数和在sort方法中写后面的返回函数是一样的,他们都利用了闭包的特性。

 let lessons = [
   {
     title:'javaScript',
     price:30,
     click:29
   },
   {
     title:'HTML',
     price:15,
     click:24
   },
   {
     title:'CSS',
     price:20,
     click:31
   },
   {
     title:'Node.js',
     price:80,
     click:45
   },
   {
     title:'Vue.js',
     price:50,
     click:22
   },
 ]
 function order(field){
    return function(a,b){
      return a[field] > b[field] ? 1 : -1
    }
 }
 let info = lessons.sort(order("price"))
//  let info = lessons.sort(function(a,b){
//       return a["click"] > b["click"] ? 1 : -1
//     })
 console.log(info)

15.闭包的内存泄露解决方法

当我们想获取div中的“desc”属性时,我们可以遍历按钮列表,并且打印出来这个属性。但是如果我们只是需要这个属性,而不需要按钮的其他内容时,我们可以优化语句,在将“desc”属性绑定到按钮之前,将这个属性复制给一个变量,当绑定完成后将当前标签设置为null,这样就可以避免内存泄漏,优化系统。

// 优化前
<body>
  <div desc="weizhuren">在线学习</div>
  <div desc="erbihouke">开源产品</div>
</body>
<script>
  let divs = document.querySelectorAll("div")
  divs.forEach(function(item){
    item.addEventListener("click",function(){
      console.log(item.getAttribute("desc"))
    })
  })
</script>
// 优化后
<body>
  <div desc="weizhuren">在线学习</div>
  <div desc="erbihouke">开源产品</div>
</body>
<script>
  let divs = document.querySelectorAll("div")
  divs.forEach(function(item){
    let desc = item.getAttribute("desc")
    item.addEventListener("click",function(){
      console.log(desc)
    })
    item = null
  })
</script>

16.this在闭包中的历史遗留问题

在这个“info”对象中,最内层返回了一个“this.user”,按照闭包的特性,应该向上寻找this(NO!NO!NO!),其实在他的这个函数中已经有this了,在我们调用的时候,这个this并不会指向对象,而是在一次返回后指向了window,所以并不能找到user对象。

  let info = {
    user:'weizhuren',
    get:function(){
      return function(){
        return this.user
      }
    }
  }
  console.log(info.get()())// undefined

我们可以利用箭头函数或者在父级作用域获取this的方式解决。

  let info = {
    user:'weizhuren',
    get:function(){
      return ()=>{
        return this.user
      }
    }
  }
  console.log(info.get()())// weizhuren

  let info = {
    user:'weizhuren',
    get:function(){
      let that = this
      return function(){
        return that.user
      }
    }
  }
  console.log(info.get()())// weizhuren

PS:说实话,闭包特性到这里我还是没有完全清楚,大概了解了特性和用法,但是让我说出来闭包定义,我还是不知道如何说起,这部分还是得学啊!学起来!!!

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值