原本发在点点上,现转移至此

前几日在网上看到一篇文章:JavaScript绝句,看了以后觉得里面的代码颇为有趣,不过文章里面只是简单的说了这样写的目的和结果,却没有令读者起到既知其然,又知其所以然的效果。这里简单写一篇小文章剖析一下这篇“绝句”背后的原理吧。


1. 取整同时转成数值型

'10.567890'|0
//结果: 10
'10.567890'^0
//结果: 10
-2.23456789|0
//结果: -2
~~-2.23456789
//结果: -2

第一条绝句短短几句话,看起来十分的简洁,实际上背后的道理确是多了去了。这个东西分三大块:

  • 首先字符型转成数值型本身没有什么可称道的,因为这就是JavaScript内置的类型转换功能,当字符型变量参与运算时,JS会自动将其转换为数值型(如果无法转化,变为NaN)。

  • 至于取整的原因,在蝴蝶书里有提到,道爷的原文如下:

    In Java, the bitwise operators work with integers. JavaScript doesn't have integers. It only has double precision floating-point numbers. So, the bitwise operators convert their number operands into integers, do their business, and then convert them back. In most languages, these operators are very close to the hardware and very fast. In JavaScript, they are very far from the hardware and very slow. JavaScript is rarely used for doing bit manipulation.

    翻译过来就是:位运算这个东西在Java里就是只能对整型进行操作的。JS压根没有整型这么个东西,JS里面的所有数值型都是双精度浮点数。因此,JS在进行位运算时,会首先将这些数字运算数转换为整数,然后再执行运算。在许多语言里,因为强类型的原因,位运算这种东西是接近于硬件处理速度的;而在JavaScript里,由于鸭子类型的存在,JavaScript根本就不知道进行运算的这货到底是个啥,所以它都尝试把它转化为整数(甚至于NaN,undefined都可以进行位运算),所以它非常非常的慢。我们基本不用JS进行位操作。

  • 所以转化为整型这里,实际上是用到了JavaScript强大的包容性。至于运算结果为什么不变呢?因为他所取的这些操作, | 是二进制或, x|0 永远等于x;^ 为异或,同0异1,所以 x^0 还是永远等于x;至于~是按位取反,搞了两次以后值当然是一样的。

结论:毫无意义,为啥不用 Math.floor ?清晰易懂还不出错。尽管这里利用了Javascript本身位运算自动取整的原理,但是 Javascript 位运算本身的效率低下的问题还是要注意。

2. 日期转数值

var d = +new Date(); //1295698416792

这一段就写的不明不白的了,什么叫日期转数值?这应该叫日期转时间戳。查看MDN上的Date()对象,里面有这么一段话:

The JavaScript date is measured in milliseconds since midnight 01 January, 1970 UTC. A day holds 86,400,000 milliseconds. The JavaScript Date object range is -100,000,000 days to 100,000,000 days relative to 01 January, 1970 UTC.

意思就是说,JS本身时间的内部表示形式就是Unix时间戳,以毫秒为单位记录着当前距离1970年1月1日0点的时间单位。这里不过是用一元运算符 + 给他转换成本来的表示形式而已。至于一元运算符 + 的功能,就是把一个变量转化为数值型,并且不对其进行任何操作。MDN里对本操作符评价极高:

unary plus is the fastest and preferred way of converting something into a number, because it does not perform any other operations on the number.

结论:可用。是JS转化时间戳的一个好方法。

3. 类数组对象转数组

var arr = [].slice.call(arguments)

这里又是一个比较有趣的写法,所谓的“类数组”,这里指的是JS里面每个函数自带的内置对象 arguments ,其可以获得函数的参数,并以一种类似数组的方式来保存(实际上这个对象只有callee, caller, length的方法)。如果你要对数组进行诸如切片,连接等操作怎么办?你就可以用上面的这个方法,当然也是MDN给出的解决方案。

