前端要給力之:代碼能夠有多爛?

一、爛代碼是怎麼定義的?

!KissyUI是淘寶Kissy這個前端項目的一個羣,龍藏同窗在看完我在公司內網的「讀爛代碼系列」以後就在羣裏問呵:爛代碼是怎麼定義的?javascript

是呵,到底什麼纔算爛代碼呢?這讓我想到一件事,是另外一個網友在gtalk上問個人一個問題:他須要a,b,c三個條件全真時爲假,全假時也爲假,請問如何判斷。php

接下來KissyUI羣裏的同窗給出了不少答案:前端

[javascript]  view plaincopy
  1. // 1. 圓心  
  2. if( a&&b&&c || !a&&!b&&!c){  
  3.     return false  
  4. }  
  5. // 2. 龍藏  
  6. (a ^ b) & c  
  7. // 3. 愚公(我給gtalk上的提問者)的答案  
  8. (a xor b) or (a xor c)  
  9. // 4. 提問者本身的想法  
  10. (a + b + c) % 3  
  11. // 5. 雲謙對答案4的改進版本  
  12. (!!a+!!b+!!c)%n  
  13. // 6. 拔赤  
  14. a ? (b?c:b) : (b?!b:!c)  
  15. // 7. 吳英傑  
  16. (a != b || b != c)  
  17. 或  
  18. (!a != !b || !b != !c)  
  19. // 8. 姬光  
  20. var v = a&&b&&c;  
  21. if(!v){  
  22.    return false;  
  23. }else if(v){  
  24.    return false;  
  25. }else{  
  26.    return true;  
  27. }  

en... 確實,我沒有徹底驗證上面的全面答案的有效性。由於如同龍藏後來強調的:「貌似咱們是要討論什麼是爛代碼?」的確,咱們怎麼才能把代碼寫爛呢?上面出現了種種奇異代碼,包括原來提問者的那個取巧的:java

 

[javascript]  view plaincopy
  1. // 4. 提問者本身的想法  
  2. (a + b + c) % 3  

由於這個問題出如今js裏面,存在弱類型的問題,即a、b、c多是整數,或字符串等等,所以(a+b+c)%3這個路子就行不通了,因此纔有了算法

 

[javascript]  view plaincopy
  1. // 5. 雲謙對答案4的改進版本  
  2. (!!a+!!b+!!c)%n  

 

 

  二、問題的泛化與求解:普通級別

若是把上面的問題改變一下:chrome

 - 若是不是a、b、c三個條件,而是兩個以上條件呢?編程

 - 若是強調a、b、c自己不必定是布爾值呢?數組

那麼這個問題的基本抽象就是:閉包

[c-sharp]  view plaincopy
  1. // v0,對任意多個運算元求xor  
  2. function e_xor() { ... }  
  3.   
  4. 對於這個e_xor()來講,最直接的代碼寫法是:  
  5. // v1,掃描全部參數,發現不一樣的即返回true,所有相同則返回false。  
  6. function e_xor() {  
  7.   var args=arguments, argn=args.length;  
  8.   args[0] = !args[0];  
  9.   for (var i=1; i<argn; i++) {  
  10.     if (args[0] != !args[i]) return true;  
  11.   }  
  12.   return false;  
  13. }  

 

接下來,咱們考慮一個問題,既然arguments就是一個數組,那麼能否使用數組方式呢?事實上,聽說在某些js環境中,直接存取arguments[x]的效率是較差的。所以,上面的v1版本能夠有一個改版:架構

[javascript]  view plaincopy
  1. // v1.1,對v1的改版  
  2. function e_xor() {  
  3.   var args=[].slice.call(arguments,0), argn=args.length;  
  4.   ...  
  5. }  

這段小小的代碼涉及到splice/slice的使用問題。由於操做的是arguments,所以splice可能致使函數入口的「奇異」變化,在不一樣的引擎中的表現效果並不一致,而slice則又可能致使多出一倍的數據複製。在這裏仍然選用slice()的緣由是:這裏畢竟只是函數參數,不會是「極大量的」數組,所以無需過分考慮存儲問題。

 

 

