【underscore.js 源碼解讀】for ... in 存在的瀏覽器兼容問題你造嗎

Why underscore

最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。

閱讀一些著名框架類庫的源碼,就好像和一個個大師對話,你會學到很多。為什么是 underscore?最主要的原因是 underscore 簡短精悍(約 1.5k 行),封裝了 100 多個有用的方法,耦合度低,非常適合逐個方法閱讀,適合樓主這樣的 JavaScript 初學者。從中,你不僅可以學到用 void 0 代替 undefined 避免 undefined 被重寫等一些小技巧 ,也可以學到變量類型判斷、函數節流&函數去抖等常用的方法,還可以學到很多瀏覽器兼容的 hack,更可以學到作者的整體設計思路以及 API 設計的原理(向后兼容)。

之后樓主會寫一系列的文章跟大家分享在源碼閱讀中學習到的知識。

歡迎圍觀~ (如果有興趣,歡迎 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容