JavaScript闭包的应用

本文最早发布于个人我的网站: https://wintc.top/article/33。转载请注明出处。

  本文介绍一下JS中的一个重要概念——闭包。其实即使是最初级的前端开发人员,应该都已经接触过它。javascript

1、闭包的概念和特性

  首先看个闭包的例子:前端

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,以致于在后续的调用中可以返回不一样的值。能记住函数自己所在做用域的变量,这就是闭包和普通函数的区别所在。vue

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

2、闭包——函数式编程之魂

Javascript和python这两门动态语言都强调一个概念:万物皆对象。天然,函数也是对象。
在Javascript里,咱们能够像操做普通变量同样,把函数在咱们的代码里抛来抛去,而后在某个时刻调用一下,这就是所谓的函数式编程。函数式编程灵活简洁,而语言对闭包的支持,让函数式编程拥有了灵魂。python

以实现一个可复用的确认框为例,好比在用户进行一些删除或者重要操做的时候,为了防止误操做,咱们可能会经过弹窗让用户再次确认操做。由于确认框是通用的,因此确认框组件的逻辑应该足够抽象,仅仅是负责弹窗、触发确认、触发取消事件,而触发确认/取消事件是异步操做,这时候咱们就须要使用两个回调函数完成操做,弹窗函数confirm接收三个参数:一个提示语句,一个确认回调函数,一个取消回调函数:ios

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

这样咱们能够经过向confirm传递回调函数,而且根据不一样结果完成不一样的动做,好比咱们根据id删除一条数据能够这样写:ajax

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

这个例子中,confirmCallback正是利用了闭包,建立了一个引用了上下文中id变量的函数,这样的例子在回调函数中比比皆是,而且大多数时候引用的变量是不少个。 试想,若是语言不支持闭包,那这些变量要怎么办?做为参数所有传递给confirm函数,而后在调用confirmCallback/cancelCallback时再做为参数传递给它们?显然,这里闭包提供了极大便利。编程

3、闭包的一些例子

1. 防抖、节流函数

  前端很常见的一个需求是远程搜索,根据用户输入框的内容自动发送ajax请求,而后从后端把搜索结果请求回来。为了简化用户的操做,有时候咱们并不会专门放置一个按钮来点击触发搜索事件,而是直接监听内容的变化来搜索(好比像vue的官网搜索栏)。这时候为了不请求过于频繁,咱们可能就会用到“防抖”的技巧,即当用户中止输入一段时间(好比500ms)后才执行发送请求。能够写一个简单的防抖函数实现这个功能:axios

function debounce (func, time) {
  let timer = 0
  return function (...args) {
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      timer = 0
      func.apply(this, args)
    }, time)
  }
}

input.onkeypress = debounce(function () {
  console.log(input.value) // 事件处理逻辑
}, 500)

  debounce函数每次调用时,都会建立一个新的闭包函数,该函数保留了对事件逻辑处理函数func以及防抖时间间隔time以及定时器标志timer的引用。相似的还有节流函数:后端

function throttle(func, time) {
  let timer = 0 // 定时器标记至关于一个锁标志
  return function (...args) {
    if (timer) return
    func.apply(this, args)
    timer = setTimeout(() => timer = 0, time)
  }
}

2. 优雅解决按钮屡次连续点击问题

        用户点击一个表单提交按钮,前端会向后台发送一个异步请求,请求还没返回,焦急的用户又多点了几下按钮,形成了额外的请求。有时候多发几回请求最多只是多消耗了一些服务器资源,而另一些状况是,表单提交自己会修改后台的数据,那屡次提交就会致使意料以外的后果了。不管是为了减小服务器资源消耗仍是避免屡次修改后台数据,给表单提交按钮添加点击限制是颇有必要的。

        怎么解决呢?一个经常使用的办法是打个标记,即在响应函数所在做用域声明一个布尔变量lock,响应函数被调用时,先判断lock的值,为true则表示上一次请求还未返回,这次点击无效;为false则将lock设置为true,而后发送请求,请求结束再将lock改成false。

        很显然,这个lock会污染函数所在的做用域,好比在Vue组件中,咱们可能就要将这个标记记录在组件属性上;而当有多个这样的按钮,则还须要不一样的属性来标记(想一想给这些属性取名都是一件头疼的事情吧!)。而生成闭包伴随着新的函数做用域的建立,利用这一点,恰好能够解决这个问题。下面是一个简单的例子:       

let clickButton = (function () {
  let lock = false
  return function (postParams) {
    if (lock) return
    lock = true
    // 使用axios发送请求
    axios.post('urlxxx', postParams).then(
      // 表单提交成功
    ).catch(error => {
      // 表单提交出错
      console.log(error)
    }).finally(() => {
      // 无论成功失败 都解锁
      lock = false
    })
  }
})()

button.addEventListener('click', clickButton)

        这样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)

        在一些不方便返回promise或者请求结束还要进行其它动做以后才能重置lock的地方,singleClick提供了第二个参数manuDone,容许你能够手动调用一个done函数来重置lock,这个done函数会放在原函数参数列表的末尾。使用例子:

let print = singleClick(function (i, done) {
  console.log('print is called', i)
  setTimeout(done, 2000)
}, true)

function test () {
  for (let i = 0; i < 10; i++) {
    setTimeout(() => {
      print(i)
    }, i * 1000)
  }
}

        print函数使用singleClick装饰,每次调用2秒后重置lock变量,测试每秒调用一次print函数,执行代码输出以下图:

       能够看到,其中一些调用没有打印结果,这正是咱们想要的结果!singleClick装饰器比每次设置lock变量要方便许多,这里singleClick函数的返回值,以及其中的done函数,都是一个闭包。

3. 闭包模拟私有方法或者变量

  “封装”是面向对象的特性之一,所谓“封装”,即一个对象对外隐藏了其内部的一些属性或者方法的实现细节,外界仅能经过暴露的接口操做该对象。JS是比较“自由”的语言,因此并无相似C++语言那样提供私有变量或成员函数的定义方式,不过利用闭包,却能够很好地模拟这个特性。

  好比游戏开发中,玩家对象身上一般会有一个经验属性,假设为exp,"打怪"、“作任务”、“使用经验书”等都会增长exp这个值,而在升级的时候又会减掉exp的值,把exp直接暴露给各处业务来操做显然是很糟糕的。在JS里面咱们能够用闭包把它隐藏起来,简单模拟以下:

function makePlayer () {
  let exp = 0 // 经验值
  return {
    getExp () {
      return exp
    },
    changeExp (delta, sReason = '') {
      // log(xxx),记录变更日志
      exp += delta
    }
  }
}

let p = makePlayer()
console.log(p.getExp()) // 0
p.changeExp(2000)
console.log(p.getExp()) // 2000

  这样咱们调用makePlayer()就会生成一个玩家对象p,p内经过方法操做exp这个变量,可是却不能够经过p.exp访问,显然更符合“封装”的特性。

4、总结

        闭包是JS中的强大特性之一,然而至于闭包怎么使用,我以为不算是一个问题,甚至咱们彻底不必研究闭包怎么使用。个人观点是,闭包应该是天然而言地出如今你的代码里,由于它是解决当前问题最直截了当的办法;而当你刻意想去使用它的时候,每每可能已经走了弯路。

相关文章
相关标签/搜索