js中的闭包以及闭包的实际运用

1.闭包的形成条件:

1.函数嵌套
2.内部函数引用了外部函数的局部变量
PS:正常情况下,当一个函数调用完之后,内存中关于函数的东西会全部释放掉,局部变量也会消失,而闭包是一种特殊的存在。由于外部函数返回的是内部函数的引用,相当于你返回了一个函数,这个函数还未被真正调用。但是内函数又使用了外函数的变量,导致内存不能释放它们,需要等到内函数使用完成之后才能释放它们----由此形成了闭包
3.闭包 是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量

2.闭包的用途

2.1
简单例子1

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

怎么来理解这句话呢?请看下面的代码。

function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999   
  nAdd();
  result(); // 1000

在这里插入图片描述

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量。

闭包简单例子2
概念: 闭包只能取得外部函数中任何变量的最后一个值.


  function create() {
    let res = []
    for (var i = 0; i < 5; i++) {
      res[i] = function() {
        return i
      }
    }
    return res
  }
  console.log(create()) // 结果: [4,4,4,4,4]

分析:
由于每个函数的作用域中都保存着同一个 函数的活动对象(create 函数), 所以他们引用的都是同一个变量 i.
如何解决这个问题?
我们可以使用匿名函数来创建一个闭包, 让闭包的行为来符合预期

function create() {
    let res = []
    for (var i = 0; i < 5; i++) {
      res[i] = (function(num) {
        return function() { return num }
      })(i)
    }
    return res
  }
  console.log(create()) // 结果: [0,1,2,3,4]

上面将 create 函数进行了一个重写.
1.现在没有直接把闭包赋值给数组,而是再定义一个匿名函数, 将变量 i 当参数传进去由num来接收并立即执行.
2.在每次执行这个匿名函数时都会创建一个属于自己的 活动对象/作用域/变量对象.
3.然后再里面返回一个 只访问 num 的闭包函数, 这时取到的 num 都是独立的值
2.2 闭包的概念和特性
首先看个闭包的例子:


function makeFab () {
  let last = 1, current = 1
  return function inner() {
    [current, last] = [current + last, current]
    return last
  }
}

let fab = makeFab()
console.log(fab()) // 1
console.log(fab()) // 2
console.log(fab()) // 3
console.log(fab()) // 5

分析:
makeFab的返回值就是一个闭包,makeFab像一个工厂函数,每次调用都会创建一个闭包函数,如例子中的fab。
注意:fab每次调用都不需要传参数,都会返回不同的值,因为在闭包生成的时候,它记住了变量last和current,以至于在后续的调用中能够返回不同的值。
PS:能够记住函数本身所在作用域的变量,这就是闭包和普通函数的区别所在

MDN中给出闭包的定义:函数与其状态即词法环境的引用共同构成闭包
这里的“词法环境的引用”,可以简单理解为“引用了函数外部的一些变量”,例如上述例子中每次调用makeFab都会创建并返回inner函数,引用了last和current两个变量。

2.2 闭包的用途

例子1
以实现一个可复用的确认框为例,比如在用户进行一些删除或者重要操作时,为了防止失误操作,我们可能会通过弹窗让用户再次确认操作。
比如在触发弹窗中的确认/取消事件时异步操作,这时候我们就需要使用两个回调函数完成操作,弹窗函数confirm接受三个参数,一个时提示语,一个是确认回调函数,一个是取消回调函数:


function confirm (confirmText, confirmCallback, cancelCallback) {
  // 插入提示框DOM,包含提示语句、确认按钮、取消按钮
  // 添加确认按钮点击事件,事件函数中做dom清理工作并调用confirmCallback
  // 添加取消按钮点击事件,事件函数中做dom清理工作并调用cancelCallback
}

之前我还一直想不通,原来是闭包在作祟!!!我服了!!!
因为使用闭包,confirm中则包含两个回调函数,那么我们就可以通过confirm传递回调函数,并且根据不同的结果完成不同的动作,比如我们根据id删除一条数据可以这样这:

