前端要给力之:代码可以有多烂?
1、烂代码是怎么定义的?
!KissyUI是淘宝Kissy这个前端项目的一个群,龙藏同学在看完我在公司内网的“读烂代码系列”之后就在群里问呵:烂代码是怎么定义的?是呵,到底什么才算烂代码呢?这让我想到一件事,是另一个网友在gtalk上问我的一个问题:他需要a,b,c三个条件全真时为假,全假时也为假,请问如何判断。
接下来KissyUI群里的同学给出了很多答案:
// 1. 圆心
if( a&&b&&c || !a&&!b&&!c){
return false
}
// 2. 龙藏
(a ^ b) & c
// 3. 愚公(我给gtalk上的提问者)的答案
(a xor b) or (a xor c)
// 4. 提问者自己的想法
(a + b + c) % 3
// 5. 云谦对答案4的改进版本
(!!a+!!b+!!c)%n
// 6. 拔赤
a ? (b?c:b) : (b?!b:!c)
// 7. 吴英杰
(a != b || b != c)
或
(!a != !b || !b != !c)
// 8. 姬光
var v = a&&b&&c;
if(!v){
return false;
}else if(v){
return false;
}else{
return true;
}
en... 确实,我没有完全验证上面的全面答案的有效性。因为如同龙藏后来强调的:“貌似我们是要讨论什么是烂代码?”的确,我们怎么才能把代码写烂呢?上面出现了种种奇异代码,包括原来提问者的那个取巧的:// 4. 提问者自己的想法
(a + b + c) % 3因为这个问题出现在js里面,存在弱类型的问题,即a、b、c可能是整数,或字符串等等,因此(a+b+c)%3这个路子就行不通了,所以才有了
// 5. 云谦对答案4的改进版本
(!!a+!!b+!!c)%n
2、问题的泛化与求解:普通级别
如果把上面的问题改变一下:- 如果不是a、b、c三个条件,而是两个以上条件呢?
- 如果强调a、b、c本身不一定是布尔值呢?
那么这个问题的基本抽象就是:
// v0,对任意多个运算元求xor
function e_xor() { ... }对于这个e_xor()来说,最直接的代码写法是:
// v1,扫描所有参数,发现不同的即返回true,全部相同则返回false。
function e_xor() {
var args=arguments, argn=args.length;
args[0] = !args[0];
for (var i=1; i<argn; i++) {
if (args[0] != !args[i]) return true;
}
return false;
}
接下来,我们考虑一个问题,既然arguments就是一个数组,那么可否使用数组方式呢?事实上,据说在某些js环境中,直接存取arguments[x]的效率是较差的。因此,上面的v1版本可以有一个改版:// v1.1,对v1的改版
function e_xor() {
var args=[].slice.call(arguments,0), argn=args.length;
...
}
这段小小的代码涉及到splice/slice的使用问题。因为操作的是arguments,因此splice可能导致函数入口的“奇异”变化,在不同的引擎中的表现效果并不一致,而slice则又可能导致多出一倍的数据复制。在这里仍然选用slice()的原因是:这里毕竟只是函数参数,不会是“极大量的”数组,因此无需过度考虑存储问题。
3、问题的泛化与求解:专业级别
接下来,我们既然在args中得到的是一个数组,那么再用for循环就实在不那么摩登了。正确的、流行风格的、不被前端鄙视做法是:// v2,使用js1.6+的数组方法的实现
function e_xor(a) {
return ([].slice.call(arguments,1)).some(function(b) { if (!b != !a) return true });
}为了向一些不太了解js1.6+新特性的同学解释v2这个版本,下面的代码分解了上述这个实现:
// v2.1,对v2的详细分解
function e_xor(a) {
var args = [].slice.call(arguments,1);
var callback = function(b) {
if (!b != !a) return true
}
return args.some(callback);
}some()这个方易做图将数组args中的每一个元素作为参数b传给callback函数。some()有一项特性正是与我们的原始需求一致的:
- 当callback()返回true的时候,some()会中断args的列举然后返回true值;否则,
- 当列举完全部元素且callback()未返回true的情况下,some()返回false值。
现在再读v2版本的e_xor(),是不是就清晰了?
当然,仅仅出于减少!a运算的必要,v2版本也可以有如下的一个改版:
// v2.2,对v2的优化以减少!a运算次数
function e_xor(a) {
return (a=!a, [].slice.call(arguments,1)).some(function(b) { if (!b != a) return true });
}在这行代码里,使用了连续运算:
(a=!a, [].slice.call(arguments,1))
而连续运算返回最后一个子表达式的值,即slice()后的数组。这样的写法,主要是要将代码控制在“一个表达式”。
4、问题的泛化与求解:Guy入门级别
好了,现在我们开始v3版本的写法了。为什么呢?因为v2版本仍然不够酷,v2版本使用的是Array.some(),这个在js1.6中扩展的特既不是那么的“函数式”,还有些面向对象的痕迹。作为一个函数式语言的死忠,我认为,类似于“列举一个数组”这样的问题的最正常解法是:递归。为什么呢?因为erlang这样的纯函数式语言就不会搞出个Array.some()的思路来——当然也是有这样的方法的,只是从“更纯正”的角度上讲,我们得自己写一个。呵呵。这种“纯正的递归”在js里面又怎么搞呢?大概的原型会是这样子:
// v3,采用纯函数式的、递归方案的框架
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上的一个原型方法。
Function.prototype.tail = function() {
return this.apply(this, [].slice.call(arguments,0).concat([].slice.call(this.arguments, this.length)));
}注意这个tail()方法的有趣之处:它用到了this.length。在javascript中的函数有两个length值,一个是foo.length,它表明foo函数在声明时的形式参数的个数;另一个是arguments.length,它表明在函数调用时,传入的实际参数的个数。也就是说,对于函数foo()来说:
function foo(a, b) {
alert([arguments.length, arguments.callee.length]);
}
foo(x);
foo(x,y,z);第一次调用将显示[1,2],第二次则会显示[3,2]。无论如何,声明时的参数a,b总是两个,所以foo.length == arguments.callee.length == 2。
回到tail()方法。它的意思是说:
Function.prototype.tail = function() {
return this.apply( // 重新调用函数自身
this, // 以函数foo自身作为this Object
[].slice.call(arguments,0) // 取调用tail时的全部参数,转换为数组
.concat( // 数组连接
[].slice.call(this.arguments, // 取本次函数foo调用时的参数,由于tail()总在foo()中调用,因此实际是取最近一次foo()的实际参数
this.length) // 按照foo()声明时的形式参数个数,截取foo()函数参数的尾部
)
);
}那么tail()在本例中如何使用呢?
// v3.1,使用tail()的版本
function e_xor(a, b) {
if (arguments.length == arguments.callee.length) return !a != !b;
return (!a == !b ? arguments.callee.tail(b) : true);
}这里又用到了arguments.callee.length来判断形式参数个数。也就是说,递归的结束条件是:只剩下a,b两个参数,无需再扫描tail()部分。当然,return中三元表达式(?:)右半部分也会中止递归,这种情况下,是已经找到了一个不相同的条件。
在这个例子中,我们将e_xor()写成了一个尾递归的函数,这个尾递归是函数式的精髓了,只可惜在js里面不支持它的优化。WUWU~~ 回头我查查资源,看看新的chrome v8是不是支持了。v8同学,尚V5否?:)
5、问题的泛化与求解:Guy进阶级别
从上一个小节中,我们看到了Guy解决问题的思路。但是在这个级别上,第一步的抽象通常是最关键的。简单地说,V3里认为:// v3,采用纯函数式的、递归方案的框架
function e_xor(a, b) { ... }这个框架抽象本身可能是有问题。正确的理解不是“a,b求异或”,而是“a跟其它元素求异或”。由此,v4的框架抽象是:
// v4,更优的函数式框架抽象,对接口的思考
function e_xor(a) { ... }在v3中,由于每次要向后续部分传入b值,因此我们需要在tail()中做数组拼接concat()。但是,当我们使用v4的框架时,b值本身就隐含在后续部分中,因此无需拼接。这样一来,tail()就有了新的写法——事实上,这更符合tail()的原意
补充:综合编程 , 其他综合 ,