前端要給力之:代碼可以有多爛?
1、爛代碼是怎么定義的?
!KissyUI是淘寶Kissy這個前端項目的一個群,龍藏同學在看完我在公司內網的“讀爛代碼系列”之后就在群里問呵:爛代碼是怎么定義的?
是呵,到底什么才算爛代碼呢?這讓我想到一件事,是另一個網友在gtalk上問我的一個問題:他需要a,b,c三個條件全真時為假,全假時也為假,請問如何判斷。
接下來KissyUI群里的同學給出了很多答案:
2. return false
3. }
4. // 2. 龍藏
5. (a ^ b) & c
6. // 3. 愚公(我給gtalk上的提問者)的答案
7. (a xor b) or (a xor c)
8. // 4. 提問者自己的想法
9. (a + b + c) % 3
10. // 5. 云謙對答案4的改進版本
11. (!!a+!!b+!!c)%n
12. // 6. 拔赤
13. a ? (b?c:b) : (b?!b:!c)
14. // 7. 吳英杰
15. (a != b || b != c)
16. 或
17. (!a != !b || !b != !c)
18. // 8. 姬光
19. var v = a&&b&&c;
20. if(!v){
21. return false;
22. }else if(v){
23. return false;
24. }else{
25. return true;
26. }
確實,我沒有完全驗證上面的全面答案的有效性。因為如同龍藏后來強調的:“貌似我們是要討論什么是爛代碼?”的確,我們怎么才能把代碼寫爛呢?上面出現了種種奇異代碼,包括原來提問者的那個取巧的:
2. (a + b + c) % 3
因為這個問題出現在js里面,存在弱類型的問題,即a、b、c可能是整數,或字符串等等,因此(a+b+c)%3這個路子就行不通了,所以才有了:
2. (!!a+!!b+!!c)%n
2、問題的泛化與求解:普通級別
如果把上面的問題改變一下:
- 如果不是a、b、c三個條件,而是兩個以上條件呢?
- 如果強調a、b、c本身不一定是布爾值呢?
那么這個問題的基本抽象就是:
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版本可以有一個改版:
2. function e_xor() {
3. var args=[].slice.call(arguments,0), argn=args.length;
4. ...
5. }
這段小小的代碼涉及到splice/slice的使用問題。因為操作的是arguments,因此splice可能導致函數入口的“奇異”變化,在不同的引擎中的表現效果并不一致,而slice則又可能導致多出一倍的數據復制。在這里仍然選用slice()的原因是:這里畢竟只是函數參數,不會是“極大量的”數組,因此無需過度考慮存儲問題。
3、問題的泛化與求解:專業級別
接下來,我們既然在args中得到的是一個數組,那么再用for循環就實在不那么摩登了。正確的、流行風格的、不被前端鄙視做法是:
2. function e_xor(a) {
3. return ([].slice.call(arguments,1)).some(function(b) { if (!b != !a) return true });
4. }
為了向一些不太了解js1.6+新特性的同學解釋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版本也可以有如下的一個改版:
2. function e_xor(a) {
3. return (a=!a, [].slice.call(arguments,1)).some(function(b) { if (!b != a) return true });
4. }
在這行代碼里,使用了連續運算:
而連續運算返回最后一個子表達式的值,即slice()后的數組。這樣的寫法,主要是要將代碼控制在“一個表達式”。
4、問題的泛化與求解:Guy入門級別
好了,現在我們開始v3版本的寫法了。為什么呢?因為v2版本仍然不夠酷,v2版本使用的是Array.some(),這個在js1.6中擴展的特既不是那么的“函數式”,還有些面向對象的痕跡。作為一個函數式語言的死忠,我認為,類似于“列舉一個數組”這樣的問題的最正常解法是:遞歸。
為什么呢?因為erlang這樣的純函數式語言就不會搞出個Array.some()的思路來——當然也是有這樣的方法的,只是從“更純正”的角度上講,我們得自己寫一個。呵呵。這種“純正的遞歸”在js里面又怎么搞呢?大概的原型會是這樣子:
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上的一個原型方法。
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()來說:
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()方法。它的意思是說:
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()在本例中如何使用呢?
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否?:)
5、問題的泛化與求解:Guy進階級別
從上一個小節中,我們看到了Guy解決問題的思路。但是在這個級別上,第一步的抽象通常是最關鍵的。簡單地說,V3里認為:
2. function e_xor(a, b) { ... }
這個框架抽象本身可能是有問題。正確的理解不是“a,b求異或”,而是“a跟其它元素求異或”。由此,v4的框架抽象是:
2. function e_xor(a) { ... }
在v3中,由于每次要向后續部分傳入b值,因此我們需要在tail()中做數組拼接concat()。但是,當我們使用v4的框架時,b值本身就隱含在后續部分中,因此無需拼接。這樣一來,tail()就有了新的寫法——事實上,這更符合tail()的原意,如果真的存在拼接過程,那它更應由foo()來處理,而不是由tail()來處理。
2. Function.prototype.tail = function() {
3. return this.apply(this, [].slice.call(this.arguments, this.length));
4. }
在v4這個版本中的代碼寫法,會變得更為簡單:
# function e_xor(a) {
# if (arguments.length < 2) return false;
# return (!a == !arguments[1] ? arguments.callee.tail() : true);
# }
# // v4.1.1,一個不使用三元表達式的簡潔版本
# function e_xor(a) {
# if (arguments.length < 2) return false;
# if (!arguments[1] != !a) return true;
# return arguments.callee.tail();
# }
6、問題的泛化與求解:Guy無階級別
所謂無階級別,就是你知道他是Guy,但不知道可以Guy到什么程度。例如,我們可以在v4.1版本的e_xor()中發現一個模式,即:
- 真正的處理邏輯只有第二行。
由于其它都是框架部分,所以我們可以考慮一種編程范式,它是對tail的擴展,目的是對在tail調用e_xor——就好象對數組調用sort()方法一樣。tail的含義是取數據,而新擴展的含義是數組與邏輯都作為整體。例如:
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. }
if (!arguments[1] != !a) return true;
}.tailed();
簡單的來看,我們可以將xor函數作為tailed()的運算元,這樣一樣,我們可以公開一個名為tailed的公共庫,它的核心就是暴露一組類似于xor的函數,開發者可以使用下面的編程范式來實現運算。例如:
Function.prototype.tailed = ....;
// 對參數a及其后的所有參數求異或
function xor(a) {
if (!arguments[1] != !a) return true;
}
// ...更多類似的庫函數
那么,這個所謂的tailed庫該如何用呢?很簡單,一行代碼:
xor.tailed()(a,b,c,d,e,f,g);
現在我們得到了一個半成熟的、名為tailed的開放庫。所謂半成熟,是因為我們的tailed()還有一個小小缺陷,下面這行代碼:
中間的f.length+1的這個“1”,是一個有條件的參數,它與xor處理數據的方式有關。簡單的說,正是因為要比較a與arguments[1],所這里要+1,如果某種算法要比較 多個運算元,則tailed()就不通用了。所以正確的、完善的tailed應該允許調用者指定終止條件。例如:
# // 當less_one返回true時,表明遞歸應該終止
# function less_one(args, f) {
# if (args.length < f.length+1) return true;
# }
# // 在函數原型上擴展的tailed方法,用于作參數的尾部化處理
# Function.prototype.tailed = function(closed) {
# return function(f) { // 將函數this通過參數f保留在閉包上
# return function() { // tailed()之后的、可調用的e_xor()函數
# if ((closed||less_one).apply(this, [arguments,f])) return false;
# if (f.apply(this, arguments)) return true; // 調用tailed()之前的函數 f
# return arguments.callee.apply(this, [].slice.call(arguments, f.length));
# }
# }(this)
# }
使用的方法仍然是:
// 或者
xor.tailed(less_one)(a,b,c,d,e,f,g);
在不同的運算中,less_one()可以是其它的終止條件。
現在,在這個方案——我的意思是tailed library這個庫夠Guy了嗎?不。所謂意淫無止盡,淫人們自有不同的淫法。比如,在上面的代碼中我們可以看到一個問題,就是tailed()中有很多層次的函數閉包,這意味著調用時效率與存儲空間都存在著無謂的消耗。那么,有什么辦法呢?比如說?哈哈,我們可以搞搞范型編程,弄個模板出來:
# Function.prototype.templeted = function(args) {
# var buff = ['[', ,'][0]'];
# buff[1] = this.toString().replace(/_([^_]*)_/g, function($0,$1) { return args[$1]||'_'});
# return eval(buff.join(''));
# }
# function tailed() {
# var f = _execute_;
# if (_closed_(arguments, f)) return false;
# if (f.apply(this, arguments)) return true;
# return arguments.callee.apply(this, [].slice.call(arguments, f.length));
# }
# function less_one(args, f) {
# if (args.length < f.length+1) return true;
# }
# function xor(a) {
# if (!arguments[1] != !a) return true;
# }
# e_xor = tailed.templeted({
# closed: less_one,
# execute: xor
# })
當然,我們仍然可以做得更多。例如這個templet引擎相當的粗糙,使用eval()的方法也不如new Function來得理想等等。關于這個部分,可以再參考QoBean對元語言的處理方式,因為事實上,這后面的部分已經在逼近meta language編程了。
7、Guy?
我們在做什么?我們已經離真相越來越遠了。或者說,我故意地帶大家兜著一個又一個看似有趣,卻又漸漸遠離真相的圈子。
我們不是要找一段“不那么爛的代碼”嗎?如果是這樣,那么對于a,b,c三個運算條件的判斷,最好的方法大概是:
或者,如果考慮到a,b,c的類型問題:
如果考慮對一組運算元進行判斷的情況,那么就把它當成數組,寫成:
for (var na=!a,i=1; i<arguments.length; i++) {
if (!arguments[i] != na) return true
}
return false;
}
對于這段代碼,我們使用JS默認對arguments的存取規則,有優化就優化,沒有就算了,因為我們的應用環境并沒有提出“這里的arguments有成千上萬個”或“e_xor()調用極為頻繁”這樣的需求。如果沒有需求,我們在這方面所做的優化,就是白費功能——除了技術上的完美之外,對應用環境毫無意義。
夠用了。我們的所學,在應用環境中已經足夠,不要讓技巧在你的代碼中泛濫。所謂技術,是控制代碼復雜性、讓代碼變得優美的一種能力,而不是讓技術本身變得強大或完美。
所以,我此前在“讀爛代碼”系統中討論時,強調的其實是三個過程:
- 先把業務的需求想清楚,
- 設計好清晰明確的調用接口,
- 用最簡單的、最短距離的代碼實現。
其它神馬滴,都系浮云。
注:本文從第2小節,至第6小節,僅供對架構、框架、庫等方面有興趣的同學學習研究,有志于在語言設計、架構抽象等,或基礎項目中使用相關技術的,歡迎探討,切勿濫用于一般應用項目。