三、問題的泛化與求解:專業級別

接下來,咱們既然在args中獲得的是一個數組,那麼再用for循環就實在不那麼摩登了。正確的、流行風格的、不被前端鄙視作法是:

[javascript]  view plaincopy
  1. // v2,使用js1.6+的數組方法的實現  
  2. function e_xor(a) {  
  3.   return ([].slice.call(arguments,1)).some(function(b) { if (!b != !a) return true });  
  4. }  

爲了向一些不太瞭解js1.6+新特性的同窗解釋v2這個版本,下面的代碼分解了上述這個實現:

 

[javascript]  view plaincopy
  1. // v2.1,對v2的詳細分解  
  2. function e_xor(a) {  
  3.   var args = [].slice.call(arguments,1);  
  4.   var callback = function(b) {  
  5.     if (!b != !a) return true  
  6.   }  
  7.   return args.some(callback);  
  8. }  

some()這個方法會將數組args中的每個元素做爲參數b傳給callback函數。some()有一項特性正是與咱們的原始需求一致的:

 

  - 當callback()返回true的時候,some()會中斷args的列舉而後返回true值;不然,

  - 當列舉徹底部元素且callback()未返回true的狀況下,some()返回false值。

如今再讀v2版本的e_xor(),是否是就清晰了?

 

固然,僅僅出於減小!a運算的必要,v2版本也能夠有以下的一個改版:

[javascript]  view plaincopy
  1. // v2.2,對v2的優化以減小!a運算次數  
  2. function e_xor(a) {  
  3.   return (a=!a, [].slice.call(arguments,1)).some(function(b) { if (!b != a) return true });  
  4. }  

在這行代碼裏,使用了連續運算:

 

[javascript]  view plaincopy
  1. (a=!a, [].slice.call(arguments,1))  

 

而連續運算返回最後一個子表達式的值,即slice()後的數組。這樣的寫法,主要是要將代碼控制在「一個表達式」。

 

 

四、問題的泛化與求解:Guy入門級別

好了,如今咱們開始v3版本的寫法了。爲何呢?由於v2版本仍然不夠酷,v2版本使用的是Array.some(),這個在js1.6中擴展的特既不是那麼的「函數式」,還有些面向對象的痕跡。做爲一個函數式語言的死忠,我認爲,相似於「列舉一個數組」這樣的問題的最正常解法是:遞歸。

爲何呢?由於erlang這樣的純函數式語言就不會搞出個Array.some()的思路來——固然也是有這樣的方法的,只是從「更純正」的角度上講,咱們得本身寫一個。呵呵。這種「純正的遞歸」在js裏面又怎麼搞呢?大概的原型會是這樣子:

[javascript]  view plaincopy
  1. // v3,採用純函數式的、遞歸方案的框架  
  2. function e_xor(a, b) {  ... }  

在這個框架裏,咱們設e_xor()有無數個參數,但每次咱們只處理a,b兩個,若是a,b相等,則咱們將其中之任一,與後續的n-2個參數遞歸比較。爲了實現「遞歸處理後續n-2個參數」,咱們須要借用函數式語言中的一個重要概念:連續/延續(continuous)。這個東東月影曾經出專題來說過,在這裏:

 

http://bbs.51js.com/viewthread.php?tid=85325

簡單地說,延續就是對函數參數進行連續的回調。這個東東呢,在較新的函數式語言範式中都是支持的。爲了本文中的這個例子,我單獨地寫個版原本分析之。我稱之爲tail()方法,意思是指定函數參數的尾部,它被設計爲函數Function上的一個原型方法。

[javascript]  view plaincopy
  1. Function.prototype.tail = function() {  
  2.   return this.apply(this, [].slice.call(arguments,0).concat([].slice.call(this.arguments, this.length)));  
  3. }  

