上一节说了执行上下文,这节我们就乘胜追击来搞搞闭包!头疼的东西让你再也不头疼!浏览器
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 不会变
复制代码
其实,就是引用类型 和 基本类型的 区别。bash
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 中 主要有两种收集方式:ui
知道个大概状况就能够了,《JavaScript高级程序设计 第三版》 4.3节 有详解,有兴趣,能够看下。.this
以前说过,JavaScript中的做用域无非就是两种:全局做用域和局部做用域。 根据做用域链的特性,咱们知道,做用域链是单向的。也就是说,在函数内部,能够直接访问函数外部和全局变量,函数。可是,反过来,函数外部和全局,是访问不了函数内的变量,函数的。spa
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
。code
其实,简单点说,就是在 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
的值,回调函数做用域内没有i,向上读取,上面做用域内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);
}
复制代码
这样看就很明显了吧,主要是利用了函数做用域,而使用当即执行函数,是为了简化步骤。
总结:判断是否是闭包,我总结了要知足如下三点:
其实这道题,知道ES6
的 let
关键词,估计也想到了另外一个解法:
for (let i=1; i<=5; i++) { //这里的关键就是使用的let 关键词,来造成块级做用域
setTimeout(function(){
console.log(i);
},i*1000);
}
复制代码
我不知道,你们有没有疑惑啊,为啥使用了块级做用域就能够了呢。反正我当初就纠结了半天。
11月 2日修正:
这个答案的关键就在于 块级做用域的规则了。它让let
声明的变量只在{}
内有效,外部是访问不了的。
作下变形,这个是为了方便理解的,事实并不是如此:
for (var i=1; i<=5; i++) {
let j = i;
setTimeout(function(){
console.log(j);
},j*1000);
}
复制代码
当for 的()
内使用 let
时,for 循环就存在两个做用域,()
括号里的父做用域,和 {}
中括号里的 子做用域。
每次循环都会建立一个 子做用域。保存着父做用域传来的值,这样,每一个子做用域内的值都是不一样的。当setTimeout 的匿名函数执行时,本身的做用域没有i
的值,向上读取到了该 子做用域 的 i
值。所以每次的值才会不同。
上面用当即执行函数模拟块级做用域,就是这个道理啦!