原生js:闭包

354 阅读9分钟

1、前言

闭包是js中最强大的特性,也是js相较于其他语言最令人着迷的地方,如果你对它研究的透彻,你会为它着迷,否则你会被吓住。

2、闭包的前置知识点

  1. 垃圾回收机制:每隔一段时间,垃圾回收期会去内存中找到那些不再使用的值,然后给它释放掉,以此来缓解内存的压力。如果一个函数被全局变量引用(将函数赋值给某个全局变量),那么它将不会被垃圾回收机制回收,这种情况多了就会内存拥堵,严重时产生内存泄漏

  2. 词法环境(词法作用域): 函数的作用域基于函数声明时的位置。也就是说,当一个函数执行时和声明时的词法作用域不是同一个,闭包就产生了

    词法作用域小案例:

      function test(fn) {
        const a = 1
        fn()
      }
      const a = 2
      function fn() {
        console.log(a)
      }
      test(fn) // 2    因为JavaScript采用的是词法作用域

tip:this的值是在函数调用时确定的,而不是函数定义时确定。而函数的作用域在函数定义时确定。

3、闭包解决了什么问题

js有回收机制,如果一个函数没有被引用,该函数执行完后他的作用域就会被销毁;如果该函数被引用了,它执行完后不会被销毁

4、闭包的定义

MDN:

