一周一章前端書·第5周:《你不知道的JavaScript(上)》S01E05

第5章:作用域閉包

到底什么是閉包

  • 本章講解閉包(Closures),它與作用域工作原理息息相關(guān)。
  • 首先我用自己總結(jié)的三句話,簡單說明什么是閉包:
    • (1)首先我們要知道,變量的查找 規(guī)則 是由內(nèi)到外的;
    • (2)所以 子函數(shù)可以訪問外部作用域 的變量;
    • (3)如果 把子函數(shù)賦值給外部變量 時,此時外部變量就 擁有 了可以 訪問封閉數(shù)據(jù)包的能力 ;
  • 一個簡單的閉包示例
function foo(){
    var a = 2;
    function bar(){
        console.log(a); //2
    }
    return bar;
}

var baz = foo();
  • 分析上述代碼:
    • bar函數(shù)能訪問外部foo函數(shù)的作用域,將bar傳遞給外部變量baz來執(zhí)行;
    • 此時bar函數(shù)在原來定義的詞法作用域之外執(zhí)行,同時持有foo函數(shù)作用域的引用,這就叫作閉包。
  • 并且通過閉包的執(zhí)行方式,foo函數(shù)在執(zhí)行后,其作用域不會被立即銷毀(畢竟bar函數(shù)還要用的啊)

閉包暴露的方式不止一種

  • 閉包函數(shù)除了可以直接賦值給外部變量,也可以通過執(zhí)行外部函數(shù),將閉包函數(shù)以參數(shù)傳遞的方式暴露出去。
var foo(){
    var a = 2;
    //閉包函數(shù)
    function baz(){
        console.log(a);; //2
    }
    //執(zhí)行外部函數(shù),將閉包函數(shù)通過參數(shù)的方式傳遞進去
    bar(baz)
}

var bar(fn){
    fn();
}

閉包是最熟悉的陌生人

  • 雖然閉包比較隱晦,但它絕不僅僅是一個好玩的玩具而已,在我們的代碼中到處都有它的身影。
  • 比如常見的定時函數(shù)setTimeout()
function wait(message){
    setTimeout(function timer(){
        console.log(message);
    },1000)
}

上述代碼等價于:

//全局的setTimout函數(shù)準備就緒
var setTimout = function(invokeFn){}
function wait(message){
    //timer內(nèi)部函數(shù)擁有對mesage的訪問權(quán)
    var timer = function(){
        console.log(message);
    }
    //執(zhí)行setTimeout()函數(shù),并將timer以參數(shù)方式傳遞進去
    setTimout(timer);
}
  • 是不是有似曾相似的感覺?內(nèi)部函數(shù)timer()具有外部函數(shù)wait()作用域中的message變量的引用,它就是閉包。
  • 除了定時函數(shù)之外,jQuery代碼也普遍的在使用閉包,不信你看下面的代碼:
//參數(shù)傳遞一個name字符串,選擇器字符串
function setupBot(name,selector){
    //通過選擇器字符串初始化成jQuery對象
    //綁定點擊事件
    $(selector).click(function activator(){
        //打印外部函數(shù)的name
        console.log('Activating:' + name);
    })
}

setupBot('Closure Bot 1','#bot_1');
setupBot('Closure Bot 2','#bot_2');
  • 其實無論何時何地,只要將函數(shù)當(dāng)做參數(shù)進行傳遞,就有閉包的應(yīng)用。
  • 什么定時器、事件監(jiān)聽函數(shù)、Ajax請求回調(diào)函數(shù)、跨窗口通信、Web Workers等代碼中,都普遍應(yīng)用到了閉包。
  • 它每天與我們擦肩而過,就好像那個最熟悉的陌生人。

閉包解決了什么問題

1. 用閉包造塊級作用域

  • 我們先看問題: 我只是想依次輸出循環(huán)的i(1 ~ 5)
for(var i=1;i<=5;i++){
    setTimtou(function timer(){
        console.log(i);  
    },0)
}
  • 見鬼了,然而輸出的結(jié)果卻是五次6,這是為什么呢?
  • 其根本原因是,定時器的回調(diào)函數(shù)永遠在循環(huán)結(jié)束后才執(zhí)行。
  • 那你可能會想,哦,那我豈不是永遠都不能在for循環(huán)中用定時器了?JavaScript真垃圾!誒誒,先別慌,我們來分析一下。
  • 我們不是預(yù)期循環(huán)的每個迭代中,都有一個i的副本,然后輸出它嗎?通過閉包就能實現(xiàn)。
for(var i=1;i<=5;i++){
    (function(index){
        setTimtou(function timer(){
            console.log(index);  
        },0)
    })(i);
}
  • 我們通過IIFE構(gòu)造了一個塊級作用域?qū)?code>i存了起來。
  • 提到塊級作用域,其實ES6的語法里還有一種更便捷的解決方式——let聲明
for(let i=1;i<=5;i++){
    setTimtou(function timer(){
        console.log(i);  
    },0)
}

