JS 闭包

300 阅读5分钟

本文大部分内容来自《你不知道的JavaScript》,其中也有一些我自己的总结和感悟,若有错误,欢迎评论区指正

闭包的产生

当一个函数可以记住并访问所在词法作用域时便产生了闭包,即使该函数是在当前词法作用域之外执行。

function foo() {
  var a = 2;
  function bar() {
    console.log(a); // 2
  }
  bar();
}
foo();

在这段代码中,函数bar()可以访问外部作用域的变量a(这里是一个RHS查询,即查询a的值是多少)

这是闭包吗?

书上给的答案是:技术上来讲,也许是。但根据前面的定义,确切地说并不是

我对于这句话的理解是这样的:

  1. 为什么从技术上来讲它是?

    因为bar()确实记住了所在作用域,它可以去使用这个作用域中的任何值

  2. 但为什么根据定义又说它不是呢?

    因为bar()虽然记住了所在作用域,创建了闭包,但是它并不是通过闭包的方式找到的变量a,这里对于a的查找运用到的是词法作用域的查找规则(通过作用域链一层一层向外层查找),这些规则只是闭包的一部分(很重要的一部分,但并不能代表闭包!)

很多情况下,我们更希望闭包是通过以下方式进行使用:

function foo() {
  var a = 2;
  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz(); // 2 —— 这才是闭包

我们可以看到变量a的值在它所在的作用域之外被找到了,这就是闭包的作用,它记住并访问了foo()的作用域

在正常情况下,foo()执行完成后,其内部作用域就会被垃圾回收,但是因为有了闭包的存在,该作用域得以在调用后不被销毁,以供bar()在以后的任何时间进行引用

通过这段话也就引出了书中对于闭包的定义

什么是闭包?

一个函数持有所在作用域的引用,这种引用就叫做闭包

还是拿上面那段代码举例,bar()对于foo()内部作用域的引用就是闭包。

再来看一段代码

function wait(message) {
  setTimeout(function timer() {
    console.log(message);
  }, 1000);
}
wait("Hello, closure!"); // 1000ms之后  Hello, closure!

在这段代码中,timer()是在wait()作用域之外执行的,但是它有对于wait()作用域的引用,所以成功访问了变量message,这就是闭包。

发现了吗?很多时候判断是不是闭包,不单单是看该函数是否有对所在作用域的引用,还要看该函数是否使用了闭包

我为什么这样说呢,再来看一个书上说的似是而非的闭包案例

var a = 2; 
(function IIFE() {
    console.log( a ); 
})();

书上对于立即执行函数表达式的判断是这么说的:严格来说,它并不是闭包。因为函数并不是在它所在的作用域之外执行的,这里对于变量a的查找也是通过作用域的查找规则找到的而非闭包。

所以,再次强调一下此书作者对于闭包的判断:不仅仅是看该函数是否有对所在作用域的引用(闭包),有了闭包还得去对它进行调用,这样才算是闭包

ps:本书的作者对于闭包的判断,我觉得是跟闭包是作名词使用还是动词使用有关,作者认为是动词(不仅仅要有,还要调用),不知道你们是咋判断闭包的,欢迎讨论

闭包的作用

说到闭包的作用,我们先来回想下闭包的定义是什么?

当一个函数保留有所在作用域的引用,这种引用就是闭包。

那么我们为什么需要保留这样的引用呢?因为我们想要访问闭包中的变量对不对,这里有的同学可能会说,那为什么不直接在函数中使用变量,而非得使用闭包的这种形式呢?因为每次调用一个函数,都会重新创建一个函数对象,当函数执行完成时,函数的作用域也会随之销毁,这也就意味着每次调用函数,函数内部的变量都会重新初始化,发现问题了吗?是的,函数内部的变量状态无法被保存,而我们又不希望把这些变量声明在全局作用域中(不仅浪费内存还可能会导致变量被其他地方修改还有污染全局作用域问题),有的时候我们确实希望函数在调用后,内部属性可以被保存,这个时候闭包的作用就体现出来了!

看下面这段代码,我们希望函数只能被调用time

function limit(fn, time = 1) {
  return function (...args) {
    if (time-- > 0) {
      fn.apply(this, args)
    }
  }
}
var a = function () {
  console.log('调用了a');
}
var loga = limit(a)
loga()  // 只调用了一次
loga()
loga()

闭包在模块中也发挥着十分重要的作用,模块可以通过闭包返回所需要的api,而不用返回变量

function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];
  function doSomething() {
    console.log(something);
  }
  function doAnother() {
    console.log(another.join(" ! "));
  }
  return {
    doSomething: doSomething,
    doAnother: doAnother
  };
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3