前几日在网上看到一篇文章:JavaScript绝句,看了以后觉得里面的代码颇为有趣,不过文章里面只是简单的说了这样写的目的和结果,却没有令读者起到既知其然,又知其所以然的效果。这里简单写一篇小文章剖析一下这篇“绝句”背后的原理吧。
1. 取整同时转成数值型
'10.567890'|0
//结果: 10
'10.567890'^0
//结果: 10
-2.23456789|0
//结果: -2
~~-2.23456789
//结果: -2
第一条绝句短短几句话,看起来十分的简洁,实际上背后的道理确是多了去了。这个东西分三大块:
首先字符型转成数值型本身没有什么可称道的,因为这就是JavaScript内置的类型转换功能,当字符型变量参与运算时,JS会自动将其转换为数值型(如果无法转化,变为NaN)。
至于取整的原因,在蝴蝶书里有提到,道爷的原文如下:
翻译过来就是:位运算这个东西在Java里就是只能对整型进行操作的。JS压根没有整型这么个东西,JS里面的所有数值型都是双精度浮点数。因此,JS在进行位运算时,会首先将这些数字运算数转换为整数,然后再执行运算。在许多语言里,因为强类型的原因,位运算这种东西是接近于硬件处理速度的;而在JavaScript里,由于鸭子类型的存在,JavaScript根本就不知道进行运算的这货到底是个啥,所以它都尝试把它转化为整数(甚至于NaN,undefined都可以进行位运算),所以它非常非常的慢。我们基本不用JS进行位操作。
所以转化为整型这里,实际上是用到了JavaScript强大的包容性。至于运算结果为什么不变呢?因为他所取的这些操作, |
是二进制或, x|0
永远等于x;^为异或,同0异1,所以 x^0
还是永远等于x;至于~是按位取反,搞了两次以后值当然是一样的。
结论:可用。利用了Javascript本身位运算自动取整的原理,至于位运算本身的效率比硬件处理速度低下……这个倒是无妨,因为我相信咱们自己写一个取整函数的效率应该也不会比Javascript自动取整高到哪儿去,多了个位运算这一点就忍了吧。
2. 日期转数值
var d = +new Date(); //
这一段就写的不明不白的了,什么叫日期转数值?这应该叫日期转时间戳。查看MDN上的Date()对象,里面有这么一段话:
意思就是说,JS本身时间的内部表示形式就是Unix时间戳,以毫秒为单位记录着当前距离1970年1月1日0点的时间单位。这里不过是用一元运算符 +
给他转换成本来的表示形式而已。至于一元运算符+
的功能,就是把一个变量转化为数值型,并且不对其进行任何操作。MDN里对本操作符评价极高:
结论:可用。是JS转化时间戳的一个好方法。
3. 类数组对象转数组
var arr =[].slice.call(arguments)
这里又是一个比较有趣的写法,所谓的“类数组”,这里指的是JS里面每个函数自带的内置对象arguments
,其可以获得函数的参数,并以一种类似数组的方式来保存(实际上这个对象只有callee, caller, length的方法)。如果你要对数组进行诸如切片,连接等操作怎么办?你就可以用上面的这个方法,当然也是MDN给出的解决方案。
写到这里我恍然大悟啊,怪不得前几日写由JavaScript反柯里化所想到的时,大牛在操作arguments时,统统都是Array.prototype.xxx.call(arguments, xxx, ...)
,原来原因很简单:arguments不是数组,木有这些方法;如果要用,请 call
或 apply
之。
这里还有一个奇技淫巧:当你需要把 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木有这种函数啊!
结论:其实它正确的使用点在于3里面的奇技淫巧,对于单纯的数组……建议还是用concat吧。
6. 用0补全位数
function prefixInteger(num, length) {
return (num / Math.pow(10, length)).toFixed(length).substr(2);
}
prefixInteger(2, 3) //
这里作者给我们展示了一个新的函数: 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
声明都应该少用。不过至于交换变量,如果用传统的方式只能再声明一个变量做中介,这样实际上依旧会占用内存,不过这样内存是在函数完成时自动释放的罢了。
结论:可用,不过如果要批量使用,还是建议写个函数用函数内部变量交换。
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
这里用到了两个函数: splice
和 concat
,我们看一下 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]
来删除的话,将会在数组里留下一个空洞,而且后面的下标也并没有递减。这个方法是道爷在书里提到的,原文如下:
道爷说了这个函数的功能的同时也说了,这个函数实际上是把后面的元素先移除掉,然后作为新的键值重新插入,这样其实等于遍历了一次,和你自己写个for循环的效率差不多。而且道爷没有提到的是,这个函数是有一个返回值的,如果多次使用这样的函数操作,显然会增加内存的负担。所以或许从省内存的方式来看,使用for循环遍历然后逐个delete后面的元素会好一些。
结论:可用。既然道爷都推荐了,就不要纠结于这点可怜的内存上了吧。但是大型数组效率始终不高。
10. 快速取数组最大和最小值
Math.max.apply(Math, [1,2,3]) //
Math.min.apply(Math, [1,2,3]) //
这个就是重复绝句,详情参见绝句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的条件注释,如果以后有精力再详细的补上吧。
结论:亲测可用,原理有待慢慢研究。