function removeItem(id){
	confirm('确认删除吗?',()=>{
	//用户点击确认,发送远程ajax请求
	  api.removeItem(id).then(xxx)
	},
	()=>{
	//用户点击取消
	  console.log('取消删除‘)
	}
}

在这里插入图片描述

解析:在这个例子中,confirm 的回调函数正式利用了闭包,创建了一个引用上下文id变量的函数!

例子2使用闭包实现防抖、节流函数
前端很常见的一个需求就是远程搜索,根据用户输入框的内容自动发送ajax请求,然后从后端把搜索结果请求回来。
为了简化用户的操作,有时候我们不会专门放置一个按钮来点击触发搜索事件,而是直接监听内容的变化来搜索(比如微信小程序中根据关键字搜索商品)
这时候为了避免请求过于频繁,我们可以利用”防抖“技术,即当用户停止输入一段事件时(比如500ms)后才执行

  • 可以使用如下代码实现
<body>
  <input type="text">
  <script>
    function debounce(func, time) {
      let timer = 0;
      return function () {
        //若使用timer && clearTimeout(timer)会更加确保timer被清除记录,或者timer=0
        //注释这一行和注释下面的timer=0也可以(只是相对没那么严谨)
        // timer && clearTimeout(timer)
        clearTimeout(timer)
        timer = setTimeout(() => {
          // timer = 0
          func.apply(this)//反之this指向错乱
        }, time)
      }
    }

    const input = document.querySelector('input')
    input.onkeypress = debounce(function () {
      console.log(input.value)//事件处理逻辑
    }, 500)
  </script>
</body>

分析:dobounce函数每次调用时,都会创建一个新的闭包函数,该函数保留了对事件逻辑处理函数func以及防抖时间间隔time以及定时器标志timer的引用

例子3—使用闭包实现节流函数

function throttle(func,time){
	var lastTime=0;
	return function(){
	    var nowTime=new Date();
		if(nowTime-lastTime>time){
			func.call(this);
			lastTime=nowTime;//由于闭包的作用原理,这里调用外层函数的局部变量,每次调用这个lastTime变量都会发生改变
		}
	}
}

例子4–使用闭包解决按钮多次连续点击问题

问题:
用户点击一个表单提交按钮时,前端会向后端发送异步请求,请求还没有返回,用户焦急又多点了即此按钮,这样带来的后果就是:①消耗服务器资源②修改了后台的数据
解决:通常办法是定义一个标记变量,即在响应函数所在的作用域声明一个布尔变量lock,响应函数被调用时,先判断lock的值,为true则表示上一次请求还没有返回来,此时点击无效;为false则将lock设置为true,然后发送请求,请求结束后将lock改为fasle
很显然,lock变量会污染函数所在的作用域,而生成闭包伴随着新的函数作用域的创建,利用这一点,刚好可以解决这个问题,下面是简单的例子:

  let clickButton = (function () {
      let lock = false;
      //postParams为发送请求时需要的参数
      return function (postParams) {
        if (lock) return
        lock = true
        //使用axios发送请求
        axios.post('urlxxxxx', postParams).then(
        ).catch(error => {
          //请求失败
          console.log(err)
        }).finnally(() => {
          //不管失败还是成功,都解锁
          lock = false
        })
      }
    })()	

说明:这样lock变量就会在一个单独的作用域里,一次点击请求发出以后,必须等请求回来,才会开始下一次请求。
当然,为了避免各个地方都声明lock,修改lock,我们可以把上述逻辑抽象以下,实现一个装饰器,就像节流/防抖函数一样。

function singleClick(func, manuDone = false) {
  let lock = false
  return function (...args) {
    if (lock) return
    lock = true
    let done = () => lock = false
    if (manuDone) return func.call(this, ...args, done)
    let promise = func.call(this, ...args)
    promise ? promise.finally(done) : done()
    return promise
  }
}

默认情况下,需要原函数返回一个promise以达到promise决议后将lock重置为false,而如果没有返回值,lock将会被立即重置(比如表单验证不通过,响应函数直接返回),调用示例:


let clickButton = singleClick(function (postParams) {
  if (!checkForm()) return
  return axios.post('urlxxx', postParams).then(
    // 表单提交成功
  ).catch(error => {
    // 表单提交出错
    console.log(error)
  })
})
button.addEventListener('click', clickButton)

3、使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

参考文章:https://mp.weixin.qq.com/s?src=11&timestamp=1606640830&ver=2736&signature=QRHsoFmN15Z-3hfSkJMVD4GsWo8YCxJGptijYHhezENErdK47vK1KL0wnNgnpECWrB5R5iIWYinNV6wJDuOk3WpArSNjhMCTbd3BPfKzqahB97V20NeW2Q1*WP04iKgN&new=1

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值