《JavaScript 模式》讀書筆記(4)— 函數3

  這篇,咱們來學習下自定義函數以及即時函數的內容。瀏覽器

 

4、自定義函數閉包

  函數能夠動態定義,也能夠分配給變量。若是建立了一個新函數,而且將其分配給保存了另外函數的同一個變量,那麼就以一個新函數覆蓋了舊函數。在某種程度上,回收了舊函數指針以指向一個新函數。而這一切發生在舊函數體的內部。在這種狀況下,該函數以一個新的實現覆蓋並從新定義了自身。函數

var scareMe = function() {
    alert("Boo!");
    scareMe = function() {
        alert("Double Boo!");
    };
};

// 使用自定義函數
scareMe();
scareMe();
scareMe();

  當您的函數有一些初始化準備工做要作,而且僅須要執行一次,那麼這種模式就很是有用。由於並無理由去執行本能夠避免的重複工做,即該函數的一些部分可能並再也不須要。在這種狀況下,自定義函數(self-defining function)能夠更新自身的實現。性能

  這裏多說下,我的以爲,這裏的自定義,並不是單純的指開發者建立的非內置函數,而是指本身定義本身的函數,也就是self-defining。因此,這裏要尤爲注意一下。學習

  使用此模式能夠顯著地幫助您提高應用程序的性能,這是因爲從新定義的函數僅執行了更少的工做。this

  這種函數的另外一個名稱是「惰性函數定義」(lazy function definition),由於該函數直到第一次使用時才被正確的定義,而且其具備後向惰性執行了更少的工做。spa

  該模式的其中一個缺點在於,當它重定義自身時已經添加到原始函數的任何屬性都會丟失。此外,若是該函數使用了不一樣的名稱,好比分配給不一樣的變量或者以對象的方法來使用,那麼重定義部分將永遠不會發生,而且將會執行原始函數體。指針

  下面的例子,咱們將上面的scareMe()函數以第一類對象的使用方式來使用:code

  1. 添加一個新的屬性
  2. 函數對象被分配給一個新的變量。
  3. 該函數也以一個方法的形式使用。
var scareMe = function() {
    alert("Boo!");
    scareMe = function() {
        alert("Double Boo!");
    };
};

// 一、添加一個新的屬性
scareMe.property = "propertly";
// 二、賦值給另外一個不一樣名稱的變量
var prank = scareMe;
// 三、做爲一個方法使用
var spooky = {
    boo:scareMe
};

// calling with a new name
prank(); //輸出「Boo!」
prank(); //輸出「Boo!」
console.log(prank.property); // 輸出「properly」

// 做爲一個方法來調用
spooky.boo(); //輸出「Boo!」
spooky.boo(); //輸出「Boo!」
console.log(spooky.boo.property); //輸出「properly」

// 使用自定義函數
scareMe(); //輸出「Double Boo!」
scareMe(); //輸出「Double Boo!」
console.log(scareMe.property); //輸出undefined

  正如上面代碼所示,當將該函數分配給一個新的變量時,如預期的那樣,函數的自定義(self-definition)並無發生。每次當調用prank()時,它都通知"Boo!"消息,同時它還覆蓋了全局scareMe()函數,可是prank()自身保持了舊函數的可見,其中還包括屬性。當該函數以spooky對象當boo()方法使用時,也發生了一樣的狀況。全部這些調用不斷的重寫全局scareMe()指針,以致於當它最終被調用時,他才第一次具備更新函數主體並通知「Double boo」消息的權利。此外,它也不能訪問scareMe.property屬性。對象

  再多說兩句,我的理解:

// 咱們先來看,爲何上面的代碼訪問不到property屬性。
// 咱們把代碼簡化一下:
var scareMe = function() {
    alert("Boo!");
    scareMe = function() {
        alert("Double Boo!");
    };
};
scareMe.property = "propertly";

console.log(scareMe.property); //輸出「propertly」
scareMe(); //輸出「Boo!」
console.log(scareMe.property); //輸出「undefined」
scareMe(); //輸出「Double Boo!」
console.log(scareMe.property); //輸出undefined

  這是爲何呢?在第一次執行scareMe()方法後,就找不到property屬性了。由於第一次執行後,綁定的是外層變量的指針,此時在綁定屬性的時候,是綁定在這個指針上的。而當函數執行了一次後,內部的scareMe()函數,替換了原來的函數指針。它已經不是曾經的它了!因此property屬性是綁定在外層的,那固然再就找不到了被。

  那麼,因爲它被覆蓋了。而且,之後不會再有新的東西覆蓋掉這個「新函數指針」,因此,之後每次執行都不會執行舊的內容。因此,之後每次的執行都會打印"Double Boo!"。那麼,咱們再看代碼:

// 咱們先來看,爲何上面的代碼訪問不到property屬性。
// 咱們把代碼簡化一下:
var scareMe = function() {
    alert("Boo!");
    scareMe = function() {
        alert("Double Boo!");
        scareMe = function () {
            alert("Third Boo!");
        }
    };
};
scareMe.property = "propertly";

console.log(scareMe.property); //輸出「propertly」
scareMe(); //輸出「Boo!」
console.log(scareMe.property); //輸出「undefined」
scareMe(); //輸出「Double Boo!」
console.log(scareMe.property); //輸出undefined
scareMe(); //輸出「Third Boo!」
scareMe(); //輸出「Third Boo!」
scareMe(); //輸出「Third Boo!」

  咱們來看這段代碼,我自覺得是的又加了一層,因而,我但願不用我說,你也已經懂了。

  最後,再說一下,爲何賦值給一個其它名字的變量以及用對象的方法來使用的時候,重定義永遠沒有發生。我的理解,由於你每次在執行的時候,賦值的動做是有的,可是並無把我覆蓋,因此,每次都是重定義,每次都沒法執行新的內部邏輯。因此,在最開始的那個例子裏,當你第一次調用scareMe()的時候,就走了Double Boo!語句。由於前面prank()或者spooky.boo()的每一次執行,都從新定義了scareMe()。但願我說的,你理解了。

 

