《你不知道的JavaScript》-- 精讀(五)

知識點

1.實質問題

當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行。閉包

function foo(){
    var a = 2;
    function bar(){
        console.log(a); // 2
    }
    bar()
}
foo()
複製代碼

根據前面的定義,嚴格來講上述代碼並非閉包,最準確地用來解釋bar()對a的引用的方法是詞法做用域的查找規則,而這些規則只是閉包的一部分。app

function foo(){
    var a = 2;
    function bar(){
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz(); // 2 ---- 這就是閉包的效果
複製代碼

上述代碼中,在foo()執行後,其返回值(也就是內部的bar()函數)賦值給變量baz並調用baz(),實際上只是經過不一樣的標識符引用調用了內部的函數bar()。異步

bar()顯然能夠被正常執行。可是在這個例子中,它在本身定義的詞法做用域之外的地方執行。ide

在foo()執行後,一般會期待foo()的整個內部做用域都被銷燬,由於咱們知道引擎有垃圾回收器來釋放再也不使用的內存空間。而閉包的「神奇」之處正是能夠阻止這件事情的發生。事實上內部做用域依然存在,所以沒有被回收,由於bar()自己在使用。函數

由於bar()所聲明的位置,它擁有涵蓋foo()內部做用域的閉包。使得該做用域一直存活,以供bar()在以後任什麼時候間進行引用。工具

bar()依然持有對該做用域的引用,而這個引用就叫做閉包。ui

閉包使得函數能夠繼續訪問定義時的詞法做用域。固然,不管使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時均可以觀察到閉包。spa

function foo(){
    var a = 2;
    function baz(){
        console.log(a);
    }
    bar(baz);
}
function bar(fn){
    fn(); // 這就是閉包
}
複製代碼

把內部函數baz傳遞給bar,當調用這個內部函數時(如今叫做fn),它涵蓋的foo()內部做用域的閉包就能夠觀察到了,由於它可以訪問a。code

傳遞函數固然也能夠是間接的。對象

var fn ;
function foo(){
    var a = 2;
    function baz(){
        console.log(a);
    }
    fn = baz; // 將baz分配給全局變量
}
function bar(){
    fn(); // 這就是閉包
}
複製代碼

不管經過何種手段將內部函數傳遞到所在的詞法做用域之外,它都會持有對原始定義做用域的引用,不管在何處執行這個函數都會使用閉包。

2.如今我懂了

function wait(message){
    setTimeout(function timer(){
        console.log(message);
    },1000)
}
wait("Hello,closure!")
複製代碼

將一個內部函數(名爲timer)傳遞給setTimeout(..)。timer具備涵蓋wait(..)做用域的閉包,所以還保有對變量message的引用。

wait(..)執行1000毫秒後,它的內部做用域並不會消失,timer函數依然保有wait(..)做用域的閉包。

本質上,不管什麼時候何地,若是將(訪問它們各自詞法做用域的)函數看成第一級的值類型並處處傳遞,你就會看到閉包在這些函數中的做用。在定時器、事件監聽器、Ajax請求、跨窗口通訊、Web Workers或者任何其餘的異步(或者同步)任務中,只要使用了回調函數,實際上就是在使用閉包。

3.循環和閉包

for(var i = 1;i <= 5; i++){
    setTimeout(function timer(){
        console.log(i); // 每秒一次的頻率輸出5次6
    },i*1000)
}
複製代碼

根據做用域的工做原理,實際狀況是儘管循環中的五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以實際上只有一個i。

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

在迭代內使用IIFE會爲每一個迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代中都會含有一個具備正確值的變量供咱們訪問。

4.重返塊做用域

for(let i = 1; i <= 5; i++){
    setTimeout(function timer(){
        console.log(i);
    },i*1000)
}
複製代碼

5.模塊

function foo(){
    var something = "cool";
    var another = [1,2,3];
    function doSomething(){
        console.log(something);
    }
    function doAnother(){
        console.log(another.join(" ! ")
    }
}
複製代碼

私有數據變量something和another,以及doSomething()和doAnother()兩個內部函數,它們的詞法做用域(而這就是閉包)也就是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
    }
}

var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1!2!3
複製代碼

這個模式在JavaScript中被稱爲模塊。最多見的實現模塊的方法一般被稱爲模塊暴露,這裏展現的是其變體。

首先,CoolModule()只是一個函數,必需要經過調用它來建立一個模塊實例。若是不執行外部函數,內部做用域和閉包都沒法被建立。

其次,CoolModule()返回一個用對象字面量語法 {key: value,...}來表示的對象。這個返回的對象中含有對內部函數而不是內部數據變量的引用。咱們保持內部數據變量是隱藏且私有的狀態。能夠將這個對象類型的返回值看做本質上是模塊的公共API。

模塊模式須要具有兩個必要條件。

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
複製代碼

將模塊函數轉換成了IIFE,當即調用這個函數並將返回值直接賦值給單例的模塊實例標識符foo。

模塊也是普通的函數,所以能夠接收參數:

function CoolModule(id){
    function identify(){
        console.log(id);
    }
    return {
        identify: identify
    }
}
var foo1 = CoolModule("foo 1");
var foo2 = CoolModule("foo 2");

foo1.identify(); // "foo 1"
foo2.identify(); // "foo 2"
複製代碼

模塊模式的一個簡單但強大的用法是命名將要做爲公共API返回的對象:

var foo = (function CoolModule(id){
    function change(){
        // 修改公共API
        publicAPI.identify = identify2;
    }
    function identify1(){
        console.log(id);
    }
    function identify2(){
        console.log(id.toUpperCase());
    }
    var publicAPI = {
        change: change,
        identify: identify1
    }
    return publicAPI;
}("foo mocule");

foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE
複製代碼

經過在模塊實例的內部保留對公共API對象的內部引用,能夠從內部對模塊實例進行修改,包括添加或刪除方法和屬性,以及修改它們的值。

6.現代的模塊機制

大多數模塊依賴加載器/管理器本質上都是將這種模塊定義封裝進一個友好的API。

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")); // Let me introduce: hippo
foo.awefome(); // LET ME INTRODUCE: HIPPO
複製代碼

"foo"和"bar"模塊都是經過一個返回公共API的函數來定義的。"foo"甚至接受"bar"的實例做爲依賴參數,並能相應地使用它。

模塊就是模塊,即便在它們外層加上一個友好的包裝工具也不會發生任何變化。

7.將來的模塊機制

ES6中爲模塊增長了一級語法支持。在經過模塊系統進行加載時,ES6會將文件當作獨立的模塊來處理。每一個模塊均可以導入其餘模塊或特定的API成員,一樣也能夠導出本身的API成員。

基於函數的模塊並非一個能被靜態識別的模式,所以能夠在運行時修改一個模塊的API。 相比之下,ES6模塊API是靜態的,所以在編譯期就會檢查導入模塊的API成員的引用是否存在,若是不存在,編譯器會在編譯時就報錯,而不會等到運行期動態解析(而且報錯)。

ES6的模塊沒有「行內」格式,必須被定義在獨立的文件中。

// bar.js
function hello(who){
    return "Let me introduce: " + who;
}
export hello;

// foo.js
// 僅從"bar"模塊導入hello()
import hello from "bar";
var hungry = "hippo";
function awesome(){
    console.log(hello(hungry).toUpperCase());
}
export awesome;

// baz.js
// 導入完整的"foo"和"bar"模塊
module foo from "foo";
module bar from "bar";

console.log(bar.hello("rhino"));
foo.awesome();
複製代碼

import能夠將一個模塊中的一個或多個API導入到當前做用域中,並分別綁定在一個變量上。module會將整個模塊的API導入並綁定到一個變量上。export會將當前模塊的一個標識符(變量、函數)導出爲公共API。這些操做能夠在模塊定義中根據須要使用任意屢次。

總結

咱們在詞法做用域的環境下寫代碼,而其中的函數也是值,能夠隨意傳來傳去。

當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這時就產生了閉包。

閉包是一個很是強大的工具,能夠用多種形式來實現模塊等模式。

模塊有兩個主要特徵:

  • 1.爲建立內部做用域而調用了一個包裝函數
  • 2.包裝函數的返回值必須至少包括一個對內部函數的引用,這樣就會建立涵蓋整個包裝函數內部做用域的閉包。

巴拉巴拉

關於腦子一熱

個人經歷告訴我,腦子一熱作的事情,多半會後悔,並且會很是後悔。可是怎麼去避免呢,方法我還沒找到,每次我遇到這樣的情緒,都會找各類理由去逃避,這是目前個人低級應對措施,至關低級。若是能從根源消除是最好不過的了,但是我尚未那麼大的控制力,因此只能慢慢去培養,儘可能減小這種上頭的次數了。

相關文章
相關標籤/搜索