一个函数和对其周围状态(Lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。

image.png

解释: 【在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来】也就是说,所有的函数,都是闭包。整个浏览器都是一个作用域,其中的每个函数(如fn)都是作为window对象的一个方法,window对象作为父函数,fn就是子函数,调用fn时,通过前面的【window.】不写,这就是一个函数嵌套函数的结构。所有的函数都有父级,所以都是子函数,所以函数都是闭包。

JavaScript高级程序设计第三版:

闭包是值有权访问另一个函数作用域中的变量的函数。 大白话:闭包是函数。什么样的函数?可以访问到另一个函数作用域中的变量,那不就是函数嵌套函数中的子函数

JavaScript高级程序设计第四版:

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。 大白话:闭包是函数。注意看,第四版相比于第三版多了一个条件——引用了,不止是可以访问到父函数中的变量,并且引用了该变量,那么才认为这个子函数是一个闭包。MDN和第三版说的是子函数就是闭包,第四版说的是引用了父函数的变量的子函数才是闭包

5、形成闭包的条件

  1. 函数嵌套函数
  2. 子函数引用了父函数的局部变量/局部函数(也就是说,如果b函数中没有使用a函数的变量,那么b执行的过程中不会产生闭包)

tip:旧版谷歌浏览器中即使不调用子函数也会产生闭包,新版谷歌不调用子函数时不会产生闭包

image.png image.png 解释:

  • 函数b在执行时产生了闭包,函数b就是闭包,但是这个闭包没有被保持下来,在b执行完后闭包就已经没了。
  • 如果说闭包没有被保持下来,那么闭包的作用就得不到体现,同样的闭包的缺点就无从谈起了。其实我们在写代码的过程中,不知不觉就产生了闭包,那这个闭包会对内存产生压力吗?并不会,因为它压根就没有被保持下来

思考:

判断b函数在执行的过程中有没有形成闭包?

      function a() {
        var num = 100
        var str = 'xxx'
        function b() {
          console.log(b) // console.log使用了a函数中的局部函数,此时会形成闭包 { b: fun }
        }
        b()
      }
      a()
      /*
        预编译过程:
          当a调用时,产生a的AO对象
            aAO: {
              num: 100
              str: 'xxx',
              b: fun
            }
          当b调用时,产生b的AO对象
            bAO: {}   bAO是一个空对象,因为b函数中没有任何可预编译的东西
            代码执行到b函数中,此时的[[Scopes]]为:
              0: bAO
              1: aAO
              2: GO
            打印的b在b函数中没有找到,向上查找,在a函数中找到局部函数b,即打印 f b() {}
      */

6、闭包的保持

  • 闭包的保持可以理解为闭包状态的保持
  • 如果希望在函数调用完后,闭包可以被保持下来,就需要将子函数返回到父函数的外部被全局变量接收
      /*
        这就是一个很经典的闭包,并且闭包的状态也会被保持:
          1、函数嵌套
          2、子函数使用了父函数的局部变量
          3、子函数返回到父函数的外面被全局变量接收,那么此时被子函数使用的局部变量num就会被长期存储在内存中
      */
      function a() {
        var num = 100
        return function () {
          console.log(num++) // 这里形成闭包
        }
      }
      var b = a()
      console.dir(b)
      b() // 100  函数b执行时产生bAO,执行完将bAO销毁,但是num的状态被保留下来了
      b() // 101  再次执行b,产生新的bAO,执行完将bAO销毁,num的状态被保留
      b() // 102  再次执行b,产生新的bAO,执行完将bAO销毁,num的状态被保留

7、什么时候需要使用闭包(闭包的作用)

  • 一般来说,函数外是不能访问到函数内的变量的,但是通过闭包就可以访问到
  • 想让一个变量长期存储在内存中,以便将来使用,但是不想定义全局变量,以免该全局变量受到污染,就要想到使用闭包

8、闭包的应用

  1. 在函数外访问函数的私有变量,替代使用全局变量,防止污染全局变量(闭包的保持那个例子)
  2. 封装私有属性和私有方法
  3. 回调函数的本质是利用了闭包的特性,将局部变量缓存起来了

9、内存泄漏的解决办法

    <div desc="aaa">aaa</div>
    <div desc="bbb">bbb</div>
    <script>
      // 要求点击div打印它的自定义属性desc
      const divs = document.querySelectorAll('div')

      // item被保存到内存中,但是并不需要它。内存中这样无用的数据多了到一定量会造成内存泄漏
      // for (const item of divs) {
      //   item.addEventListener('click', () => {
      //     console.log(item.getAttribute('desc'))
      //     console.log(item) // <div desc="aaa">aaa</div>
      //   })
      // }

      // 获取到item的desc后将item设置为null,释放内存
      for (let item of divs) {
        const desc = item.getAttribute('desc')
        item.addEventListener('click', () => {
          console.log(desc)
          console.log(item) // null
        })
        item = null // 将item设置成null,就会被垃圾回收机制回收
      }

image.png

也可以使用bind

      const fn = item => {
        console.log(item.getAttribute('desc'))
      }

      for (let item of divs) {
        item.addEventListener('click', fn.bind(this, item))
        item = null
      }

10、this在闭包中的历史遗留问题

      const person = {
        username: '小明',
        getName: function () {
          console.log(this.username) // 小明
          return function () {
            console.log(this) // window
            return this.username // 闭包按理来说可以访问到上级函数中的变量,但是this比较特殊。this的指向在于被谁调用,a函数是被window调用的,所以这里的this是window,所以会打印undefined
          }
        }
      }
      const a = person.getName()
      console.log(a()) // undefined

解决方法一: 通过_this保存当前对象

      const person = {
        username: '小明',
        getName: function () {
          const _this = this
          return function () {
            console.log(_this) // {username: '小明', getName: ƒ}
            return _this.username // 小明
          }
        }
      }
      const a = person.getName()
      console.log(a()) // 小明

解决方法二: 通过箭头函数,改变this指向,箭头函数的this指向它的上级this

      const person = {
        username: '小明',
        getName: function () {
          return () => {
            console.log(this) // {username: '小明', getName: ƒ}
            return this.username // 小明
          }
        }
      }
      const a = person.getName()
      console.log(a()) // 小明

11、闭包的实际使用

1、求数组的一段区间

      const arr = [1, 23, 5, 6, 34, 26, 78, 9]
      const a1 = arr.filter(function (item) {
        return item >= 2 && item <= 9
      })
      const a2 = arr.filter(function (item) {
        return item >= 3 && item <= 6
      })
      console.log(a1)
      console.log(a2)
每次获取结果都调用filter方法,filter方法中的代码重复,可以使用闭包优化:
      function between(a, b) {
        return function (item) {
          return item >= a && item <= b
        }
      }
      // const between = (a, b) => (item) => item >= a && item <= b
      console.log(arr.filter(between(2, 9)))
      console.log(arr.filter(between(3, 6)))

2、数组对象根据某个属性排序

      const goods = [
        { name: '苹果', price: 10, num: 52 },
        { name: '梨子', price: 4, num: 200 },
        { name: '芒果', price: 12, num: 150 },
        { name: '香蕉', price: 8, num: 32 },
        { name: '火龙果', price: 11, num: 22 },
        { name: '橙子', price: 15, num: 88 }
      ]
      const priceOrder = goods.sort((a, b) => a.price - b.price)
      console.table(priceOrder)

      const numOrder = goods.sort((a, b) => a.num - b.num)
      console.table(numOrder)
sort函数中那段代码可以利用闭包复用:
      const order = propertyName => (a, b) => a[propertyName] - b[propertyName]

      console.table(goods.sort(order('price')))
      console.table(goods.sort(order('num')))

3、每个1秒在页面打印一次当前时间

      let second = 0
      const counter = () => ++second
      const recordSecond = setInterval(function () {
        if (second === 5) {
          clearInterval(recordSecond)
          console.log('计时结束')
          return
        }
        const str = counter() + '秒'
        console.log(str, new Date())
      }, 1000)
改成闭包,闭包的优势——不需要定义全局变量,减少了全局变量的污染:
      const counter = () => {
        let second = 0
        return function () {
          if (second === 5) {
            clearInterval(recordSecond)
            doCounter = null // 清除闭包:将引用内层函数的变量赋值为null
            console.log('计时结束')
            return
          }
          second++
          console.log(second + '秒', new Date())
        }
      }
      let doCounter = counter()
      const recordSecond = setInterval(function () {
        doCounter()
      }, 1000)

4、使用闭包实现累加函数

牢记这3点:
    1. 利用了闭包的作用:将局部变量长期保存在内存中
    2. 如何形成闭包:①函数嵌套函数②子函数使用了父函数的局部变量
    3. 闭包如何被保持:将子函数返回到父函数外使用全局变量接收
      function addFn() {
        let num = 0
        return function () {
          console.log(num++)
        }
      }
      const add = addFn()
      add() // 0
      add() // 1
      add() // 2

      function addFn() {
        let num = 0
        return function () {
          return num++
        }
      }
      const add = addFn()
      console.log(add()) // 0
      console.log(add()) // 1
      console.log(add()) // 2

5、使用闭包实现私有变量

(1)通过set/get设置和读取变量

      function fn() {
        const obj = {}
        return {
          set: function (key, val) {
            obj[key] = val
          },
          get: function (key) {
            return obj[key]
          }
        }
      }
      const f = fn()
      console.log(f.get('name')) // undefined
      f.set('name', '小明')
      console.log(f.get('name')) // 小明

构造函数的写法:

      function Fn() {
        const obj = {}
        this.set = function (key, val) {
          obj[key] = val
        }
        this.get = function (key) {
          return obj[key]
        }
      }
      const f = new Fn()
      f.set('name', '小明')
      console.log(f.get('name'))

(2)通过add/minus实现加减

      function counter() {
        let count = 0
        return {
          add: function () {
            count++
          },
          minus: function () {
            count--
          },
          value: function () {
            return count
          }
        }
      }
      const myCounter = counter()
      console.log(myCounter.value()) // 0
      myCounter.add()
      myCounter.add()
      console.log(myCounter.value()) // 2
      myCounter.minus()
      console.log(myCounter.value()) // 1

第二种写法(自执行函数):

      const counter = (() => {
        let count = 0
        return {
          add: function () {
            count++
          },
          minus: function () {
            count--
          },
          value: function () {
            return count
          }
        }
      })()
      console.log(counter.value()) // 0
      counter.add()
      counter.add()
      console.log(counter.value()) // 2
      counter.minus()
      console.log(counter.value()) // 1

(3)保持用户点击链接的次数和地址---设计思路:模块化

tracker.js
    let accessCounter = 0
    const adAccessRec = []
    // CommonJS语法导出
    module.exports = {
      storeAccessPage: function (page) {
        adAccessRec.push(page)
      },
      checkAdAccessRec: function () {
        return adAccessRec
      },
      increaseCounter: function () {
        accessCounter++
      },
      getAccessCounter: function () {
        return accessCounter
      }
    }
test.js
    // var tracker = (function() {
    //   var accessCounter = 0
    //   var adAccessRec = []
    //   return {
    //     storeAccessPage: function(page) {
    //       adAccessRec.push(page)
    //     },
    //     checkAdAccessRec: function() {
    //       return adAccessRec
    //     },
    //     increaseCounter: function() {
    //       accessCounter++
    //     },
    //     getAccessCounter: function() {
    //       return accessCounter
    //     }
    //   }
    // })()
    // 将tracker的内容封装成模块
    var tracker = require('./tracker') // CommonJS语法导入

    console.log(tracker.getAccessCounter()) // 0
    tracker.increaseCounter()
    tracker.increaseCounter()
    console.log(tracker.getAccessCounter()) // 2

    var page1 = 'xxx'
    var page2 = 'yyy'
    tracker.storeAccessPage(page1)
    tracker.storeAccessPage(page2)
    console.log(tracker.checkAdAccessRec()) // [ 'xxx', 'yyy' ]

6、回调函数的本质是闭包

    <style>
      #box {
        position: absolute;
      }
    </style>
    <div id="box">hello world</div>
    <script>
      function animate(elementId) {
        const elem = document.getElementById(elementId)
        let count = 0
        let timer = setInterval(function a() {
          if (count < 10) {
            elem.style.top = elem.style.left = count + 'px'
            count++
          } else {
            clearInterval(timer)
            timer = null
          }
        }, 300)
      }
      animate('box')
    </script>