注意這個tail()方法的有趣之處:它用到了this.length。在javascript中的函數有兩個length值,一個是foo.length,它代表foo函數在聲明時的形式參數的個數;另外一個是arguments.length,它代表在函數調用時,傳入的實際參數的個數。也就是說,對於函數foo()來講:

 

[javascript]  view plaincopy
  1. function foo(a, b) {  
  2.   alert([arguments.length, arguments.callee.length]);  
  3. }  
  4. foo(x);  
  5. foo(x,y,z);  

第一次調用將顯示[1,2],第二次則會顯示[3,2]。不管如何,聲明時的參數a,b老是兩個,因此foo.length == arguments.callee.length == 2。

 

回到tail()方法。它的意思是說:

[javascript]  view plaincopy
  1. Function.prototype.tail = function() {  
  2.   return this.apply( // 從新調用函數自身  
  3.     this, // 以函數foo自身做爲this Object  
  4.     [].slice.call(arguments,0) // 取調用tail時的所有參數,轉換爲數組  
  5.     .concat( // 數組鏈接  
  6.       [].slice.call(this.arguments, // 取本次函數foo調用時的參數,因爲tail()總在foo()中調用,所以實際是取最近一次foo()的實際參數  
  7.         this.length)  // 按照foo()聲明時的形式參數個數,截取foo()函數參數的尾部  
  8.     )  
  9.   );  
  10. }  

那麼tail()在本例中如何使用呢?

 

[javascript]  view plaincopy
  1. // v3.1,使用tail()的版本  
  2. function e_xor(a, b) {  
  3.   if (arguments.length == arguments.callee.length) return !a != !b;  
  4.   return (!a == !b ? arguments.callee.tail(b) : true);  
  5. }  

這裏又用到了arguments.callee.length來判斷形式參數個數。也就是說,遞歸的結束條件是:只剩下a,b兩個參數,無需再掃描tail()部分。固然,return中三元表達式(?:)右半部分也會停止遞歸,這種狀況下,是已經找到了一個不相同的條件。

 

在這個例子中,咱們將e_xor()寫成了一個尾遞歸的函數,這個尾遞歸是函數式的精髓了,只惋惜在js裏面不支持它的優化。WUWU~~ 回頭我查查資源,看看新的chrome v8是否是支持了。v8同窗,尚V5否?:)

 

五、問題的泛化與求解:Guy進階級別

從上一個小節中,咱們看到了Guy解決問題的思路。可是在這個級別上,第一步的抽象一般是最關鍵的。簡單地說,V3裏認爲:

[javascript]  view plaincopy
  1. // v3,採用純函數式的、遞歸方案的框架  
  2. function e_xor(a, b) {  ... }  

這個框架抽象自己多是有問題。正確的理解不是「a,b求異或」,而是「a跟其它元素求異或」。由此,v4的框架抽象是:

 

[javascript]  view plaincopy
  1. // v4,更優的函數式框架抽象,對接口的思考  
  2. function e_xor(a) {  ... }  

在v3中,因爲每次要向後續部分傳入b值,所以咱們須要在tail()中作數組拼接concat()。可是,當咱們使用v4的框架時,b值自己就隱含在後續部分中,所以無需拼接。這樣一來,tail()就有了新的寫法——事實上,這更符合tail()的原意,若是真的存在拼接過程,那它更應由foo()來處理,而不是由tail()來處理。

 

[javascript]  view plaincopy
  1. // 更符合原始抽象含義的tail方法  
  2. Function.prototype.tail = function() {  
  3.   return this.apply(this, [].slice.call(this.arguments, this.length));  
  4. }  

在v4這個版本中的代碼寫法,會變得更爲簡單:

 

[javascript]  view plaincopy
  1. // v4.1,相較於v3更爲簡單的實現  
  2. function e_xor(a) {  
  3.   if (arguments.length < 2) return false;  
  4.   return (!a == !arguments[1] ? arguments.callee.tail() : true);  
  5. }  
  6. // v4.1.1,一個不使用三元表達式的簡潔版本  
  7. function e_xor(a) {  
  8.   if (arguments.length < 2) return false;  
  9.   if (!arguments[1] != !a) return true;  
  10.   return arguments.callee.tail();  
  11. }  

 

 

