首页 > 代码库 > 《你不知道的JavaScript》 作用域闭包

《你不知道的JavaScript》 作用域闭包

一、什么是闭包

function foo() {    var a = 2;   //函数bar( )的词法作用域能访问函数foo( )的内部作用域。将bar( )函数当做值传递。   //bar( )在foo( )内声明,它拥有涵盖foo( )内部作用域的闭包,使得该作用域能一直存活,供bar( ) 在之后任何时间引用。bar( )本身在使用foo( )的内部作用域,因此foo执行后不会被销毁。    function bar(){        console.log( a );    }    return bar;}//bar( )可以正常运行,而且是在自己的词法作用域以外执行。var baz = foo();//foo( )执行后,其返回值bar()函数赋值给变量baz,并调用baz( ),实际上是调用了内部的函数bar( )。baz();

 

bar( )依然持有对该作用域的引用,这个引用叫作闭包

  

无论通过任何手段将内部函数传递到所在词法作用域以外的,它都会有对原始定义作用域的引用,无论在何处执行这个函数都会产生闭包。

function foo() {    var a = 2;    function baz() {        console.log( a ); // 2    }    bar( baz );}function bar(fn) {    fn(); // 闭包}

 

var fn;function foo() {    var a = 2;    function baz() {        console.log( a );    }    fn = baz();}function bar() {    fn(); // 闭包}foo();bar();

 

将内部函数timer传递给setTImeout,timer涵盖wait作用域的闭包,因此还保有对message的引用。wait执行1000毫秒后,它的内部作用域不会消失,timer函数还保有wait作用域的闭包。

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

  

如果将(访问它们各自词法作用域的)函数当做第一级的值类型并到处传递,就能看到闭包在这些函数的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或任何其他的异步(同步)

任务中,只要使用了回调函数,就是在使用闭包。

function setup(name, selector) {    $( selector ).click( function activator( ) {        console.log( "Activating: " + name );       });   }setupBot( "Closure Bot 1", "#bot_1" );setupBot( "Closure Bot 2", "#bot_2" );

 

函数IIFE并不是在本身的词法作用域以外执行,在它定义时所在的作用域执行。a是通过普通的词法作用域查找而非闭包被发现的。尽管IEFF本身不少观察闭包的恰当例子,但它的确创建了闭包,

并且也是最常用来创建可以被封闭起来的闭包的工具。

var a = 2;(function IIFE( ) {    console.log( a );}());

 

 

循环和闭包

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

// 每秒一次输出5个6

 

延迟函数的回调会在循环结束后才执行。当定时器运行时即使每个迭代中执行的是setTimeout(..., 0),所有的回调函数依然在循环结束后执行。

根据作用域原理,尽管循环的五个函数在各个迭代分别定义,但它们都被在全局作用域,实际只有一个i。

 

需要更多的闭包作用域,特别是在循环的过程中每次迭代都需要闭包作用域。

//这个例子不能实现
for (var i = 0; i <= 5; i++){     (function( ) {          setTimeout( function() {               console.log(i);          }, i*1000);     }())}//每个延迟函数都会在IIFE在每次迭代中创建的作用域封闭//但这里的是空的作用域

  

它需要有自己的变量,用来在每个迭代中存储 i。

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

 

稍加改进

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

 

在迭代内使用IIFE会给每个迭代生成新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有i一个具有正确值的变量。

 

使用块作用域

for(var i = 0; i <= 5; i++){    let j = i;  //闭包的块作用域    setTimeout( function timer() {        console.log(j);    }, j*1000);}

 

for循环的let声明有一个特殊行为,变量在循环过程不止声明一次,每次迭代都会声。随后的每次迭代都会用上一个迭代结束时的值来初始化这个变量。

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

 

 

模块

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(); //coolfoo.doAnother(); // 1 ! 2 ! 3

 

 这种模式被成为模块,最常见的实现模块方式被成为模块暴露,这里的是其变体。 

首先,coolModule( )只是一个函数,必须通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。

其次,coolModule( )返回一个用对象字面量语法 { key: value, ... }来表示的对象。这个对象包含对内部函数而不是内部变量的引用。外面保持内部变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上的是模块的公共API

这个对象类型的返回值最终被赋值给外部的变量foo,然后就可以通过它来访问API的属性方法。

  • 从模块返回实际的对象不少必须的,可以返回一个内部函数,如jQuery。

模块模式需要具备两个必要条件。

  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(); //coolfoo.doAnother(); // 1 ! 2 ! 3

 

 

模块也是普通的函数,因此可以接受参数:

function coolModule(id) {    function identify() {        console.log( id );    }    return {        identify: identify    };}var foo1 = coolModule( "foo 1" );foo1.identify();  // "foo 1"

 

 

模块模式另一个简单但强大的用法是命名将要作为公共API返回的对象:

var foo = (function coolModule(id){    function change(){        publicAPI.identify = identify2;    }    function identify1() {        console.log( id );    }    function identify2() {        console.log( id.toUpperCase() );    }    var publicAPI = {        change: change,        identify: identify1    }    return publicAPI;}("foo module"));foo.identify();  //  foo modulefoo.change();foo.identify();  //  FOO MODULE

 

 

现代的模块机制

 

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    }}());

 

这段代码的核心是 modules[name] = impl.apply( impl, deps)。为了模块的定义引入了包装函数,并将返回值(模块API)存储在一个根据名字来管理的模块列表中

 

//定义模块
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" ));foo.awesome();

 

 

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

 

《你不知道的JavaScript》 作用域闭包