您现在的位置是:首页 > 文章详情

周大侠啊 进击的 JavaScript(四) 之 闭包

日期:2018-09-25点击:301

原文链接:周大侠啊 进击的 JavaScript(四) 之 闭包

上一节说了执行上下文,这节咱们就乘胜追击来搞搞闭包!头疼的东西让你不再头疼!

一、函数也是引用类型的。

function f(){ console.log("not change") }; var ff = f; function f(){ console.log("changed") }; ff(); //"changed" //ff 保存着函数 f 的引用,改变f 的值, ff也变了 //来个对比,估计你就明白了。 var f = "not change"; var ff = f; f = "changed"; console.log(ff); //"not change" //ff 保存着跟 f 一样的值,改变f 的值, ff 不会变

其实,就是引用类型 和 基本类型的 区别。

二、函数创建一个参数,就相当于在其内部声明了该变量

function f(arg){ console.log(arg) } f(); //undefined function f(arg){ arg = 5; console.log(arg); } f(); //5


三、参数传递,就相当于变量复制(值的传递)

基本类型时,变量保存的是数据,引用类型时,变量保存的是内存地址。参数传递,就是把变量保存的值 复制给 参数。

var o = { a: 5 }; function f(arg){ arg.a = 6; } f(o); console.log(o.a); //6


四、垃圾收集机制

JavaScript 具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。函数中,正常的局部变量和函数声明只在函数执行的过程中存在,当函数执行结束后,就会释放它们所占的内存(销毁变量和函数)。

而js 中 主要有两种收集方式:

  1. 标记清除(常见) //给变量标记为“进入环境” 和 “离开环境”,回收标记为“离开环境”的变量。
  2. 引用计数 // 一个引用类型值,被赋值给一个变量,引用次数加1,通过变量取得引用类型值,则减1,回收为次数为0 的引用类型值。

知道个大概情况就可以了,《JavaScript高级程序设计 第三版》 4.3节 有详解,有兴趣,可以看下。.

五、作用域

之前说过,JavaScript中的作用域无非就是两种:全局作用域局部作用域
根据作用域链的特性,我们知道,作用域链是单向的。也就是说,在函数内部,可以直接访问函数外部和全局变量,函数。但是,反过来,函数外部和全局,是访问不了函数内的变量,函数的。

function testA(){ var a = 666; } console.log(a); //报错,a is not defined var b = 566; function testB(){ console.log(b); } //566

但是,有时候,我们需要在函数外部 访问函数内部的变量,函数。一般情况下,我们是办不到的,这时,我们就需要闭包来实现了。


六、开始闭包!

function fa(){ var va = "this is fa"; function fb(){ console.log(va); } return fb; } var fc = fa(); fc(); //"this is fa"

想要读取fa 函数内的变量 va,我们在内部定义了一个函数 fb,但是不执行它,把它返回给外部,用 变量fc接受。此时,在外部再执行fc,就读取了fa 函数内的变量 va

七、闭包的概念

其实,简单点说,就是在 A 函数内部,存在 B 函数, B函数 在 A 函数 执行完毕后再执行。B执行时,访问了已经执行完毕的 A函数内部的变量和函数。

由此可知:闭包是函数A的执行环境 以及 执行环境中的函数 B组合而构成的。

上篇文章中说过,变量等 都储存在 其所在执行环境的活动对象中,所以说是 函数A 的执行环境。

当 函数A执行完毕后,函数B再执行,B的作用域中就保留着 函数A 的活动对象,因此B中可以访问 A中的 变量,函数,arguments对象。此时产生了闭包。大部分书中,都把 函数B 称为闭包,而在谷歌浏览器中,把 A函数称为闭包。

八、闭包的本质

之前说过,当函数执行完毕后,局部活动对象就会被销毁。其中保存的变量,函数都会被销毁。内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况就不同了。

以上面的例子来说,函数fb 和其所在的环境 函数fa,就组成了闭包。函数fa执行完毕后,按道理说, 函数fa 执行环境中的 活动对象就应该被销毁了。但是,因为 函数fa 执行时,其中的 函数fb 被 返回,被 变量fc 引用着。导致,函数fa 的活动对象没有被销毁。而在其后 fc() 执行,就是 函数fb 执行时,构建的作用域中保存着 函数fa 的活动对象,因此,函数fb 中 可以通过作用域链访问 函数fa 中的变量。