六、問題的泛化與求解:Guy無階級別

所謂無階級別,就是你知道他是Guy,但不知道能夠Guy到什麼程度。例如,咱們能夠在v4.1版本的e_xor()中發現一個模式,即:

  - 真正的處理邏輯只有第二行。

因爲其它都是框架部分,因此咱們能夠考慮一種編程範式,它是對tail的擴展,目的是對在tail調用e_xor——就好象對數組調用sort()方法同樣。tail的含義是取數據,而新擴展的含義是數組與邏輯都做爲總體。例如:

[javascript]  view plaincopy
  1. // 在函數原型上擴展的tailed方法,用於做參數的尾部化處理  
  2. Function.prototype.tailed = function() {  
  3.   return function(f) {  // 將函數this經過參數f保留在閉包上  
  4.     return function() {  // tailed()以後的、可調用的e_xor()函數  
  5.       if (arguments.length < f.length+1) return false;  
  6.       if (f.apply(this, arguments)) return true;  // 調用tailed()以前的函數f  
  7.       return arguments.callee.apply(this, [].slice.call(arguments, f.length));  
  8.     }  
  9.   }(this)  
  10. }  

 

tailed()的用法很簡單:

[javascript]  view plaincopy
  1. e_xor = function(a){  
  2.   if (!arguments[1] != !a) return true;  
  3. }.tailed();  

 

簡單的來看,咱們能夠將xor函數做爲tailed()的運算元,這樣同樣,咱們能夠公開一個名爲tailed的公共庫,它的核心就是暴露一組相似於xor的函數,開發者可使用下面的編程範式來實現運算。例如:

[javascript]  view plaincopy
  1. /* tiny tailed library, v0.0.0.1 alpha. by aimingoo. */  
  2. Function.prototype.tailed = ....;  
  3. // 對參數a及其後的全部參數求異或  
  4. function xor(a) {  
  5.   if (!arguments[1] != !a) return true;  
  6. }  
  7. // ...更多相似的庫函數  

那麼,這個所謂的tailed庫該如何用呢?很簡單,一行代碼:

 

[javascript]  view plaincopy
  1. // 求任意多個參數的xor值  
  2. xor.tailed()(a,b,c,d,e,f,g);  

 

 

如今咱們獲得了一個半成熟的、名爲tailed的開放庫。所謂半成熟,是由於咱們的tailed()還有一個小小缺陷,下面這行代碼:

[javascript]  view plaincopy
  1. if (arguments.length < f.length+1) return false;  

 

 

中間的f.length+1的這個「1」,是一個有條件的參數,它與xor處理數據的方式有關。簡單的說,正是由於要比較a與arguments[1],所這裏要+1,若是某種算法要比較 多個運算元,則tailed()就不通用了。因此正確的、完善的tailed應該容許調用者指定終止條件。例如:

[javascript]  view plaincopy
  1. // less_one()做爲tailed庫函數中的全局常量,以及缺省的closed條件  
  2. // 當less_one返回true時,代表遞歸應該終止  
  3. function less_one(args, f)  {  
  4.   if (args.length < f.length+1) return true;  
  5. }  
  6. // 在函數原型上擴展的tailed方法,用於做參數的尾部化處理  
  7. Function.prototype.tailed = function(closed) {  
  8.   return function(f) {  // 將函數this經過參數f保留在閉包上  
  9.     return function() {  // tailed()以後的、可調用的e_xor()函數  
  10.       if ((closed||less_one).apply(this, [arguments,f])) return false;  
  11.       if (f.apply(this, arguments)) return true;  // 調用tailed()以前的函數f  
  12.       return arguments.callee.apply(this, [].slice.call(arguments, f.length));  
  13.     }  
  14.   }(this)  
  15. }  

 

使用的方法仍然是:

[javascript]  view plaincopy
  1. xor.tailed()(a,b,c,d,e,f,g);  
  2. // 或者  
  3. xor.tailed(less_one)(a,b,c,d,e,f,g);  

 

 

