一文带你了解闭包

259 阅读3分钟

什么是闭包?

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

作用

为什么闭包会产生呢?或者说闭包的作用是什么?

让我们先来看看下面这个例子:

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

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

function person() {
  var age = 18;
  function student() {
    age++;
    console.log(age);
  }
  return student()
}
person(); // 19
person(); // 19

可以看出,此时每次调用函数person,进入该作用域时,变量age都会重新赋值18。同时,变量age已在函数内部,不易修改和外泄,相对比较安全。

所以说,闭包有以下几个作用:1、隐藏变量,避免全局污染; 2、可以读取函数内部的变量。

缺点

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

    1. 缺点1:变量难以被垃圾回收机制回收,造成内存过度占用
function person(propertyName) {
  return function(object) {
    let student = object[propertyName]
    console.log(student)
  }
}
let personName = person('name')
// 调用函数
let studentName = personName({name: 'btqf'})
// 解除函数的引用,释放内存
personName = null

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

因为闭包会保留它们包含函数的作用域,所以比其他函数更占用内存,过度使用闭包可能导致内存过度占用

    1. 缺点2:不恰当的使用闭包可能会造成内存泄漏
function assignHandler() {
  let element = document.getElementById('someElement')
  element.onclick = () => console.log(element.id)
}

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

应用场景

说了这么多,它主要能用来干什么?

通常在定时器、事件监听器、ajax请求、跨窗口通信、webworkers或者其他的同步/异步任务中,只要使用了回调函数,实际上就是使用了闭包。比较经典的题目大概有防抖节流函数.

以下为防抖函数实现:

function debounce(func, wait) {
  let timer = null;

  return function () {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    let self = this;
    let args = arguments;

    timer = setTimeout(function () {
      func.apply(self, args);
      timer = null;
    }, wait);
  };
}