zoukankan      html  css  js  c++  java
  • 《你不知道的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
Copyright © 2011-2022 走看看