amandakelake/blog

You don't konw JavaScript => 闭包

amandakelake opened this issue · 0 comments

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

这段代码,严格意义上来说并不属于闭包
虽然bar劫持了foo作用域中的a变量,但是它在foo执行时也同时执行了,并没有把foo的作用域告诉foo之外的兄弟们。
当foo执行完毕后,JS的自动垃圾回收机制,会把a变量回收,因为已经没有其他的函数或者什么地方保持对a的引用了,说白了,就是没有把bar所引用的函数对象当做返回值返回

其他地方的引用

JavaScript拥有自动的垃圾回收机制,关于垃圾回收机制,有一个重要的行为,那就是,当一个值,在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存

再来看一段代码

function foo() {
  var a = 2;
  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。

看到了吗,foo()执行后,返回值(内部的bar()函数)赋值给了变量baz,这个时候,变量baz就保持了对foo内部的a变量的引用,按照上面说的垃圾回收机制,foo的作用域就没办法被销毁了,因为a卡在内存中,也就说闭包的存在,阻止了foo的内部作用域被回收这一过程

其他地方的引用

函数的执行上下文,在执行完毕之后,生命周期结束,那么该函数的执行上下文就会失去引用。其占用的内存空间很快就会被垃圾回收器释放,闭包的存在,会阻止这一过程。

到这里,我自己的理解就是:当一个函数所定义的内部作用域,可以在外部被访问到,就产生了闭包
再看书里面的定义,这些话就好理解多了

bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用 域的引用,无论在何处执行这个函数都会使用闭包

再来看一段代码

var fn;
function foo() {
  var a = 2;
  function baz() {
    console.log(a);
  }
  fn = baz; // 将 baz 分配给全局变量
}

function bar() {
  fn();// 妈妈快看呀,这就是闭包!
}

foo();
bar(); //2

上面把内部函数baz传递了出来,全局变量fn保持了对baz的引用,当执行bar()的时候,间接调用了baz,也就是调用了foo中的a变量,闭包就形成了

举一反三来想,我们平时使用回调函数的时候,不正是将内部函数传递到所在的词法作用域以外么?
说明了什么?
说明调用回调函数的过程,就是使用了闭包呀,开心
什么定时器、事件监听、网络请求、异步操作、跨窗口通信、web worker、service worker等等,不都是在使用闭包么

现在回到一道经典的循环题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

稍微有点基础的都知道,会输出五个6
因为循环结束时,i = 6
这里循环时的每个i都共享同一个全局作用域,同时因为setTimeout是延迟执行的,所以输出全是最后的那个i

那么每次的时长间隔又是多少呢?
有同学可能会以为
先是1s后输出6,然后间隔2s后再输出一个6,然后3s、4s、5s
我告诉你,这样是错的

你先打印这个东西看一下
f2700abd-00f7-485c-92b5-c0cd95c2762a

每次的i是不是不一样
对,是不一样
但是,这个时延,其实是相对与开始执行这个for循环时开始,并不是相对于上一个循环开始,这里要好好区分一下
也就是说从开始执行for循环时,1s后输出6,2s后输出6,……
所以每次输出的间隔都是1s
这样说,应该明白了吧

然后,下一个问题
怎么依次输出1,2,3,4,5呢?
先加个IIFE试一下

for (var i = 1; i <= 5; i++) {
  (function() {
    setTimeout(function timer() {
      console.log(i);
    }, i * 1000);
  })();
}

答案还是5个6,为什么呢?

如果作用域是空的,那么仅仅将它们进行封闭是不够的。仔细看一下,我们的 IIFE 只是一 个什么都没有的空作用域。它需要包含一点实质内容才能为我们所用。
它需要有自己的变量,用来在每个迭代中储存 i 的值:

那就把i传进去吧

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

这次终于对了

大兄弟,闭包在哪呢?前面不是说IIFE跟闭包不太像么
IIFE是在函数本身所定义时的作用域内(),并不是在作用域之外被执行的

尽管 IIFE 本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建 可以被封闭起来的闭包的工具。因此 IIFE 的确同闭包息息相关,即使本身并不会真的使用 闭包。

那就用个闭包

for (var i = 1; i <= 5; i++) {
  let j = i; //这里就是闭包的快作用域
  setTimeout(function timer() {
    console.log(j);
  }, j * 1000);
}

再来个酷点的

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

闭包写到这里,基本概念就已经写完了
书里还有关于模块的高级用法,就留给大伙(包括我自己)去慢慢研读吧。