写到这里我恍然大悟啊,怪不得前几日写由JavaScript反柯里化所想到的时,大牛在操作arguments时,统统都是 Array.prototype.xxx.call(arguments, xxx, ...) ,原来原因很简单:arguments不是数组,木有这些方法;如果要用,请 callapply 之。

这里还有一个奇技淫巧:当你需要把 arguments 合并入一个数组时,你当然可以先用上面的方法转换然后 concat 之,你也可以利用 push 的原理直接用 push.apply,方法对比如下:

function test() {
    var res = ['item1', 'item2']
    res = res.concat(Array.prototype.slice.call(arguments)) //方法1
    Array.prototype.push.apply(res, arguments)              //方法2
}

我们可以清楚的看到,方法二比方法一短那么一点(喂!)。嗯,就是这样。

结论:可用。当然直接写[]会为内存增加垃圾,如果不怕绝句写的太长,还是可以写成上文 Array.prototype.push.apply 这种形式的。

4. 漂亮的随机码

Math.random().toString(16).substring(2);
Math.random().toString(36).substring(2);

这个十分好理解,生成一个随机数,转化为n进制,然后截取其中几位而已。其中 toString() 函数的参数为基底,范围为2~36。

结论:可用,但是位数是不确定的,为保险起见建议 toString(36).substring(2, 10) ,可以妥妥的截出八位来。

5. 合并数组:

var a = [1,2,3];
var b = [4,5,6];
Array.prototype.push.apply(a, b);
uneval(a); //[1,2,3,4,5,6]

好,这个东西其实非常的不错。在上文的奇技淫巧中我们也提到了,当b是类数组时,可以用 push 方法来进行数组合并。但这里的问题是……这个b根本就是数组啊喂!有什么必要啊,难道你觉得JS的 concat 还不够好用么?再次比较一下代码:

var a = [1,2,3]
var b = [4,5,6]
Array.prototype.push.apply(a, b)   //方法1
a = a.concat(b)                    //方法2

作者的方法长好多啊!然后那个自定义的函数uneval是个什么东西啊!JS木有这种函数啊!

结论:毫无意义,建议使用原生 concat

6. 用0补全位数

function prefixInteger(num, length) {
    return (num / Math.pow(10, length)).toFixed(length).substr(2);
}
prefixInteger(2, 3)          //002

这里作者给我们展示了一个新的函数: toFixed(n) ,赶紧滚去查了一下MDN中的函数说明,这个函数的意思是对一个浮点数进行四舍五入,保留小数点后n位;默认为0,也即直接取整。

而作者这个函数的意思是把你给的一个数值先四舍五入取整,然后在前面补上各种0使最终获得一个等长的字符串。不过,由于他的算法是让原整数除以十的幂然后截取,这样当num的位数本身就多于length的时候就会出现bug,如下面这个输入:

prefixInteger(1234567, 3)     //34.567

最终输出的长度是5,不符合要求,所以函数应该进行错误处理之类的,比如加上下面这个 try catch 语句?

function prefixInteger(num, length) {
    try{
        if (num.toFixed().toString().length > length) 
            throw 'illegal number!'
        return (num / Math.pow(10, length)).toFixed(length).substr(2);
    }catch(err){
        console.log(err)
    }
}

结论:有点小bug,修改可用,不过改了以后蛮长的不像绝句像八股文呵呵其实我觉得还是可以再改进一点的。在某些场合的用处还是蛮强大的。

7. 交换值

a= [b, b=a][0];

本绝句中最帅的一句终于出场。这句话甚至有了pythonic的风格,虽然python的写法更简单:

a, b = b, a        #还是python最帅啊!

不过有豆瓣的网友对这一方法提出了质疑:交换值时声明的一个数组[b, b=a]产生了内存,只能等待JS自己进行内存回收。确实,如果要严格的节约内存,提高JS内存回收的效率,那么 new[]{}function 声明都应该少用(可以参照这篇文章:减少JavaScript垃圾回收)。不过至于交换变量,如果用传统的方式只能再声明一个变量做中介,这样实际上依旧会占用内存,不过这样内存是在函数完成时自动释放的罢了。

