定义

什么是闭包?
闭包就是在一个函数定义的作用域外,使用该函数作用域内的局部变量,它通常是在嵌套函数中发生的。它的原理是基于词法作用域链和垃圾回收机制,通过维持函数作用域的引用,让函数作用域可以在当前作用域外被访问到

作用

为什么闭包会产生呢?或者说闭包的作用是什么?
让我们先来看看下面这个例子:

  1. var age = 18;
  2. function person() {
  3. age++;
  4. console.log(age);// cat函数内输出age,该作用域没有,则向外层寻找,结果找到了,输出[19];
  5. }
  6. person(); // 19
  7. person(); // 20

由上述例子可知,如果我们再次调用时,结果会一直增加,也就是变量age的值一直递增;并且当其他函数调用该变量age时,将会很容易修改,这也就是全局变量容易污染的原因。为了解决变量污染问题,我们可以把变量修改到函数内,让它成为局部变量。

  1. function person() {
  2. var age = 18;
  3. function student() {
  4. age++;
  5. console.log(age);
  6. }
  7. return student()
  8. }
  9. person(); // 19
  10. person(); // 19

可以看出,此时每次调用函数person,进入该作用域时,变量age都会重新赋值18。同时,变量age已在函数内部,不易修改和外泄,相对比较安全。
所以说,闭包有以下几个作用:1、隐藏变量,避免全局污染; 2、可以读取函数内部的变量。

缺点

既然闭包能够做到这些,那它会不会有什么缺点?

  1. 缺点1:变量难以被垃圾回收机制回收,造成内存过度占用

    1. function person(propertyName) {
    2. return function(object) {
    3. let student = object[propertyName]
    4. console.log(student)
    5. }
    6. }
    7. let personName = person('name')
    8. // 调用函数
    9. let studentName = personName({name: 'btqf'})
    10. // 解除函数的引用,释放内存
    11. personName = null

    我们现在可以知道,闭包是基于作用域链和垃圾回收机制去读取某函数内部的变量。就拿上述的person()来说,person()的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中依然有对它的引用。当person()执行完毕后,其执行上下文的作用域链会被销毁,但它的活动对象仍会保留在内存中,直到匿名函数被销毁后才会被销毁。为了释放内存,可以把personName设置为null从而解除对函数的引用。
    因为闭包会保留它们包含函数的作用域,所以比其他函数更占用内存,过度使用闭包可能导致内存过度占用

  2. 缺点2:不恰当的使用闭包可能会造成内存泄漏

    1. function assignHandler() {
    2. let element = document.getElementById('someElement')
    3. element.onclick = () => console.log(element.id)
    4. }

    以上代码中,匿名函数引用着assignHandler()的活动对象,阻止了element的引用计数(详情可学习垃圾回收机制) 归零。只要该匿名函数在,element的引用计数就至少等于1,即内存不会回收。

    应用场景

    说了这么多,它主要能用来干什么?
    通常在定时器、事件监听器、ajax请求、跨窗口通信、webworkers或者其他的同步/异步任务中,只要使用了回调函数,实际上就是使用了闭包。比较经典的题目大概有防抖节流函数.
    以下为防抖函数实现:

    1. function debounce(func, wait) {
    2. let timer = null;
    3. return function () {
    4. if (timer) {
    5. clearTimeout(timer);
    6. timer = null;
    7. }
    8. let self = this;
    9. let args = arguments;
    10. timer = setTimeout(function () {
    11. func.apply(self, args);
    12. timer = null;
    13. }, wait);
    14. };
    15. }

    为什么要使用闭包呢?我们可以看看不使用闭包去实现防抖函数:

    1. let timer = null;
    2. function dalay() {
    3. if (timer) {
    4. clearTimeout(timer);
    5. timer = null;
    6. }
    7. timer = setTimeout(() => {
    8. // do sth...
    9. }, wait)
    10. }

    在上述的实现中,由于需要声明全局代码timer,这会污染作用域;同时,当页面需要实现多个防抖函数时,我们将需要写很多重复代码。
    为了解决这两个问题,我们可以采用闭包,既能保护全局作用域不受污染,又能做到函数重用。