【你不知道的JavaScript】(五)作用域闭包
(一)垃圾回收机制
函数中局部变量经历一个生命周期:
- 当我们定义一个变量时,会为它分配一个内存空间用来储存变量的值;
- 当我们读取或者使用这个变量时,会使用它的内存空间;
- 使用完毕时会释放内存空间。
上面生命周期的最后一步,其实就是垃圾回收。JavaScript
有自动垃圾收集机制,就是找出那些不再继续使用的值,而后释放其占用的内存,那么怎样判断什么值不再使用呢,一般来说,当一个函数执行完毕,那么这个局部作用域内的变量就没有存在的必要了,它的内存空间就会释放,但是有一种情况可以阻止这一进程,那就是——闭包。
(二)闭包的概念
JavaScript
中闭包无处不在,你只要要能够识别并拥抱它。
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即便函数是在当前词法作用域之外执行。
function foo() { var a = 2; // 函数bar() 的词法作用域能够访问foo() 的内部作用域。 function bar() { console.log( a ); } return bar;}var baz = foo();baz(); // 2 —— 朋友,这就是闭包的效果。// 在foo() 执行后,其返回值(也就是内部的bar() 函数)赋值给变量baz并调用baz()// 实际上只是通过不同的标识符引用调用了内部的函数bar()。// bar() 显然可以被正常执行,而且是在自己定义的词法作用域以外的地方执行了。
↑以上代码中,在foo()
执行后,通常会期待foo()
的整个内部作用域都被销毁,由于我们知道引擎有垃圾回收器用来释放不再使用的内存空间。因为看上去foo()
的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域仍然存在,因而没有被回收。谁在使用这个内部作用域?原来是bar()
本身在使用。
拜bar()
所公告的位置所赐,它拥有涵盖foo()
内部作用域的闭包,使得该作用域能够一直存活,以供bar()
在之后任何时间进行引用。bar()
仍然持有对该作用域的引用,而这个引用就叫作闭包。
现在我懂了
- 函数在定义时的词法作用域以外的地方被调用时,闭包使得函数可以继续访问定义时的词法作用域。
- 无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
(三)循环和闭包
for (var i = 1; i <= 5; i++) { setTimeout( function timer() { console.log(new Date(),i); }, i*1000 ); }console.log("end",new Date(),i); //为方便后边演示,这里加了打印end标志
↑以上代码,正常情况下,我们对for
循环中的这段代码行为的预期是分别输出数字1~5
,每秒一次,每次一个。
但实际上,for
循环中的代码在运行时会以每秒一次的频率输出五次 6
,结果如下:
循环与闭包
为什么是每秒一次?每次都输出6呢?
那首先得对setTimeout
有正确的认识:setTimeout
的推迟不是绝对准确的
setTimeout
的意思是传递一个函数,推迟一段时候把该函数增加到队列当中,并不是立即执行;所以说假如当前正在运行的代码没有运行完,即便推迟的时间已经过完,该函数会等待到函数队列中前面所有的函数运行完毕之后才会运行;也就是说所有传递给setTimeout
的回调方法都会在整个环境下的所有代码运行完毕之后执行;
setTimeout(function(){ console.log("here");}, 0);var i = 0;//具体数值根据你的计算机CPU来决定,达到推迟效果就好while (i < 3000000000) { i ++;}console.log("test");// ↑运行上面代码,结果为在过了一段时间之后,先打印了test,而后才是here。// 而且需要注意的是,上面的代码写的是setTimeout(..,0),// 假如按照之前错误地将setTimeout函数了解为推迟一段时间执行,那这里把时间赋为0岂不是马上执行了?// 而试验结论则印证了上面“setTimeout的意思是传递一个函数,推迟一段时间把该函数增加到队列中,并不是立即执行”的结论。
再回到最初的那个问题,刚进入for
循环的时候,i
为1
,所以相对于现在推迟一秒将timer
函数增加到队列当中,而后for
循环继续(而并不是等一秒再继续循环),进行第二次循环,这时候i
为2
,所以相对于现在推迟两秒将timer
函数送进队列。以此类推,for
循环的时间可以忽略不计,timer
函数就以每秒一次的频率执行。
因为setTimeout
是异步的,所以在for
循环执行结束后(i
的值为6
),会接着执行后面的代码,等过了1s
后,最后一行代码console.log("end",new Date(),i);
早就执行完了,所以会先打印最后一行代码i
的值6
,而后以每秒一次的频率输出五次 6
。
那么问题来了?
代码中究竟有什么缺陷导致它的行为同语义所暗示的不一 致呢?
缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i
的副本。但是根据作用域的工作原理,实际情况是虽然循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因而实际上只有一个 i。
想要到达预期的结果,就得想办法让循环的过程中每个迭代都有一个闭包作用域。
1. 立即执行函数
for (var i = 1 ; i <= 5 ; ++i) { (function(j){ setTimeout( function timer(){ console.log(j); } , j*1000); })(i);}// 这里将i立即传进去,形成了封闭的5个函数// timer只能访问到传进去的那个i,也就是我们所需的i
2. ES6
中的let
与块作用域
for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }// for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,// 事实上它将其重新绑定到了循环的每一个迭代中,// 确保使用上一个循环迭代结束时的值重新进行赋值。
(四)板块
1. 板块模式
板块模式需要具有两个必要条件:
- 必需有外部的封闭函数,该函数必需至少被调用一次(每次调用都会创立一个新的板块实例)。
- 封闭函数必需返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者者修改私有的状态。
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// doSomething()和doAnother()函数具备涵盖板块实例内部作用域的闭包(通过调用CoolModule()实现)
↑以上代码中有一个叫作CoolModule()
的独立的板块创立器,可以被调用任意屡次,每次调用都会创立一个新的板块实例。当只要要一个实例时,可以对这个模式进行简单的改进来实现单例模式:
var foo = (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 }; })(); foo.doSomething(); // cool foo.doAnother(); // 1 ! 2 ! 3
2. 现代的板块机制
// 板块APIvar MyModules = (function Manager() { var modules = {}; function define(name, deps, impl) { for (var i=0; i<deps.length; i++) { deps[i] = modules[deps[i]]; } modules[name] = impl.apply( impl, deps ); // 核心 } function get(name) { return modules[name]; } return { define: define, get: get };})();
MyModules.define( "bar", [], function() { function hello(who) { return "Let me introduce: " + who; } return { hello: hello };} );
MyModules.define( "foo", ["bar"], function(bar) { var hungry = "hippo"; function awesome() { console.log( bar.hello( hungry ).toUpperCase() ); } return { awesome: awesome };} );
var bar = MyModules.get( "bar" );var foo = MyModules.get( "foo" );console.log(bar.hello("hippo")); // Let me introduce: hippofoo.awesome(); // LET ME INTRODUCE: HIPPO
"foo"
和 "bar"
板块都是通过一个返回公共 API
的函数来定义的。"foo"
甚至接受 "bar"
的 示例作为依赖参数,并能相应地使用它。
3. 未来的板块机制
ES6
中import
可以将一个板块中的一个或者多个API
导入到当前作用域中,并分别绑定在一个变量上;export
会将当前板块的一个标识符(变量、函数)导出为公共API
。
小结
当函数可以记住并访问所在的词法作用域,即便函数是在当前词法作用域之外执行,这时就产生了闭包。
板块有两个主要特征:
- 为创立内部作用域而调用了一个包装函数;
- 包装函数的返回 值必需至少包括一个对内部函数的引用,这样就会创立涵盖整个包装函数内部作用域的闭包。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 【你不知道的JavaScript】(五)作用域闭包