5、即時函數

  即時函數模式(Immediate Function pattern)是一種能夠支持在定義函數後當即執行該函數的語法。

(function() {
    alert('watch out!');
}());

  這種模式本質上只是一個函數表達式(不管是命名仍是匿名的),該函數會在建立後馬上執行。在ECMAScript標準中並無定義術語「即時函數(immediate function)」,可是這種模式很是簡潔。

  該模式由一下幾部分組成:

  • 可使用函數表達式定義一個函數(函數聲明是不能夠的)。
  • 在末尾添加一組括號,這將致使該函數當即執行。
  • 將整個函數包裝在括號中(只有不將該函數分配給變量才須要這樣作)。
(function() {
    alert('watch out!');
})();

  這樣的語法也能夠,可是JSLint偏好第一種。

  這種模式是很是有用的,由於它爲初始化代碼提供了一個做用域沙箱。好比:當頁面加載時,代碼必須初始化執行一些設置任務,好比附加事件處理程序、建立對象等諸如此類的任務。全部這些工做僅須要執行一次,所以沒有理由去建立一個可複用的命名函數。可是代碼也還須要一些臨時變量,而在初始化階段完成後就再也不須要這些變量。然而,以全局變量形式建立全部哪些變量是一個差勁的方法。這就是爲何須要一個即時函數的緣由,用以將全部代碼包裝到它的局部做用域中,且不會將任何變量泄露到全局做用域中;

(function () {
    var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
        today = new Date(),
        msg = 'Today is' + days[today.getDay()] + ', ' + today.getDate();
    alert(msg);
}());

  若是上面的代碼沒有包裝在即時函數中,那麼days、today和msg等變量將會成爲全局變量,並遺留在初始化代碼中。

 

即時函數的參數

  也能夠將參數傳遞到即時函數中:

(function (who,when) {
    console.log("I met " + who + " on " + when);
}("Zaking",new Date()));

  通常狀況下,全局對象是以參數方式傳遞給即時函數的,以便於在不使用window指定全局做用域限定的狀況下能夠在函數內部訪問該對象,這樣將使得代碼在瀏覽器環境以外時具備更好的操做性。

(function (global) {
    // 經過global訪問全局變量
}(this));

  請注意,通常來講,不該該傳遞過多的參數到即時函數中,由於這樣將迅速成爲一種閱讀負擔,致使在理解代碼運行流程時須要不斷地滾動到該函數的頂部和底部。

 

即時函數的返回值

  正如任何其餘函數同樣,即時函數能夠返回值,而且這些返回值也能夠分配給變量:

var result = (function() {
    return 2 + 2;
}());

  另外一種方式也能夠達到效果,即忽略包裝函數的括號,由於將即時函數的返回值分配給一個變量時並不須要這些括號:

var result = function() {
    return 2 + 2;
}();

  這個語法雖然比較簡單,可是看起來可能有點使人誤解。在沒有注意到該函數尾部的括號時,一些閱讀代碼的人可能會認爲result變量指向一個函數。實際上,result指向由即時函數返回的值。

  另外一種語法也能夠獲得一樣的結果:

var result = (function() {
    return 2 + 2;
})();

  實際上,即時函數不只能夠返回原始值,還能夠返回任意類型的值,包括另一個函數。所以,可使用即時函數的做用域以存儲一些私有數據,而這特定於返回的內部函數。

var getResult = (function() {
    var res = 2 + 2;
    return function () {
        return res;
    }
}());

  上面這段代碼,即時函數返回的值是一個函數,它將分配給變量getResult,而且將簡單的返回res值,該值被預計算並存儲在即時函數的閉包中。

  當定義對象屬性時也可使用即時函數。想象一下,若是須要定義一個在對象生存期內永遠都不會改變的屬性,可是在定義它以前須要執行一些工做以找出正確的值。此時,可使用一個即時函數包裝這些工做,而且即時函數的返回值將會成爲屬性值。

var o = {
    message:(function () {
        var who = "me",
            what = "call";
            return what + " " + who;
    }()),
    getMsg:function () {
        return this.message;
    }
};
console.log(o.getMsg())
console.log(o.message)

  這個例子中,message是一個字符串屬性,而不是一個函數,可是它須要一個在腳本加載時執行的函數來幫助定義該o.message屬性。

 

優勢和用法

  即時函數模式獲得了普遍的使用。它能夠幫助包裝許多想要執行的工做,且不會在後臺留下任何全局變量。定義的全部這些變量將會是用於自調用函數的局部變量,而且不用擔憂全局空間被臨時變量所污染。

  還可使用即時函數模式來定義模塊(固然ES6中以及由模塊的概念了,可是這樣的方法仍舊有學習的地方):

// 文件module1.js中定義的模塊module1
(function() {
    //模塊1的全部代碼
}());

  使用這種方式,能夠編寫其餘模塊。而後,將該代碼發佈到在線站點時,能夠決定哪些功能準備應用於黃金時間,而且使用構建腳本將對應文件合併。

 

  這篇文章就到這裏了。後面還有...

相關文章
相關標籤/搜索