來源:玉伯也叫射鵰
緣由
JavaScript 陣列去重經常出現在前端招聘的筆試題裡,比如:
有陣列
var arr = ['a', 'b', 'c', '1', 0, 'c', 1, '', 1, 0]
,請用 JavaScript 實現去重函式unqiue
,使得unique(arr)
返回['a', 'b', 'c', '1', 0, 1, '']
作為筆試題,考點有二:
1. 正確。別小看這個考點,考慮到 JavaScript 經常要在瀏覽器上執行,在千姿百態的各種瀏覽器環境下要保障一個函式的正確性可不是一件簡單的事,不信你繼續讀完這篇部落格。
2. 效能。雖然大部分情況下 JavaScript 語言本身(狹義範疇,不包含 DOM 等延拓)不會導致效能問題,但很不幸這是一道考題,因此面試官們還是會把效能作為一個考點。
在繼續往下閱讀之前,建議先實現一個自己認為最好的版本。
直覺方案
對於陣列去重,只要寫過程式的,立刻就能得到第一個解法:
1 2 3 4 5 6 7 8 9 10 11 12 |
function unique(arr) { var ret = [] for (var i = 0; i < arr.length; i++) { var item = arr[i] if (ret.indexOf(item) === -1) { ret.push(item) } } return ret } |
直覺往往很靠譜,在現代瀏覽器下,上面這個函式很正確,效能也不錯。但前端最大的悲哀也是挑戰之處在於,要支援各種執行環境。在 IE6-8 下,陣列的 indexOf
方法還不存在。直覺方案要稍微改造一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var indexOf = [].indexOf ? function(arr, item) { return arr.indexOf(item) } : function indexOf(arr, item) { for (var i = 0; i < arr.length; i++) { if (arr[i] === item) { return i } } return -1 } function unique(arr) { var ret = [] for (var i = 0; i < arr.length; i++) { var item = arr[i] if (indexOf(ret, item) === -1) { ret.push(item) } } return ret } |
寫到這一步,正確性已沒問題,但效能上,兩重迴圈會讓面試官們看了不爽。
優化方案
一談到優化,往往就是八仙過海、百花齊放。但八仙往往不接地氣,百花則很容易招來臭蟲。陣列去重的各種優化方案在此不一一討論,下面只說最常用效果也很不錯的一種。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function unique(arr) { var ret = [] var hash = {} for (var i = 0; i < arr.length; i++) { var item = arr[i] var key = typeof(item) + item if (hash[key] !== 1) { ret.push(item) hash[key] = 1 } } return ret } |
核心是構建了一個 hash
物件來替代 indexOf
. 注意在 JavaScript 裡,物件的鍵值只能是字串,因此需要var key = typeof(item) + item
來區分數值 1
和字串 '1'
等情況。
但優化真的很容易帶來坑,比如上面的實現,對下面這種輸入就無法判斷:
1 |
unique([ new String(1), new Number(1) ]) |
可以繼續修改程式碼,做到效能和正確性都很好。但往往,這帶來的結果並不好。
真實需求
寫到這裡,這篇部落格才算進入正題。程式設計師心中都會有一些夢想,比如寫出又通用效能又好的普適函式。這種夢想是讓程式設計師變得卓越的重要內驅力,但倘若不加以控制,也很容易走入迷途。
回到效能優化。這年頭有各種各樣優化,核心系統、資料庫、網路、前端等等,所有這些優化,都必須回答下面這個問題:
1. 當前有什麼。在什麼場景下進行優化,場景下有哪些具體限制。理清限制很重要,限制往往帶來自由。
2. 究竟要什麼。優化的目的是什麼。是提高穩定性,還是增大吞吐量,抑或減少使用者等待時間。在回答這個問題之前,優化都是徒勞。對這個問題的準確回答,能為優化帶來具體可測量的引數,這樣優化才有目標。
3. 可以放棄什麼。魚與熊掌不可兼得。優化的本質是在具體場景下的取捨、權衡。什麼都不願意放棄的話,優化往往會舉步維艱。
寫這篇部落格,不是為了解答一到筆試題,這道筆試題有點無聊。寫這篇部落格的原始驅動力是因為最近在做 SeaJS 的效能調優,其中有一個需求是:
1 2 3 4 5 6 |
define(function(require, exports) { var a = require('./a') var b = require('./b') ... require('./a').fn(...) }) |
上面是一個模組,通過解析函式字串,可以拿到模組的依賴陣列:['./a', './b', './a']
,這個依賴資訊有可能會出現重複欄位,因此要做去重。
針對這個具體場景,來回答上面三個問題:
1. 當前有什麼。有的是輸入限制,只需要考慮字串。
2. 究竟要什麼。這個問題比較簡單,希望 unique 方法儘可能快,指標是用 Chrome 除錯工具中的 Profiles 皮膚檢視指定測試頁面中 unique 方法的耗時,目標是 5ms 以內。
3. 可以放棄什麼。只需處理字串,其他型別的都可以不支援。談到放棄往往很有意思,這個問題不那麼簡單,接下來再說。
SeaJS 下的解決方案
一旦分析清楚了具體場景,解決方案就相對簡單:
1 2 3 4 5 6 7 8 9 |
function unique(arr) { var obj = {} forEach(arr, function(item) { obj[item] = 1 }) return keys(obj) } |
上面的程式碼依賴 forEach
和 keys
,離不開上下文環境(環境很重要很重要),完整程式碼:util-lang.js
上面這個方案,無論從程式碼體積、正確性、還是各種瀏覽器下的綜合效能來考量,都很不錯。
直到有一天出現這樣一個測試用例:
1 2 3 4 5 |
define(function(require, exports) { var a = require('toString') var b = require('hasOwnProperty') ... }) |
“完美”解決方案
上面的測試用例,會呼叫
1 |
unique([ 'toString', 'hasOwnProperty' ]) // 期待返回 [ 'toString', 'hasOwnProperty' ] |
IE 有各種各樣的 bug,下面是不怎麼著名但真實存在的一個:
1 2 3 4 |
var obj = { toString: 1, hasOwnProperty: 1 } for (var p in obj) { console.log(p) } |
在現代瀏覽器下,上面會正確輸出兩個值,但在 Old IE 下不會輸出。這是 IE 的列舉 bug:A safer Object.keys compatibility implementation “完美”的解決方案如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
var keys = Object.keys || (function () { var hasOwnProperty = Object.prototype.hasOwnProperty, hasDontEnumBug = !{toString:null}.propertyIsEnumerable("toString"), DontEnums = [ 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor' ], DontEnumsLength = DontEnums.length; return function (o) { if (typeof o != "object" && typeof o != "function" || o === null) throw new TypeError("Object.keys called on a non-object"); var result = []; for (var name in o) { if (hasOwnProperty.call(o, name)) result.push(name); } if (hasDontEnumBug) { for (var i = 0; i < DontEnumsLength; i++) { if (hasOwnProperty.call(o, DontEnums[i])) result.push(DontEnums[i]); } } return result; }; })(); |
除了 DontEnums
陣列,還可以特別注意 hasOwnProperty
的處理方式。**對於前端來說,要保障“正確”是一件多麼不容易的事。**
注意:行文至此,已經不是在討論 unique
的實現問題,比如上面實際上在討論的是 Object.keys
的實現問題。
我可以放棄什麼
我有什麼、我要什麼、我可以放棄什麼,這其實是馬雲在回答內網一個神貼時的回覆,那個神貼是我發的,因此馬雲這幾句話讓我印象非常深刻。
優化的本質是做減法,做減法最困難的是選擇放棄。
對於 SeaJS 來說,真的需要上面那個“完美”的解決方案嗎?
程式設計師心中的完美主義、理想主義情結曾一度讓我非常不能容忍程式碼中有 “bug” 存在。
可是,大家都懂的:
還有紅樓夢……
知道道理容易,比如很懷念小時候的《思想品德》課,要扶老奶奶過馬路、要誠實等等,絕大部分人都懂得這些道理,可做到的,發現沒幾個。
讓場景說話
如果你聽了我上面一通知易行難的扯淡就決定趕緊“放棄”,那也有悖程式設計師的職業素養。
依舊得回到具體場景。在 SeaJS 裡,適當調整程式碼邏輯:
1 2 |
// Remove duplicated dependencies mod.dependencies = unique(resolve(meta.dependencies)) |
上面的程式碼,能保證傳給 unique 方法的輸入是:
1 2 3 4 5 |
[ 'http://path/to/a.js', 'http://path/to/toString.js', 'http://path/to/hasOwnProperty.js' ] |
因此 DontEnums bug 在 SeaJS 裡通過這麼一調整就不存在了。
仔細分析,控制好輸入,會讓程式碼更簡單同時可靠。
其實不控制 unique 的輸入引數,DontEnums 在 SeaJS 裡也可以忽略。只要心理上邁過那道完美主義設定的檻就好。
小結
2010 年時,總結過效能優化的 ROBI 法則:
1. Reduce(減少)。減少可減少的。
2. Organize(組織)。妥善組織剩下的。
3. Balance(權衡)。權衡所失與所得。
4. Invent(創新)。這是更高的要求,比如 SPDY、Chrome 等。
當時忽略了一個重要因素是: 所有這些點,都必須腳踏實地在具體應用場景下去分析、去選擇,要讓場景說話。
因為瀏覽器的多樣性,前端的應用場景經常很複雜,效能優化充滿挑戰,也充滿機遇。