前幾日在網上看到一篇文章:JavaScript絕句,看了以后覺得里面的代碼頗為有趣,不過文章里面只是簡單的說了這樣寫的目的和結果,卻沒有令讀者起到既知其然,又知其所以然的效果。這里簡單寫一篇小文章剖析一下這篇“絕句”背后的原理吧。
1. 取整同時轉成數值型
'10.567890'|0 //結果: 10 '10.567890'^0 //結果: 10 -2.23456789|0 //結果: -2 ~~-2.23456789 //結果: -2
第一條絕句短短幾句話,看起來十分的簡潔,實際上背后的道理確是多了去了。這個東西分三大塊:
首先字符型轉成數值型本身沒有什么可稱道的,因為這就是JavaScript內置的類型轉換功能,當字符型變量參與運算時,JS會自動將其轉換為數值型(如果無法轉化,變為NaN)。
至于取整的原因,在蝴蝶書里有提到,道爺的原文如下:
InJava, 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;至于~是按位取反,搞了兩次以后值當然是一樣的。
結論:可用。利用了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不是數組,木有這些方法;如果要用,請 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) //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
聲明都應該少用。不過至于交換變量,如果用傳統的方式只能再聲明一個變量做中介,這樣實際上依舊會占用內存,不過這樣內存是在函數完成時自動釋放的罷了。
結論:可用,不過如果要批量使用,還是建議寫個函數用函數內部變量交換。
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]
來刪除的話,將會在數組里留下一個空洞,而且后面的下標也并沒有遞減。這個方法是道爺在書里提到的,原文如下:
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的條件注釋,如果以后有精力再詳細的補上吧。
結論:親測可用,原理有待慢慢研究。
文章列表