我已经尽力地说明白了。就看各位的了。哈哈!其实,简单的说:就是fa函数执行完毕了,其内部的 fb函数没有执行,并返回fb的引用,当fb再次执行时,fb的作用域中保留着 fa函数的活动对象。

再来个有趣经典的例子:

for (var i=1; i<=5; i++) { setTimeout(function(){ console.log(i); },i*1000); } //每隔一秒输出一个6,共5个。

是不是跟你想的不一样?其实,这个例子重点就在setTimeout函数上,这个函数的第一个参数接受一个函数作为回调函数,这个回调函数并不会立即执行,它会在当前代码执行完,并在给定的时间后执行。这样就导致了上面情况的发生。

可以下面对这个例子进行变形,可以有助于你的理解把:

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

正因为,setTimeout里的第一个函数不会立即执行,当这段代码执行完之后,i 已经 被赋值为6了(等于5时,进入循环,最后又加了1),所以 这时再执行setTimeout 的回调函数,读取 i 的值,就是6了。但是 i * 1000,是立即执行的,所以,每次读的 i 值 都是对的。

这时候,就需要利用闭包来保存每个循环时, i 不同的值。

function makeClosures(i){ //这里就和 内部的匿名函数构成闭包了 var i = i; //这步是不需要的,为了让看客们看的轻松点 return function(){ console.log(i); //匿名没有执行,它可以访问i 的值,保存着这个i 的值。 } } for (var i=1; i<=5; i++) { setTimeout(makeClosures(i),i*1000); //这里简单说下,这里makeClosures(i), 是函数执行,并不是传参,不是一个概念 //每次循环时,都执行了makeClosures函数,都返回了一个没有被执行的匿名函数 //(这里就是返回了5个匿名函数),每个匿名函数内部保存着每次传进来的i值, //因此,每个i 都是不一样的,所以,就得到了想要的结果 } //1 //2 //3 //4 //5

闭包的关键就在,外部的函数执行完毕后,内部的函数再执行,并访问了外部函数内的变量。

你可能在别处,或者自己想到了下面这种解法:

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

如果你一直把这个当做闭包,那你可能看到的是不同的闭包定义吧(犀牛书和高程对闭包的定义不同)。严格来说,这不是闭包,这是利用了立即执行函数函数作用域 来解决的。

做下变形,你再看看:

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

这样看就很明显了吧,主要是利用了函数作用域,而使用立即执行函数,是为了简化步骤。

总结:判断是不是闭包,我总结了要满足以下三点:

  1. 两个函数。有内函数 和 外函数。
  2. 外函数执行完毕后,内函数 还没有执行。
  3. 当内函数执行时(通过外部引用或者返回内函数),访问了 外函数内部的 变量,函数等(说是访问,其实内函数保存着外函数的活动对象,因此,arguments对象也可以访问到)。




附录:

其实这道题,知道ES6let 关键词,估计也想到了另一个解法:

for (let i=1; i<=5; i++) { //这里的关键就是使用的let 关键词,来形成块级作用域 setTimeout(function(){ console.log(i); },i*1000); }

我不知道,大家有没有疑惑啊,为啥使用了块级作用域就可以了呢。反正我当初就纠结了半天。

这个答案的关键就在于 块级作用域的规则了。它只在{}内有效,外部是访问不了的。因为,当{} 内的代码执行完后,let 声明的变量 就会被销毁了。这就导致了。其内部如果有二级的 {},该二级块作用域就会保存 let 声明的变量,直至它执行后才会被销毁。

做下变形:

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

其实,for 循环时,每次都会用let创建一个新变量,并以之前迭代中同名变量的值将其初始化。

setTimeout 的第一个函数就包含了一个 {} 块级作用域。而其并没有立刻执行,它保存了,每次循环时, 不同的 j 值。 直至指定时间后,执行完毕后, j 的值 才会被销毁。

原文链接:https://yq.aliyun.com/articles/645738
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章