!KissyUI是淘寶Kissy這個前端項目的一個羣,龍藏同窗在看完我在公司內網的「讀爛代碼系列」以後就在羣裏問呵:爛代碼是怎麼定義的?javascript
是呵,到底什麼纔算爛代碼呢?這讓我想到一件事,是另外一個網友在gtalk上問個人一個問題:他須要a,b,c三個條件全真時爲假,全假時也爲假,請問如何判斷。php
接下來KissyUI羣裏的同窗給出了不少答案:前端
en... 確實,我沒有徹底驗證上面的全面答案的有效性。由於如同龍藏後來強調的:「貌似咱們是要討論什麼是爛代碼?」的確,咱們怎麼才能把代碼寫爛呢?上面出現了種種奇異代碼,包括原來提問者的那個取巧的:java
由於這個問題出如今js裏面,存在弱類型的問題,即a、b、c多是整數,或字符串等等,所以(a+b+c)%3這個路子就行不通了,因此纔有了算法
若是把上面的問題改變一下:chrome
- 若是不是a、b、c三個條件,而是兩個以上條件呢?編程
- 若是強調a、b、c自己不必定是布爾值呢?數組
那麼這個問題的基本抽象就是:閉包
接下來,咱們考慮一個問題,既然arguments就是一個數組,那麼能否使用數組方式呢?事實上,聽說在某些js環境中,直接存取arguments[x]的效率是較差的。所以,上面的v1版本能夠有一個改版:架構
這段小小的代碼涉及到splice/slice的使用問題。由於操做的是arguments,所以splice可能致使函數入口的「奇異」變化,在不一樣的引擎中的表現效果並不一致,而slice則又可能致使多出一倍的數據複製。在這裏仍然選用slice()的緣由是:這裏畢竟只是函數參數,不會是「極大量的」數組,所以無需過分考慮存儲問題。
接下來,咱們既然在args中獲得的是一個數組,那麼再用for循環就實在不那麼摩登了。正確的、流行風格的、不被前端鄙視作法是:
爲了向一些不太瞭解js1.6+新特性的同窗解釋v2這個版本,下面的代碼分解了上述這個實現:
some()這個方法會將數組args中的每個元素做爲參數b傳給callback函數。some()有一項特性正是與咱們的原始需求一致的:
- 當callback()返回true的時候,some()會中斷args的列舉而後返回true值;不然,
- 當列舉徹底部元素且callback()未返回true的狀況下,some()返回false值。
如今再讀v2版本的e_xor(),是否是就清晰了?
固然,僅僅出於減小!a運算的必要,v2版本也能夠有以下的一個改版:
在這行代碼裏,使用了連續運算:
而連續運算返回最後一個子表達式的值,即slice()後的數組。這樣的寫法,主要是要將代碼控制在「一個表達式」。
好了,如今咱們開始v3版本的寫法了。爲何呢?由於v2版本仍然不夠酷,v2版本使用的是Array.some(),這個在js1.6中擴展的特既不是那麼的「函數式」,還有些面向對象的痕跡。做爲一個函數式語言的死忠,我認爲,相似於「列舉一個數組」這樣的問題的最正常解法是:遞歸。
爲何呢?由於erlang這樣的純函數式語言就不會搞出個Array.some()的思路來——固然也是有這樣的方法的,只是從「更純正」的角度上講,咱們得本身寫一個。呵呵。這種「純正的遞歸」在js裏面又怎麼搞呢?大概的原型會是這樣子:
在這個框架裏,咱們設e_xor()有無數個參數,但每次咱們只處理a,b兩個,若是a,b相等,則咱們將其中之任一,與後續的n-2個參數遞歸比較。爲了實現「遞歸處理後續n-2個參數」,咱們須要借用函數式語言中的一個重要概念:連續/延續(continuous)。這個東東月影曾經出專題來說過,在這裏:
http://bbs.51js.com/viewthread.php?tid=85325
簡單地說,延續就是對函數參數進行連續的回調。這個東東呢,在較新的函數式語言範式中都是支持的。爲了本文中的這個例子,我單獨地寫個版原本分析之。我稱之爲tail()方法,意思是指定函數參數的尾部,它被設計爲函數Function上的一個原型方法。
注意這個tail()方法的有趣之處:它用到了this.length。在javascript中的函數有兩個length值,一個是foo.length,它代表foo函數在聲明時的形式參數的個數;另外一個是arguments.length,它代表在函數調用時,傳入的實際參數的個數。也就是說,對於函數foo()來講:
第一次調用將顯示[1,2],第二次則會顯示[3,2]。不管如何,聲明時的參數a,b老是兩個,因此foo.length == arguments.callee.length == 2。
回到tail()方法。它的意思是說:
那麼tail()在本例中如何使用呢?
這裏又用到了arguments.callee.length來判斷形式參數個數。也就是說,遞歸的結束條件是:只剩下a,b兩個參數,無需再掃描tail()部分。固然,return中三元表達式(?:)右半部分也會停止遞歸,這種狀況下,是已經找到了一個不相同的條件。
在這個例子中,咱們將e_xor()寫成了一個尾遞歸的函數,這個尾遞歸是函數式的精髓了,只惋惜在js裏面不支持它的優化。WUWU~~ 回頭我查查資源,看看新的chrome v8是否是支持了。v8同窗,尚V5否?:)
從上一個小節中,咱們看到了Guy解決問題的思路。可是在這個級別上,第一步的抽象一般是最關鍵的。簡單地說,V3裏認爲:
這個框架抽象自己多是有問題。正確的理解不是「a,b求異或」,而是「a跟其它元素求異或」。由此,v4的框架抽象是:
在v3中,因爲每次要向後續部分傳入b值,所以咱們須要在tail()中作數組拼接concat()。可是,當咱們使用v4的框架時,b值自己就隱含在後續部分中,所以無需拼接。這樣一來,tail()就有了新的寫法——事實上,這更符合tail()的原意,若是真的存在拼接過程,那它更應由foo()來處理,而不是由tail()來處理。
在v4這個版本中的代碼寫法,會變得更爲簡單:
所謂無階級別,就是你知道他是Guy,但不知道能夠Guy到什麼程度。例如,咱們能夠在v4.1版本的e_xor()中發現一個模式,即:
- 真正的處理邏輯只有第二行。
因爲其它都是框架部分,因此咱們能夠考慮一種編程範式,它是對tail的擴展,目的是對在tail調用e_xor——就好象對數組調用sort()方法同樣。tail的含義是取數據,而新擴展的含義是數組與邏輯都做爲總體。例如:
tailed()的用法很簡單:
簡單的來看,咱們能夠將xor函數做爲tailed()的運算元,這樣同樣,咱們能夠公開一個名爲tailed的公共庫,它的核心就是暴露一組相似於xor的函數,開發者可使用下面的編程範式來實現運算。例如:
那麼,這個所謂的tailed庫該如何用呢?很簡單,一行代碼:
如今咱們獲得了一個半成熟的、名爲tailed的開放庫。所謂半成熟,是由於咱們的tailed()還有一個小小缺陷,下面這行代碼:
中間的f.length+1的這個「1」,是一個有條件的參數,它與xor處理數據的方式有關。簡單的說,正是由於要比較a與arguments[1],所這裏要+1,若是某種算法要比較 多個運算元,則tailed()就不通用了。因此正確的、完善的tailed應該容許調用者指定終止條件。例如:
使用的方法仍然是:
在不一樣的運算中,less_one()能夠是其它的終止條件。
如今,在這個方案——個人意思是tailed library這個庫夠Guy了嗎?不。所謂意淫無止盡,淫人們自有不一樣的淫法。好比,在上面的代碼中咱們能夠看到一個問題,就是tailed()中有不少層次的函數閉包,這意味着調用時效率與存儲空間都存在着無謂的消耗。那麼,有什麼辦法呢?好比說?哈哈,咱們能夠搞搞範型編程,弄個模板出來:
固然,咱們仍然能夠作得更多。例如這個templet引擎至關的粗糙,使用eval()的方法也不如new Function來得理想等等。關於這個部分,能夠再參考QoBean對元語言的處理方式,由於事實上,這後面的部分已經在逼近meta language編程了。
咱們在作什麼?咱們已經離真相愈來愈遠了。或者說,我故意地帶你們兜着一個又一個看似有趣,卻又漸漸遠離真相的圈子。
咱們不是要找一段「不那麼爛的代碼」嗎?若是是這樣,那麼對於a,b,c三個運算條件的判斷,最好的方法大概是:
或者,若是考慮到a,b,c的類型問題:
若是考慮對一組運算元進行判斷的狀況,那麼就把它當成數組,寫成:
對於這段代碼,咱們使用JS默認對arguments的存取規則,有優化就優化,沒有就算了,由於咱們的應用環境並無提出「這裏的arguments有成千上萬個」或「e_xor()調用極爲頻繁」這樣的需求。若是沒有需求,咱們在這方面所作的優化,就是白費功能——除了技術上的完美以外,對應用環境毫無心義。
夠用了。咱們的所學,在應用環境中已經足夠,不要讓技巧在你的代碼中氾濫。所謂技術,是控制代碼複雜性、讓代碼變得優美的一種能力,而不是讓技術自己變得強大或完美。
因此,我此前在「讀爛代碼」系統中討論時,強調的實際上是三個過程:
- 先把業務的需求想清楚,
- 設計好清晰明確的調用接口,
- 用最簡單的、最短距離的代碼實現。
其它神馬滴,都系浮雲。
=====
注:本文從第2小節,至第6小節,僅供對架構、框架、庫等方面有興趣的同窗學習研究,有志於在語言設計、架構抽象等,或基礎項目中使用相關技術的,歡迎探討,切勿濫用於通常應用項目。