函数a就是一个闭包,它可以访问到animate函数中的elem和count

7、值缓存

      const cacheBox = (function () {
        const cache = {}
        return {
          search: function (id) {
            if (id in cache) {
              return '1结果' + cache[id]
            }
            const result = dealFn(id)
            cache[id] = result
            return '2结果' + result
          }
        }
      })()
      function dealFn(id) {
        console.log('这是一段比较耗时的操作')
        return id
      }
      // 第一次执行cacheBox.search(100),cache中没有100,走dealFn函数,比较耗时;第二次执行cacheBox.search(100),cache中已经存在100,直接输出
      let res = cacheBox.search(100)
      console.log(res) // 2结果100
      let res1 = cacheBox.search(100)
      console.log(res1) // 1结果100

8、for循环中的定时器

      for (var i = 0; i < 3; i++) {
        setTimeout(() => {
          console.log(i)
        }, 100)
      }
以上这段代码将打印33,并非想象中的 0 1 2
原因:定时器中的函数使用了本函数外的变量i,导致变量i被缓存到内存中不被销毁,这里不是因为异步导致的

如果需要打印对应的下标i:

  1. 定时器中可以放函数的调用,此时函数不受定时器的控制,直接执行打印 image.png
  2. 使用闭包 image.png
  3. 使用ES6的let,具有块级作用域 image.png

