《JavaScript 模式》讀書筆記(4)— 函數3
- 2020 年 3 月 28 日
- 筆記
這篇,我們來學習下自定義函數以及即時函數的內容。
四、自定義函數
函數可以動態定義,也可以分配給變量。如果創建了一個新函數,並且將其分配給保存了另外函數的同一個變量,那麼就以一個新函數覆蓋了舊函數。在某種程度上,回收了舊函數指針以指向一個新函數。而這一切發生在舊函數體的內部。在這種情況下,該函數以一個新的實現覆蓋並重新定義了自身。
var scareMe = function() { alert("Boo!"); scareMe = function() { alert("Double Boo!"); }; }; // 使用自定義函數 scareMe(); scareMe(); scareMe();
當您的函數有一些初始化準備工作要做,並且僅需要執行一次,那麼這種模式就非常有用。因為並沒有理由去執行本可以避免的重複工作,即該函數的一些部分可能並不再需要。在這種情況下,自定義函數(self-defining function)可以更新自身的實現。
這裡多說下,個人覺得,這裡的自定義,並非單純的指開發者創建的非內置函數,而是指自己定義自己的函數,也就是self-defining。所以,這裡要尤其注意一下。
使用此模式可以顯着地幫助您提升應用程序的性能,這是由於重新定義的函數僅執行了更少的工作。
這種函數的另一個名稱是「惰性函數定義」(lazy function definition),因為該函數直到第一次使用時才被正確的定義,並且其具有後向惰性執行了更少的工作。
該模式的其中一個缺點在於,當它重定義自身時已經添加到原始函數的任何屬性都會丟失。此外,如果該函數使用了不同的名稱,比如分配給不同的變量或者以對象的方法來使用,那麼重定義部分將永遠不會發生,並且將會執行原始函數體。
下面的例子,我們將上面的scareMe()函數以第一類對象的使用方式來使用:
- 添加一個新的屬性
- 函數對象被分配給一個新的變量。
- 該函數也以一個方法的形式使用。
var scareMe = function() { alert("Boo!"); scareMe = function() { alert("Double Boo!"); }; }; // 1、添加一個新的屬性 scareMe.property = "propertly"; // 2、賦值給另一個不同名稱的變量 var prank = scareMe; // 3、作為一個方法使用 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()。希望我說的,你理解了。
五、即時函數
即時函數模式(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的所有代碼 }());
使用這種方式,可以編寫其他模塊。然後,將該代碼發佈到在線站點時,可以決定哪些功能準備應用於黃金時間,並且使用構建腳本將對應文件合併。
這篇文章就到這裡了。後面還有…