结论:可用,不过如果要批量使用,还是建议写个函数用函数内部变量交换。

8. 将一个数组插入另一个数组的指定位置

var a = [1,2,3,7,8,9];
var b = [4,5,6];
var insertIndex = 3;
a.splice.apply(a, Array.prototype.concat(insertIndex, 0, b));
// a: 1,2,3,4,5,6,7,8,9

这里用到了两个函数: spliceconcat ,我们看一下 splice 这个函数的定义arr.splice(x, y, item1, item2, ...) :就是从arr数组的第x位开始,首先削掉后面的y个,之后插入item1, item2等等。其实,这里是 apply 函数的一个通用应用:当函数foo的参数仅支持(item1, item2, ..)这样的参数传入时,如果你把item1, item2, ..存在数组items里,想把数组作为参数传给foo时,就可以这样写:

xx.foo.apply(xx, items)

结论:可用。鉴于 apply 函数可以把数组作为参数依次传入的性质,这只是广大应用中的一个特例。

9. 删除数组元素

var a = [1,2,3,4,5];
a.splice(3,1);           //a = [1,2,3,5]

是的,Javascript对于数组删除来说,没有什么好的方法。如果你用 delete a[3] 来删除的话,将会在数组里留下一个空洞,而且后面的下标也并没有递减。这个方法是道爷在书里提到的,原文如下:

Fortunately, JavaScript arrays have a splice method. It can do surgery on an array, deleting some number of elements and replacing them with other elements. The first argument is an ordinal in the array. The second argument is the number of elements to delete. (...) Because every property after the deleted property must be removed and reinserted with a new key, this might not go quickly for large arrays.

道爷说了这个函数的功能的同时也说了,这个函数实际上是把后面的元素先移除掉,然后作为新的键值重新插入,这样其实等于遍历了一次,和你自己写个for循环的效率差不多。而且道爷没有提到的是,这个函数是有一个返回值的,如果多次使用这样的函数操作,显然会增加内存的负担。所以或许从省内存的方式来看,使用for循环遍历然后逐个delete后面的元素会好一些。

结论:可用。既然道爷都推荐了,就不要纠结于这点可怜的内存上了吧。但是大型数组效率始终不高。

10. 快速取数组最大和最小值

Math.max.apply(Math, [1,2,3]) //3
Math.min.apply(Math, [1,2,3]) //1

这个就是重复绝句,详情参见绝句8。可能作者自己也不知道,apply一直是这么用的。

结论:可用,而且要学会这个技巧呀~

11. 条件判断:

var a = b && 1; 
//相当于
if (b) {
    a = 1
}

呵呵,这也算绝句呀……好吧。而且作者没有考虑到,如果b不为真,a的值就变成b了,也有豆瓣的网友看出了这个问题,其实这个应该相当于:

if (b) {
    a = 1
} else {
    a = b
}

结论:必须可用,没啥可说的。不过这是C语言里面的特性,不能算做是JavaScript的绝句吧。条件赋值如果不这么写你就out啦~

12. 判断IE:

var ie = /*@cc_on !@*/false;

好顶赞!当然不是说这个绝句好顶赞,而是我之前从来没有研究过如何判断IE,因为这个去看了一下,发现还是有很多方式的,列举如下:

// 貌似是最短的,利用IE不支持标准的ECMAscript中数组末逗号忽略的机制
var ie = !-[1,];
// 利用了IE的条件注释
var ie = /*@cc_on!@*/false;
// 还是条件注释
var ie//@cc_on=1;
// IE不支持垂直制表符
var ie = '\v'=='v';
// 原理同上
var ie = !+"\v1";

至于IE的条件注释,如果以后有精力再详细的补上吧。

结论:亲测可用,原理有待慢慢研究。