9、打印li对应的下标

DOM:

    <ul id="ul">
      <li>第一个</li>
      <li>第二个</li>
      <li>第三个</li>
    </ul>

JS:

      const ul = document.getElementById('ul')
      const lis = ul.getElementsByTagName('li')
      for (var i = 0; i < lis.length; i++) {
        lis[i].onclick = function () {
          console.log(i)
        }
      }
此时,每次点击li打印的都是3。循环的执行是一瞬间的,而事件的回调是点击后才触发,此时i已经是3

如果需要打印对应的下标i:

  1. 使用闭包

image.png

image.png

  1. 使用ES6的let

image.png

  1. 循环时保存i

image.png

  1. 使用bind

image.png

  1. 使用forEach代替for

image.png

10、定时器函数传参

      // 定时器中function无法直接实现传参
      setTimeout(params => {
        console.log(params) // undefined
      }, 100)

      // 通过闭包实现定时器传参
      function fn(params) {
        return function () {
          console.log(params) // 100
        }
      }
      var one = fn(100)
      setTimeout(one, 100)
扩展:利用定时器第三个参数传参
      // 定时器的第三个参数可以传参
      function one(params) {
        console.log(params) // 200
      }
      setTimeout(one, 1000, 200)

11、vue中computed传参

需求:页面中展示本周的日期,并且让今天的日期变为红色

