《你不知道的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(); //cool
foo.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(); //cool
foo.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 module
foo.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”的实例作为依赖参数,并能相应地使用它。

原文地址:https://www.cnblogs.com/surahe/p/5951956.html