悄悄地說一個bug

前言

underscore.js源碼分析第四篇,前三篇地址分別是,若是你對這個系列感興趣,歡迎點擊watch,隨時關注動態。javascript

教你認清這8大殺手鐗java

那些不起眼的小工具?git

(void 0)與undefined之間的小九九github

原文地址
源碼地址數組

逗我呢?哥!你要說什麼bug,什麼bug,什麼bug,我最討厭bug。去他妹的bug。瀏覽器

客觀別急,今天真的是要說一個bug,也許你早已知曉,也許你時常躺槍於他手,悄悄地,咱們慢慢開始。函數

for in 遍歷對象屬性時存在bug工具

for in 遍歷對象屬性時存在bug源碼分析

for in 遍歷對象屬性時存在bugthis

使用for in去遍歷一個對象俺們再熟悉不過了,常常幹這種事,那他到底能夠遍歷一個對象哪些類型的屬性呢? 長得帥的仍是看起來美美的,瞎說,它可以遍歷的是對象身上那些可枚舉標誌([[Enumerable]])爲true的屬性。

  1. 對於經過直接的賦值和屬性初始化的屬性,該標識值默認爲即爲 true

  2. 對於經過 Object.defineProperty 等定義的屬性,該標識值默認爲 false

舉個例子哪些屬性能夠被枚舉

let Person = function (name, sex) {
  this.name = name
  this.sex = sex
}

Person.prototype = {
  constructor: Person,
  showName () {
    console.log(this.name)
  },
  showSex () {
    console.log(this.sex)
  }
}

Person.wrap = {
  sayHi () {
    console.log('hi')
  }
}

var p1 = new Person('qianlongo', 'sex')

p1.sayBye = () => {
  console.log('bye')
}

p1.toString = () => {
  console.log('string')
}

Object.defineProperty(p1, 'info', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 'feDev'
});Ï

for (var key in p1) {
  console.log(key)
}


// name
// sex
// sayBye
// constructor
// showName
// showSex
// toString
  1. 能夠看到咱們手動地用defineProperty,給某個對象設置屬性時,enumerable爲false此時該屬性是不可枚舉的

  2. Person繼承自Object構造函數,可是for in並無枚舉出Object原型上的一些方法

  3. 手動地覆蓋對象原型上面的方法toString也是可枚舉的

如何判斷一個對象的屬性是可枚舉的

方式其實很簡單,使用原生js提供的Object.propertyIsEnumerable來判斷

let obj = {
  name: 'qianlongo'
}

let obj2 = {
  name: 'qianlongo2',
  toString () {
    return this.name
  }
}

obj.propertyIsEnumerable('name') // true
obj.propertyIsEnumerable('toString') // false

obj2.propertyIsEnumerable('name') // true
obj2.propertyIsEnumerable('toString') // true

爲何obj判斷toString爲不可枚舉屬性,而obj2就是可枚舉的了呢?緣由很簡單,obj2將toString重寫了,而一個對象自身直接賦值的屬性是可被枚舉的

說了這麼多,接下來咱們來看一下下劃線中涉及到遍歷的部分對象方法,come on!!!

_.has(object, key)

判斷對象obejct是否包含key屬性

平時你可能常常這樣去判斷一個對象是否包含某個屬性

if (obj && obj.key) {
  // xxx
}

可是這樣作有缺陷,好比某個屬性其對應的值爲0,null,false,''空字符串呢?這樣明明obj有如下對應的屬性,卻由於屬性值爲而經過不了驗證

let obj = {
  name: '',
  sex: 0,
  handsomeBoy: false,
  timer: null
}

因此咱們能夠採用下劃線中的這種方式

源碼

var hasOwnProperty = ObjProto.hasOwnProperty;
_.has = function(obj, key) {
  return obj != null && hasOwnProperty.call(obj, key);
};

_.keys(object)

獲取object對象全部的屬性名稱。

使用示例

let obj = {
  name: 'qianlongo',
  sex: 'boy'
}

let keys = _.keys(obj)
// ["name", "sex"]

源碼

_.keys = function(obj) {
  // 若是obj不是object類型直接返回空數組
  if (!_.isObject(obj)) return [];
  // 若是瀏覽器支持原生的keys方法,則使用原生的keys
  if (nativeKeys) return nativeKeys(obj);
  var keys = [];
  // 注意這裏一、for in會遍歷原型上的鍵,因此用_.has來確保讀取的只是對象自己的屬性
  for (var key in obj) if (_.has(obj, key)) keys.push(key);
  // Ahem, IE < 9.
  // 這裏主要處理ie9如下的瀏覽器的bug,會將對象上一些本該枚舉的屬性認爲不可枚舉,詳細能夠看collectNonEnumProps分析
  if (hasEnumBug) collectNonEnumProps(obj, keys); 
  return keys;
};

collectNonEnumProps函數分析

該函數爲下劃線中的內部函數一枚,專門處理ie9如下的枚舉bug問題,for in到底有啥bug,終於能夠說出來了。

簡單地說就是若是對象將其原型上的相似toString的方法覆蓋了的話,那麼咱們認爲toString就是可枚舉的了,可是在ie9如下的瀏覽器中仍是認爲是不能夠枚舉的,又是萬惡的ie

源碼

// 判斷瀏覽器是否存在枚舉bug,若是有,在取反操做前會返回false
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); 
// 全部須要處理的可能存在枚舉問題的屬性
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
                    'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; 

// 處理ie9如下的一個枚舉bug                      
function collectNonEnumProps(obj, keys) {
  var nonEnumIdx = nonEnumerableProps.length;
  var constructor = obj.constructor;
  // 讀取obj的原型
  var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;  

  // 這裏我有個疑問,對於constructor屬性爲何要單獨處理?
  // Constructor is a special case.
  var prop = 'constructor'; 
  if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);

  while (nonEnumIdx--) {
    prop = nonEnumerableProps[nonEnumIdx];
    // nonEnumerableProps中的屬性出如今obj中,而且和原型中的同名方法不等,再者keys中不存在該屬性,就添加進去
    if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
      keys.push(prop);
    }
  }
}

代碼看起來並不複雜,可是有一個小疑問,對於constructor屬性爲何要單獨處理呢?各個看官,若是知曉,請教我啊

_.allKeys(object)

獲取object中全部的屬性,包括原型上的。

舉個簡單的例子說明

let Person = function (name, sex) {
  this.name = name
  this.sex = sex
}

Person.prototype = {
  constructor: Person,
  showName () {
    console.log(this.name)
  }
}

let p = new Person('qianlongo', 'boy')

_.keys(p)
// ["name", "sex"] 只包括自身的屬性


_.allKeys(p)
// ["name", "sex", "constructor", "showName"] 還包括原型上的屬性

接下來看下源碼是怎麼幹的

源碼

// 獲取對象obj的全部的鍵
// 與keys不一樣,這裏包括繼承來的key

// Retrieve all the property names of an object.
_.allKeys = function(obj) {
  if (!_.isObject(obj)) return [];
  var keys = [];
  // 直接讀遍歷取到的key,包括原型上的
  for (var key in obj) keys.push(key); 
  // Ahem, IE < 9.
  if (hasEnumBug) collectNonEnumProps(obj, keys); // 一樣處理一下有枚舉問題的瀏覽器
  return keys;
};

能夠看到和_.keys的惟一的不一樣就在於遍歷obj的時候有沒有用hasOwnProperty去判斷

_.values()

返回object對象全部的屬性值。

使用案例

let obj = {
  name: 'qianlongo',
  sex: 'boy'
}

_.values(obj)
// ["qianlongo", "boy"]

源碼

// Retrieve the values of an object's properties.
_.values = function(obj) {
  // 用到了前面已經寫好的keys函數,因此values認爲獲取的屬性值,不包括原型
  var keys = _.keys(obj);
  var length = keys.length;
  var values = Array(length);
  for (var i = 0; i < length; i++) {
    values[i] = obj[keys[i]];
  }
  return values;
};

_.invert(object)

返回一個object副本,使其鍵(keys)和值(values)對換。

使用案例

let obj = {
  name: 'qianlongo',
  secName: 'qianlongo',
  age: 100
}

_.invert(obj)

// {100: "age", qianlongo: "secName"}

注意喲,若是對象中有些屬性值是相等的,那麼翻轉過來的對象其key取最後一個

源碼

_.invert = function(obj) {
  var result = {};
  // 因此也只是取對象自己的屬性
  var keys = _.keys(obj); 
  for (var i = 0, length = keys.length; i < length; i++) {
    // 值爲key,key爲值,若是有值相等,後面的覆蓋前面的
    result[obj[keys[i]]] = keys[i]; 
  }
  return result;
};

_.functions(object)

返回一個對象裏全部的方法名, 並且是已經排序的(注意這裏包括原型上的屬性)

源碼

_.functions = _.methods = function(obj) {
  var names = [];
  for (var key in obj) {
    // 是函數,就裝載進去
    if (_.isFunction(obj[key])) names.push(key);
  }
  return names.sort(); // 最後返回通過排序的數組
};

結尾

夜深人靜,悄悄地說一個bug這個鬼故事講完了,各位good night。

相關文章
相關標籤/搜索