第 2 章: 一等公民的函式
快速概覽
當我們說函式是“一等公民”的時候,我們實際上說的是它們和其他物件都一樣...所以就是普通公民(坐經濟艙的人?)。函式真沒什麼特殊的,你可以像對待任何其他資料型別一樣對待它們——把它們存在數組裡,當作引數傳遞,賦值給變數...等等。
這是 JavaScript 語言的基礎概念,不過還是值得提一提的,因為在 Github 上隨便一搜就能看到對這個概念的集體無視,或者也可能是無知。我們來看一個杜撰的例子:
var hi = function(name){
return "Hi " + name;
};
var greeting = function(name) {
return hi(name);
};
這裡 greeting
指向的那個把 hi
包了一層的包裹函式完全是多餘的。為什麼?因為 JavaScript 的函式是可呼叫的,當 hi
後面緊跟 ()
的時候就會執行並返回一個值;如果沒有 ()
,hi
就簡單地返回存到這個變數裡的函式。我們來確認一下:
hi;
// function(name){
// return "Hi " + name
// }
hi("jonas");
// "Hi jonas"
greeting
只不過是轉了個身然後以相同的引數呼叫了 hi
函式而已,因此我們可以這麼寫:
var greeting = hi;
greeting("times");
// "Hi times"
換句話說,hi
已經是個接受一個引數的函數了,為何要再定義一個額外的包裹函式,而它僅僅是用這個相同的引數呼叫 hi
?完全沒有道理。這就像在大夏天裡穿上你最厚的大衣,只是為了跟熱空氣過不去,然後吃上個冰棍。真是脫褲子放屁多此一舉。
用一個函式把另一個函式包起來,目的僅僅是延遲執行,真的是非常糟糕的程式設計習慣。(稍後我將告訴你原因,跟可維護性密切相關。)
充分理解這個問題對讀懂本書後面的內容至關重要,所以我們再來看幾個例子。以下程式碼都來自 npm 上的模組包:
// 太傻了
var getServerStuff = function(callback){
return ajaxCall(function(json){
return callback(json);
});
};
// 這才像樣
var getServerStuff = ajaxCall;
世界上到處都充斥著這樣的垃圾 ajax 程式碼。以下是上述兩種寫法等價的原因:
// 這行
return ajaxCall(function(json){
return callback(json);
});
// 等價於這行
return ajaxCall(callback);
// 那麼,重構下 getServerStuff
var getServerStuff = function(callback){
return ajaxCall(callback);
};
// ...就等於
var getServerStuff = ajaxCall; // <-- 看,沒有括號哦
各位,以上才是寫函式的正確方式。一會兒再告訴你為何我對此如此執著。
var BlogController = (function() {
var index = function(posts) {
return Views.index(posts);
};
var show = function(post) {
return Views.show(post);
};
var create = function(attrs) {
return Db.create(attrs);
};
var update = function(post, attrs) {
return Db.update(post, attrs);
};
var destroy = function(post) {
return Db.destroy(post);
};
return {index: index, show: show, create: create, update: update, destroy: destroy};
})();
這個可笑的控制器(controller)99% 的程式碼都是垃圾。我們可以把它重寫成這樣:
var BlogController = {index: Views.index, show: Views.show, create: Db.create, update: Db.update, destroy: Db.destroy};
...或者直接全部刪掉,因為它的作用僅僅就是把檢視(Views)和資料庫(Db)打包在一起而已。
為何鍾愛一等公民?
好了,現在我們來看看鐘愛一等公民的原因是什麼。前面 getServerStuff
和 BlogController
兩個例子你也都看到了,雖說新增一些沒有實際用處的間接層實現起來很容易,但這樣做除了徒增程式碼量,提高維護和檢索程式碼的成本外,沒有任何用處。
另外,如果一個函式被不必要地包裹起來了,而且發生了改動,那麼包裹它的那個函式也要做相應的變更。
httpGet('/post/2', function(json){
return renderPost(json);
});
如果 httpGet
要改成可以丟擲一個可能出現的 err
異常,那我們還要回過頭去把“膠水”函式也改了。
// 把整個應用裡的所有 httpGet 呼叫都改成這樣,可以傳遞 err 引數。
httpGet('/post/2', function(json, err){
return renderPost(json, err);
});
寫成一等公民函式的形式,要做的改動將會少得多:
httpGet('/post/2', renderPost); // renderPost 將會在 httpGet 中呼叫,想要多少引數都行
除了刪除不必要的函式,正確地為引數命名也必不可少。當然命名不是什麼大問題,但還是有可能存在一些不當的命名,尤其隨著程式碼量的增長以及需求的變更,這種可能性也會增加。
專案中常見的一種造成混淆的原因是,針對同一個概念使用不同的命名。還有通用程式碼的問題。比如,下面這兩個函式做的事情一模一樣,但後一個就顯得更加通用,可重用性也更高:
// 只針對當前的部落格
var validArticles = function(articles) {
return articles.filter(function(article){
return article !== null && article !== undefined;
});
};
// 對未來的專案友好太多
var compact = function(xs) {
return xs.filter(function(x) {
return x !== null && x !== undefined;
});
};
在命名的時候,我們特別容易把自己限定在特定的資料上(本例中是 articles
)。這種現象很常見,也是重複造輪子的一大原因。
有一點我必須得指出,你一定要非常小心 this
值,別讓它反咬你一口,這一點與物件導向程式碼類似。如果一個底層函式使用了 this
,而且是以一等公民的方式被呼叫的,那你就等著 JS 這個蹩腳的抽象概念發怒吧。
var fs = require('fs');
// 太可怕了
fs.readFile('freaky_friday.txt', Db.save);
// 好一點點
fs.readFile('freaky_friday.txt', Db.save.bind(Db));
把 Db 繫結(bind)到它自己身上以後,你就可以隨心所欲地呼叫它的原型鏈式垃圾程式碼了。this
就像一塊髒尿布,我儘可能地避免使用它,因為在函數語言程式設計中根本用不到它。然而,在使用其他的類庫時,你卻不得不向這個瘋狂的世界低頭。
也有人反駁說 this
能提高執行速度。如果你是這種對速度吹毛求疵的人,那你還是合上這本書吧。要是沒法退貨退款,也許你可以去換一本更入門的書來讀。
至此,我們才準備好繼續後面的章節。