Why underscore
最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。
閱讀一些著名框架類庫的源碼,就好像和一個個大師對話,你會學到很多。為什么是 underscore?最主要的原因是 underscore 簡短精悍(約 1.5k 行),封裝了 100 多個有用的方法,耦合度低,非常適合逐個方法閱讀,適合樓主這樣的 JavaScript 初學者。從中,你不僅可以學到用 void 0 代替 undefined 避免 undefined 被重寫等一些小技巧 ,也可以學到變量類型判斷、函數節流&函數去抖等常用的方法,還可以學到很多瀏覽器兼容的 hack,更可以學到作者的整體設計思路以及 API 設計的原理(向后兼容)。
之后樓主會寫一系列的文章跟大家分享在源碼閱讀中學習到的知識。
- underscore-1.8.3 源碼全文注釋 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/underscore-1.8.3-analysis.js
- underscore-1.8.3 源碼解讀項目地址https://github.com/hanzichi/underscore-analysis
- underscore-1.8.3 源碼解讀系列文章https://github.com/hanzichi/underscore-analysis/issues
歡迎圍觀~ (如果有興趣,歡迎 star & watch~)您的關注是樓主繼續寫作的動力
for ... in
今天要跟大家聊聊 for ... in 在瀏覽器中的兼容問題。
for ... in 大家應該都不陌生,循環只遍歷可枚舉屬性。像 Array 和 Object 使用內置構造函數所創建的對象都會繼承自 Object.prototype 和 String.prototype 的不可枚舉屬性,例如 String 的 indexOf() 方法或者 Object 的 toString 方法。循環將迭代對象的所有可枚舉屬性和從它的構造函數的 prototype 繼承而來的(包括被覆蓋的內建屬性)。
我們舉個簡單的例子:
var obj = {name: 'hanzichi', age: 30};
for (var k in obj) {
console.log(k, obj[k]);
}
// 輸出
// name hanzichi
// age 30
等等,你跟我說 for ... in 這玩意有瀏覽器兼容性?!從來沒注意過啊,好像工作中也沒碰到過這樣的兼容性問題啊!確實如此,for ... in 要出問題,得滿足兩個條件,其一是在 IE < 9 瀏覽器中(又是萬惡的 IE!!),其二是被枚舉的對象重寫了某些鍵,比如 toString。
還是舉個簡單的例子:
var obj = {toString: 'hanzichi'};
for (var k in obj) {
alert(k);
}
ok,在 chrome 中我們 alert 出了預期的 "toString",而在 IE 8 中啥都沒有彈出。
我們回頭看看 for ... in 的作用,循環遍歷 可枚舉屬性,那么顯然 IE 8 將 toString "內定" 成了不可枚舉屬性(盡管已經被重寫)。那么如何判斷是否在類似 IE 8 這樣的環境中呢?underscore 中有個 hasEnumBug 函數就是用來做這個判斷的:
// Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
// IE < 9 下 不能用 for key in ... 來枚舉對象的某些 key
// 比如重寫了對象的 `toString` 方法,這個 key 值就不能在 IE < 9 下用 for in 枚舉到
// IE < 9,{toString: null}.propertyIsEnumerable('toString') 返回 false
// IE < 9,重寫的 `toString` 屬性被認為不可枚舉
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
代碼一目了然,用了 propertyIsEnumerable 方法。
那么哪些屬性被重寫之后不能用 for ... in 在 IE < 9 下枚舉到呢?有如下這些:
// IE < 9 下不能用 for in 來枚舉的 key 值集合
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
恩,應該還漏了個 constructor。
我們來看看 underscore 是怎么做的。
function collectNonEnumProps(obj, keys) {
var nonEnumIdx = nonEnumerableProps.length;
var constructor = obj.constructor;
// proto 是否是繼承的 prototype
var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
// Constructor is a special case.
// `constructor` 屬性需要特殊處理
// 如果 obj 有 `constructor` 這個 key
// 并且該 key 沒有在 keys 數組中
// 存入數組
var prop = 'constructor';
if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
// nonEnumerableProps 數組中的 keys
while (nonEnumIdx--) {
prop = nonEnumerableProps[nonEnumIdx];
// prop in obj 應該肯定返回 true 吧?是否不必要?
// obj[prop] !== proto[prop] 判斷該 key 是否來自于原型鏈
if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
keys.push(prop);
}
}
}
proto 變量保存了原型,一個對象的原型可以通過 obj.constructor.prototype 獲取,但是如果重寫了 constructor 很顯然就無法這樣獲取了,則用 Object.prototype 替換。這樣比如說重寫了 toString,我們只需要比較 obj.toString 是否和 proto.toString 引用相同即可。個人覺得源碼中的 prop in obj 判斷多余了,這不肯定返回 true 嗎?如果有理解錯誤,望指出。
而對于重寫了 constructor 的情況,underscore 用 hasOwnProperty 進行判斷。
對于重寫了以上幾種屬性的情況,underscore 確實能夠獲取其在 IE < 9 中的鍵,但是愛鉆牛角尖的樓主也十分不解,constructor 真的有必要和其他屬性分開來檢測嗎?
對于 toString 這樣的屬性被重寫,underscore 的判斷非常好,如果沒有被重寫,那么對象的 toString 方法肯定是繼承于原型鏈的,判斷對象的 toString 方法是否和原型鏈上的一致即可,但是用 hasOwnProperty 能判斷嗎?樓主覺得也是可以的,hasOwnProperty 方法用來判斷對象的 key 是否是自有屬性,即是否來自于原型鏈,如果被重寫了,那么應該會返回 true,否則 false。
而被重寫的 constructor 能否用 obj[prop] !== proto[prop] 來判斷呢?樓主覺得也是可以的,如果沒有被重寫,那么 obj.constructor === obj.constructor.prototype.constructor 返回 true,如果被重寫,obj.constructor === Object.prototype.constructor 返回 false。
關于這點,樓主也是百思不得其解,但是很顯然 constructor 屬性和其他屬性是有明顯區別的,從代碼理解角度來看,也是 underscore 這樣處理比較容易接受。如果是樓主理解有出入的地方,還望指出!
最后,小結下,對于 for ... in 在 IE < 9 下的兼容問題,樓主感覺并沒有那么重要,畢竟誰會沒事去重寫這些屬性呢!所以,知道有這么一回事就可以了。
最后的最后,給出這部分源碼位置,有興趣的同學可以看下 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L904-L946