前幾天一個朋友在微信裏面問我一個關於 JS 數組排序的問題。java
原始數組以下:算法
var data = [ {value: 4}, {value: 2}, {value: undefined}, {value: undefined}, {value: 1}, {value: undefined}, {value: undefined}, {value: 7}, {value: undefined}, {value: 4} ];
data
是個數組,數組的每一項都是一個擁有 value
做爲 key 的對象,值爲數字或者 undefined
。編程
data .sort((x, y) => x.value - y.value) .map(x => x.value);
對數組的 value
進行排序,而後把排完序的數組進行 flat 處理。獲得的結果以下:數組
[2, 4, undefined, undefined, 1, undefined, undefined, 7, undefined, 4]
顯然這沒有達到咱們的目的。瀏覽器
如今咱們修改一下排序,挑戰一下函數的調用順序:先對數組進行扁平化(flat)處理,而後再排序。微信
data .map(x => x.value) .sort((x, y) => x - y)
這時咱們獲得的結果和以前大相徑庭:函數
[1, 2, 4, 4, 7, undefined, undefined, undefined, undefined, undefined]
遇到這種狀況第一感受確定是要去看看 ECMA 規範,萬一是 JS 引擎的 bug 呢。性能
在 ES6 規範 22.1.3.24 節寫道:this
Calling
comparefn(a,b)
always returns the same valuev
when given a specific pair of valuesa
andb
as its two arguments. Furthermore,Type(v)
isNumber
, andv
is notNaN
. Note that this implies that exactly one ofa < b
,a = b
, anda > b
will betrue
for a given pair ofa
andb
.spa
簡單翻譯一下就是:第二個參數 comparefn
返回一個數字,而且不是 NaN
。一個注意事項是,對於參與比較的兩個數 a
小於 b
、a
等於 b
、a
大於 b
這三種狀況必須有一個爲 true
。
因此嚴格意義上來講,這段代碼是有 bug 的,由於比較的結果出現了 NaN
。
在 MDN 文檔上還有一個細節:
若是
comparefn(a, b)
等於0
,a
和b
的相對位置不變。備註:ECMAScript 標準並不保證這一行爲,並且也不是全部瀏覽器都會遵照。
翻譯成編程術語就是:sort
排序算法是不穩定排序。
其實咱們最疑惑的問題上,上面兩行代碼爲何會輸出不一樣的結果。咱們只能經過查看 V8 源碼去找答案了。
V8 對數組排序是這樣進行的:
若是沒有定義 comparefn 參數,則生成一個(高能預警,有坑啊):
comparefn = function (x, y) { if (x === y) return 0; if (%_IsSmi(x) && %_IsSmi(y)) { return %SmiLexicographicCompare(x, y); } x = TO_STRING(x); // <----- 坑 y = TO_STRING(y); // <----- 坑 if (x == y) return 0; else return x < y ? -1 : 1; };
而後定義了一個插入排序算法:
function InsertionSort(a, from, to) { for (var i = from + 1; i < to; i++) { var element = a[i]; for (var j = i - 1; j >= from; j--) { var tmp = a[j]; var order = comparefn(tmp, element); if (order > 0) { // <---- 注意這裏 a[j + 1] = tmp; } else { break; } } a[j + 1] = element; }
爲何是插入排序?V8 爲了性能考慮,當數組元素個數少於 10 個時,使用插入排序;大於 10 個時使用快速排序。
後面還定義了快速排序函數和其它幾個函數,我就不一一列出了。
函數都定義完成後,開始正式的排序操做:
// %RemoveArrayHoles returns -1 if fast removal is not supported. var num_non_undefined = %RemoveArrayHoles(array, length); if (num_non_undefined == -1) { // There were indexed accessors in the array. // Move array holes and undefineds to the end using a Javascript function // that is safe in the presence of accessors. num_non_undefined = SafeRemoveArrayHoles(array); }
中間的註釋:Move array holes and undefineds to the end using a Javascript function。排序以前會把數組裏面的 undefined
移動到最後。所以第二個排序算法會把 undefined
移動到最後,而後對剩餘的數據 [4,2,1,7,4]
進行排序。
而在第一種寫法時,數組的每一項都是一個 Object,而後最 Object 調用 x.value - y.value
進行計算,當 undefined
參與運算時比較的結果是 NaN
。當返回 NaN
時 V8 怎麼處理的呢?我前面標註過,再貼一次:
var order = comparefn(tmp, element); if (order > 0) { // <---- 這裏 a[j + 1] = tmp; } else { break; }
NaN > 0
爲 false
,執行了 else
分支代碼。
思考題,如下代碼的結果:
[1, 23, 2, 3].sort()
掃碼二維碼關注個人公衆號