【WEB前端】JavaScript絕句的小研究

於明昊發表於2013-07-21

原本發在點點上,現轉移至此

前幾日在網上看到一篇文章: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的條件註釋,如果以後有精力再詳細的補上吧。

結論:親測可用,原理有待慢慢研究。

相關文章