读书笔记《你不知道的JavaScript上卷》1.5作用域闭包

版权声明:本文为博主逝水流光原创,可以转载,请注明出处。 https://blog.csdn.net/javao_0/article/details/75008300

5.1 闭包的概念

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

这是闭包的定义,对于一个作用域(就比如函数所形成的作用域)来说,通常情况下,当执行完这个函数以后,JS引擎的垃圾回收机制就可能会回收掉这个函数所占用的内存,但是如果这个函数形成了闭包那么它就不会回收掉这个函数所占用的内存了。
举个简单的例子:

function foo() {
    var a = 2;
    function bar() {
        console.log( a );
    }
    return bar;//这里bar被返回去了,所以bar是不会被垃圾回收机制回收掉的,所以产生了闭包
}
var baz = foo();
baz(); //这个地方已经在函数foo的作用域外面了,但是仍然可以访问函数内部的东西

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
上面使用return是一种最常见的形成闭包的方式,还有一种常用的方式就是回调函数:

function foo() {
    var a = 2;
    function baz() {
        console.log( a ); // 2
    }
    bar( baz );//这里把baz当做参数传进去了
}
function bar(fn) {
    fn(); // 所以在这里其实是执行了foo中的baz,也就是bar作用域调用了另外一个作用域foo中的东西,所以也产生了闭包
}

只要使用了回调函数,实际上就是在使用闭包,我们常用的setTimeout函数其实就是一个闭包,这里就不再重复了。
综上,闭包并不是什么高大上的东西,我们有意或者无意肯定使用过了好多闭包。

5.2 闭包的功能

闭包有两大功能:

  • 1.访问局部变量
  • 2.变量所占的内存不被释放,以实现数据共享

其中第二点有同样会产生一个问题就是内存溢出,所以使用闭包时要小心。

5.3 闭包与循环

有一个经典的不能再经典的面试题,就是分别输出1~5,每一秒执行一次,每次一个,现在考虑如下代码:

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

它执行结果是每秒打印一个数字,打印的数字全部都是6。为什么会产生这样的情况呢?
我们知道setTimeout里面的函数timer是一个回调函数,它并不会立即执行,而是等到相应的时间到了才会执行,每次循环的时候都会启动这么一个延迟函数,让他分别在1~5秒后执行函数timer,但是当回调函数timer执行的时候这个循环早就结束了,而timer中引用的i是循环中的i,循环结束后i的值是6,所以都会打印6。其实这里如果把循环的时间设置成0,同样的也会打印出5个6,因为JS是单线程的(不考虑新特性),当调用setTimeout时他会把回调函数放在一个新的调用栈中,然后等线程空闲了,它才会在相应的时间去处理回调函数,而如果时间是0的话,那么线程把回调函数放入栈后,线程并没有空闲而会继续执行setTimeout后面的语句,并没有立刻执行回调方法。
那么怎么样才能使得这个功能正常工作呢?

for (var i=1; i<=5; i++) {
    (function(){
        var j = i;
        setTimeout( function timer(){
            console.log( j );
        }, j*1000 );
    })();//使用自调用函数产生一个新的作用域,并定义一个临时变量接收i
}

其实除了上面这种形式意外还有一种形式,下面这种方式直接传递参数进去,代码更加简洁:

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

在ES6中引入了let它会形成作用域,可以更好的解决这个问题:

for (let i=1; i<=5; i++) {//注意这里使用的是let不是var
    setTimeout( function timer(){
        console.log( i );
    }, i*1000 );
}

5.4 模块

5.4.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

这几行代码很容易看懂,函数CoolModule有return,形成了闭包,而CoolModule其实就是一个模块。这样做的好处就是屏蔽了内部的属性,并且向外暴露了接口,可以有效的解决无意中产生的变量名重复等问题。这个模式在JavaScript中被称为模块
模块的两个必要条件:

1.必须有外部的封闭的函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域形成闭包,并且可以访问或者修改私有的状态。

上面的这种模块是可以创建多个模块实例,更多情况下我们只需要创建一个模块实例,如下:

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

如果我们把变量foo命名为$,里面函数是对DOM做一些操作,那么这不是就和JQuery类似了吗?其实,JQuery就是引用了模块机制。

5.4.2 现代模块机制

现在模块机制将不作为重点,这里直接给出一些代码,有兴趣的读者可以参考seaJS,RequireJS等模块化类库。

var 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(){//这里定义了一个模块bar
    function hello(who) {
        return "Let me introduce: " + who;
    }

    return {
        hello: hello
    };
} );

MyModules.define( "foo", ["bar"], function(bar){//这里定义了一个模块foo
    var hungry = "hippo";

    function awesome() {
        console.log( bar.hello( hungry ).toUpperCase() );
    }

    return {
        awesome: awesome
    };
} );

var bar = MyModules.get( "bar" );//获取模块bar
var foo = MyModules.get( "foo" );//获取模块foo

console.log(
    bar.hello( "hippo" )
); // 模块bar调用方法
foo.awesome(); //模块foo调用方法

5.4.3 未来模块机制

未来模块机制就是ES6自带的export和import,有兴趣的可以参考阮一峰的ES6相关文章

猜你喜欢

转载自blog.csdn.net/javao_0/article/details/75008300