【你不知道的JavaScript】(五)作用域闭包

作者 : 开心源码 本文共4310个字,预计阅读时间需要11分钟 发布时间: 2022-05-12 共185人阅读

(一)垃圾回收机制

函数中局部变量经历一个生命周期:

  1. 当我们定义一个变量时,会为它分配一个内存空间用来储存变量的值;
  2. 当我们读取或者使用这个变量时,会使用它的内存空间
  3. 使用完毕时会释放内存空间

上面生命周期的最后一步,其实就是垃圾回收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() 仍然持有对该作用域的引用,而这个引用就叫作闭包

现在我懂了

  1. 函数在定义时的词法作用域以外的地方被调用时,闭包使得函数可以继续访问定义时的词法作用域。
  2. 无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

(三)循环和闭包

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循环的时候,i1,所以相对于现在推迟一秒将timer函数增加到队列当中,而后for循环继续(而并不是等一秒再继续循环),进行第二次循环,这时候i2,所以相对于现在推迟两秒将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. 未来的板块机制

ES6import 可以将一个板块中的一个或者多个API导入到当前作用域中,并分别绑定在一个变量上;export 会将当前板块的一个标识符(变量、函数)导出为公共API

小结

当函数可以记住并访问所在的词法作用域,即便函数是在当前词法作用域之外执行,这时就产生了闭包。

板块有两个主要特征

  • 为创立内部作用域而调用了一个包装函数;
  • 包装函数的返回 值必需至少包括一个对内部函数的引用,这样就会创立涵盖整个包装函数内部作用域的闭包。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 【你不知道的JavaScript】(五)作用域闭包

发表回复