2. 用閉包造模塊

function CoolModule(){
    var something = 'cool';
    var another = [1, 2, 3];
    
    function doSomething(){
        console.log(something);
    }
    
    function doAnother(){
        console.log(another.join(","));
    }
    
    //對外暴露內(nèi)部函數(shù)
    return {
        doSomething : doSomething,
        doAnother : doAnother
    }
}
  • 上述代碼演示了JavaScript模塊暴露。通過調(diào)用CoolModule函數(shù)創(chuàng)建一個模塊實例,CoolModule返回的對象中包含內(nèi)部函數(shù)的引用,就相當(dāng)于模塊的公共API。
  • 當(dāng)然上述代碼可以任意調(diào)用多次,重復(fù)返回新的模塊實例,我們可以改成單例模式:
//通過一個IIFE函數(shù)來包裝
var foo = (function CoolModule(){
    var something = 'cool';
    var another = [1, 2, 3];
    
    function doSomething(){
        console.log(something);
    }
    
    function doAnother(){
        console.log(another.join(","));
    }
    
    //對外暴露內(nèi)部函數(shù)
    return {
        doSomething : doSomething,
        doAnother : doAnother
    }
})();

foo.doSomething();  //cool
foo.doAnother();    //1,2,3
  • 模塊的公共API不僅可以是內(nèi)部私有變量的訪問,也可以是修改私有變量的方法:
var foo = (function CoolModule(id){
    var moduleId = id;

    function showId(){
        console.log(moduleId);
    }

    function uppcaseId(){
        moduleId = moduleId.toUpperCase();
    }

    return{
        showId : showId,
        uppcaseId : uppcaseId
    }
})('fooModule');

foo.showId();
foo.uppcaseId();
foo.showId();
  • 其實大多數(shù)模塊管理機制本質(zhì)是也是通過類似的方式來實現(xiàn)的,我們來嘗試寫一個簡版的模塊管理器:
/**
 * 定義牛批哄哄的超級模塊管理器
 */
var SuperModules = (function(){
    //所有的模塊集合,以name作為key
    var moduleMap = {};
    
    function define(name,deps,impl){
        //獲取依賴
        for(var i=0;i<deps.length;i++){
            deps[i] = moduleMap[deps[i]]        
        }
        //執(zhí)行引入的模塊,并以deps作為參數(shù)
        moduleMap[name] = impl.apply(impl,deps);
    }
    
    function get(name){
        reutrn moduleMap[name];
    }
    
    //暴露公共API
    return{
        define : define,
        get : get
    }
})()

/**
 * 先定義一個bar模塊
 * 沒有依賴,impl是一個執(zhí)行函數(shù)
 */
SuperModules.define('bar',[],function(){
    function hello(name){
        return 'let me introduce:' + name;
    }
    return {
        hello : hello
    }
})

/**
 * 再定義一個foo模塊
 */
SuperModules.difine('foo',['bar'],function(bar){
    var hungry = 'hippo';
    
    function awesome(){
        console.log(bar.hello(hungry).toUpperCase());
    }
    
    return {
        awesome : awesome
    }
})

/**
 * 調(diào)用測試
 */
var bar = SuperModules.get('bar');
var foo = SuperModules.get('foo');

//let me introduce: hippo
console.log(bar.hello('hippo'));
//let me introduce: HIPPO
foo.awesome();
  • 是不是看起來比較復(fù)雜……不過不用擔(dān)心,ES6以及添加了模塊的語法支持!
  • ES6會將每個js文件當(dāng)做獨立的模塊來處理,每個模塊可以通過import關(guān)鍵字導(dǎo)入依賴的模塊,或者通過export關(guān)鍵字導(dǎo)出API。你需要做的,只是擁抱ES6!
  • bar.js
function hello(name){
    return 'let me introduce:' + name;
}
export hello;
  • foo.js
import hello from 'bar';

var hungry = 'hippo';
function awesome(){
    console.log(bar.hello(hungry).toUpperCase());
}
export awesome;
  • baz.js
module foo from 'foo';
module bar from 'bar';

//let me introduce: hippo
console.log(bar.hello('hippo'));
//let me introduce: HIPPO
foo.awesome();

小結(jié)

  • 內(nèi)部函數(shù)可以訪問外部函數(shù)的作用域,如果將內(nèi)部函數(shù)暴露給外部變量時,或者說內(nèi)部函數(shù)在定義的詞法作用域之外執(zhí)行時,就產(chǎn)生了閉包。
  • 閉包是個非常強大的工具,可以用來實現(xiàn)模塊模式。
  • 模塊的特征:
    • 為創(chuàng)建內(nèi)部作用域而調(diào)用一個包裝函數(shù);
    • 包裝函數(shù)的返回值必須至少包含一個對內(nèi)部函數(shù)的引用,這樣就會創(chuàng)建涵蓋整個包裝函數(shù)內(nèi)部作用域的閉包;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容