在不一樣的運算中,less_one()能夠是其它的終止條件。

 

如今,在這個方案——個人意思是tailed library這個庫夠Guy了嗎?不。所謂意淫無止盡,淫人們自有不一樣的淫法。好比,在上面的代碼中咱們能夠看到一個問題,就是tailed()中有不少層次的函數閉包,這意味着調用時效率與存儲空間都存在着無謂的消耗。那麼,有什麼辦法呢?好比說?哈哈,咱們能夠搞搞範型編程,弄個模板出來:

[javascript]  view plaincopy
  1. /* tiny tailed library with templet framework, v0.0.0.1 beta. by aimingoo. */  
  2. Function.prototype.templeted = function(args) {  
  3.   var buff = ['[', ,'][0]'];  
  4.   buff[1] = this.toString().replace(/_([^_]*)_/g, function($0,$1) { return args[$1]||'_'});  
  5.   return eval(buff.join(''));  
  6. }  
  7. function tailed() {  
  8.   var f = _execute_;  
  9.   if (_closed_(arguments, f)) return false;  
  10.   if (f.apply(this, arguments)) return true;  
  11.   return arguments.callee.apply(this, [].slice.call(arguments, f.length));  
  12. }  
  13. function less_one(args, f)  {  
  14.   if (args.length < f.length+1) return true;  
  15. }  
  16. function xor(a) {  
  17.   if (!arguments[1] != !a) return true;  
  18. }  
  19. e_xor = tailed.templeted({  
  20.   closed: less_one,  
  21.   execute: xor  
  22. })  

固然,咱們仍然能夠作得更多。例如這個templet引擎至關的粗糙,使用eval()的方法也不如new Function來得理想等等。關於這個部分,能夠再參考QoBean對元語言的處理方式,由於事實上,這後面的部分已經在逼近meta language編程了。

 

 

七、Guy?

咱們在作什麼?咱們已經離真相愈來愈遠了。或者說,我故意地帶你們兜着一個又一個看似有趣,卻又漸漸遠離真相的圈子。

咱們不是要找一段「不那麼爛的代碼」嗎?若是是這樣,那麼對於a,b,c三個運算條件的判斷,最好的方法大概是:

[javascript]  view plaincopy
  1. (a!=b || a!=c)  

 

或者,若是考慮到a,b,c的類型問題:

[javascript]  view plaincopy
  1. (!a!=!b || !a!=!c)  

 

若是考慮對一組運算元進行判斷的狀況,那麼就把它當成數組,寫成:

[javascript]  view plaincopy
  1. function e_xor(a) {  
  2.   for (var na=!a,i=1; i<arguments.length; i++) {  
  3.     if (!arguments[i] != na) return true  
  4.   }  
  5.   return false;  
  6. }  

 

對於這段代碼,咱們使用JS默認對arguments的存取規則,有優化就優化,沒有就算了,由於咱們的應用環境並無提出「這裏的arguments有成千上萬個」或「e_xor()調用極爲頻繁」這樣的需求。若是沒有需求,咱們在這方面所作的優化,就是白費功能——除了技術上的完美以外,對應用環境毫無心義。

 

夠用了。咱們的所學,在應用環境中已經足夠,不要讓技巧在你的代碼中氾濫。所謂技術,是控制代碼複雜性、讓代碼變得優美的一種能力,而不是讓技術自己變得強大或完美。

 

因此,我此前在「讀爛代碼」系統中討論時,強調的實際上是三個過程:

 - 先把業務的需求想清楚,

 - 設計好清晰明確的調用接口,

 - 用最簡單的、最短距離的代碼實現。

 

其它神馬滴,都系浮雲。

 

=====

注:本文從第2小節,至第6小節,僅供對架構、框架、庫等方面有興趣的同窗學習研究,有志於在語言設計、架構抽象等,或基礎項目中使用相關技術的,歡迎探討,切勿濫用於通常應用項目。

相關文章
相關標籤/搜索