image.png

通过闭包的方式传值:

  • computed中的方法需要return一个函数,该函数可以接收到参数
  • 使用computed中的方法,一般是将函数放这(无括号),这里要写函数的调用(有括号)
<template>
  <div>
    <ul>
      <li v-for="(date,index) of list" :key="index" :style="{color:today(date)?'red':''}">{{date}}</li>
    </ul>
  </div>
</template>
<script>
export default {
  methods: {
    // 获取本周日期
    getWeekDataList() {
      const weekList = []
      const date = new Date()
      const currentDate = date.getDate() // 今天是多少号
      const weekDate = date.getDay() // 今天是周几
      date.setDate(
        weekDate == '0' ? currentDate - 6 : currentDate - weekDate + 1
      )
      let myDate = date.getDate() // 本周周一是多少号
      let myMonth = date.getMonth() + 1
      if (myDate < 10) myDate = '0' + myDate
      if (myMonth < 10) myMonth = '0' + myMonth
      weekList.push(date.getFullYear() + '-' + myMonth + '-' + myDate)
      // 获取周二以后日期
      for (var i = 0; i < 6; i++) {
        date.setDate(myDate + 1)
        myDate = date.getDate()
        myMonth = date.getMonth() + 1
        if (myDate < 10) myDate = '0' + myDate
        if (myMonth < 10) myMonth = '0' + myMonth
        weekList.push(date.getFullYear() + '-' + myMonth + '-' + myDate)
      }
      return weekList
    }
  },
  data() {
    return { list: this.getWeekDataList() }
  },
  computed: {
    today() {
      return function (date) {
        return date.slice(-2) == new Date().toString().slice(8, 10)
      }
    },
    // 这种方式接收不到参数
    today1(date) {
      return date.slice(-2) == new Date().toString().slice(8, 10)
    }
  }
}
</script>

12、闭包的总结

  1. 什么是闭包:函数嵌套函数,并且子函数引用了父函数的局部变量,其中子函数就是闭包(JavaScript高级程序设计第4版)。要注意,在新版谷歌浏览器中,子函数必须要调用才能产生闭包。

  2. 闭包的特点:

    ①闭包可以访问到父函数的局部变量,这其实是作用域链的特性

    ②闭包很常见,通常一个函数被创建出来,闭包就会同时被创建出来(MDN这么说的)。回调函数都是闭包,一般的闭包没有实现长期保存局部变量的特性,所以也不会对内存产生大的压力

  3. 闭包的作用:让局部变量长期存储在内存中,以便将来使用,避免了过多的使用全局变量

  4. 如何才能让一个局部变量长期保存在内存中:将子函数返回到父函数的外面用一个全局变量接收

  5. 如何缓解内存泄漏的风险:当获取到需要的数据后将变量设置为null

  6. this在闭包中的指向:记住this永远都是谁调用该方法,方法内的this就指向谁。闭包中的this可能与你想象中的不同,为了尽可能避免这种问题,推荐使用箭头函数

  7. 闭包的应用:实现私有变量全局化,避免全局变量的污染;回调函数的本质是闭包;computed传参