Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

You don't konw JavaScript => 闭包 #24

Open
amandakelake opened this issue Feb 28, 2018 · 0 comments
Open

You don't konw JavaScript => 闭包 #24

amandakelake opened this issue Feb 28, 2018 · 0 comments
Labels

Comments

@amandakelake
Copy link
Owner

amandakelake commented Feb 28, 2018

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);
}

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant