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

(一)垃圾回收机制

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

  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. 现代的模块机制

// 模块API
var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i=0; i
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: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO

"foo""bar" 模块都是通过一个返回公共 API 的函数来定义的。"foo" 甚至接受 "bar" 的 示例作为依赖参数,并能相应地使用它。

3. 未来的模块机制

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

小结

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

模块有两个主要特征

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

你可能感兴趣的:(【你不知道的JavaScript】(五)作用域闭包)