JavaScript中in操做符(for..in)、Object.keys()和Object.getOwnPropertyNames()的區別

ECMAScript將對象的屬性分爲兩種:數據屬性訪問器屬性。每一種屬性內部都有一些特性,這裏咱們只關注對象屬性的[[Enumerable]]特徵,它表示是否經過 for-in 循環返回屬性,也能夠理解爲:是否可枚舉。html

而後根據具體的上下文環境的不一樣,咱們又能夠將屬性分爲:原型屬性實例屬性。原型屬性是定義在對象的原型(prototype)中的屬性,而實例屬性一方面來本身構造函數中,而後就是構造函數實例化後添加的新屬性。java

本文主要介紹JavaScript中獲取對象屬性經常使用到的三種方法的區別和適用場景。chrome

for..in循環

使用for..in循環時,返回的是全部可以經過對象訪問的、可枚舉的屬性,既包括存在於實例中的屬性,也包括存在於原型中的實例。這裏須要注意的是使用for-in返回的屬性因各個瀏覽器廠商遵循的標準不一致致使對象屬性遍歷的順序有可能不是當初構建時的順序。數組

遍歷數組

雖然for..in主要用於遍歷對象的屬性,但一樣也能夠用來遍歷數組元素。瀏覽器

 

var arr = ['a', 'b', 'c', 'd'];

// 使用for..in
for (var i in arr) {
  console.log('索引:' + i + ',值:' + arr[i]);
}

// 使用for循環
for (var j = 0; j < arr.length; j++) {
  console.log('索引:' + j + ',值:' + arr[j]);
}

/* 兩種方式都輸出:
 * ----------------
 * 索引:0,值:a
 * 索引:1,值:b
 * 索引:2,值:c
 * 索引:3,值:d
 * ----------------
 */

 

 

上面這個簡單例子相信你們對輸出沒有任何質疑吧。然而,我在網上看到一些關於for和for..in遍歷數組的文章,好比js中數組遍歷for與for in區別(強烈建議不要使用for in遍歷數組)[原]js數組遍歷 千萬不要使用for...in...,同時也看了stackoverflow關於Why is using 「for…in」 with array iteration such a bad idea?的討論。看完後仍是雲裏霧裏的,因而尋根問底,打算本身來研究一下。for..in在數組遍歷方面就那麼差強人意嗎?安全

關於for..in和for遍歷數組的的爭論總結起來主要在三個點。ide

第一個問題:若是擴展了原生的Array,那麼擴展的屬性爲何會被for..in輸出?

這個問題也是上面我提到的兩篇文章關注的重點。其實,這個問題若是咱們將關注點放在for..in方法的定義上就不難看出端倪,定義中強調了一點它所遍歷的是可枚舉的屬性。咱們在擴展Array原型的時候有去對比本身添加的屬性與Array原生的屬性有什麼不同的地方嗎?這裏我強調的不一致的地方在於屬性其中的一個特性[[enumberable]],在文章開頭也有特地介紹了一下。如何查看一個屬性的特性可使用propertyIsEnumberable()Object.getOwnPropertyDescriptor()這兩個方法。函數

var colors = ['red', 'green', 'blue'];
// 擴展Array.prototype
Array.prototype.demo = function () {};

for (var i in colors) {
  console.log(i); // 輸出: 0 1 2 demo
}

// 查看原生的方法[[enumberable]]特徵,這裏以splice爲例
Array.prototype.propertyIsEnumerable('splice'); // false
Object.getOwnPropertyDescriptor(Array.prototype, 'splice'); // {writable: true, enumerable: false, configurable: true}

// 查看 demo 屬性的特性
Array.prototype.propertyIsEnumerable('demo'); // true
Object.getOwnPropertyDescriptor(Array.prototype, 'demo'); // {writable: true, enumerable: true, configurable: true}

從上面的示例代碼中能夠看出,咱們添加的demo方法,默認是能夠被for..in枚舉出來的。若是想讓其不被枚舉,那麼可使用ES5的Object.defineProperty()來定義屬性,此外若是瀏覽器版本不支持ES5的話,咱們可使用hasOwnProperty()方法在for..in代碼塊內將可枚舉的屬性過濾掉。工具

var colors = ['red', 'green', 'blue'];
Object.defineProperty(Array.prototype, 'demo', {
  enumerable: false,
  value: function() {}
});

Array.prototype.propertyIsEnumerable('demo'); // false
Object.getOwnPropertyDescriptor(Array.prototype, 'demo'); // {writable: false, enumerable: false, configurable: false}

for (var i in colors) {
  console.log(i); // 輸出:0 1 2
}

// 或者使用 hasOwnProperty
var colors = ['red', 'green', 'blue'];
Array.prototype.demo = function() {};

// 安全使用hasOwnProperty方法
var hasOwn = Object.prototype.hasOwnProperty;
for (var i in colors) {
  if (hasOwn.call(colors, i)) {
    console.log(i); // 輸出:0 1 2
  }
}
第二問題:for..in和for遍歷數組時下標類型不同

這裏指的是for (var i in colors) {}for (var i = 0; i < colors.length; i++) {}中的i,示例以下:ui

var colors = ['red', 'green', 'blue'];

for (var i in colors) {
  typeof i; // string
}

for (var j = 0; j < colors.length; j++) {
  typoef i; // number
}

 

至於爲何for..in在遍歷數組時i爲字符串?個人理解是若是咱們從對象的視角來看待數組的話,實際上它是一個key爲下標,value爲數組元素值的對象,好比colors數組能夠寫成下面對象的形式:

var colors = {
  0: 'red',
  1: 'green',
  2: 'blue'
}

而後,咱們須要訪問colors對象中的屬性,colors.0這樣顯然會報語法錯識,那麼只能使用colors['0']這種形式了。這可能就是爲何i的值爲字符串,而不是數字的緣由。

第三個問題:對於不存在的數組項的處理差別

最後一個問題在於數組中不存在元素的處理。對於數組來說,咱們知道若是將其length屬性設置爲大於數組項數的值,則新增的每一項都會取得undefined值。

var colors = ['red', 'green', 'blue'];
// 將數組長度變爲10
colors.length = 10;
// 再添加一個元素的數組末尾
colors.push('yellow');

for (var i in colors) {
  console.log(i); // 0 1 2 10
}

for (var j = 0; j < colors.length; j++) {
  console.log(j); // 0 1 2 3 4 5 6 7 8 9 10
}

示例中colors數組位置3到位置10項實際上都是不存在的。仔細觀察使用for..in遍歷數組的結果,咱們發現對於不存在的項是不會被枚舉出來的。經過chrome調式並監聽colors變量,咱們能夠看到它的內部結構以下:

|----------------------|
|       colors         |
|----------------------|
| 0      | 'red'       |
|----------------------|
| 1      | 'green'     |
|----------------------|
| 2      | 'blue'      |
|----------------------|
| 10     | 'yellow'    |
|----------------------|
| length | 11          |
|----------------------|
| __proto__ | Array[0] |
|----------------------|

也就是說使用for..in遍歷數組的結果其實是和它在調試工具中看到的結構是一致的。雖然不存在的元素沒有在調試工具中顯示出來,可是它在內存中是存在的,咱們仍然能夠刪除這些元素。

var colors = ['red', 'green', 'blue'];
colors.length = 10;
colors.push('yellow');

// 刪除第4至第10項元素
colors.splice(3, 6);

for (var i in colors) {
  console.log(i); // 輸出:0 1 2 4
}

 

雖然使用for..in遍歷數組它自動過濾掉了不存在的元素,可是對於存在的元素且值爲undefined或者'null'仍然會有效輸出。此外咱們也可使用in操做符來判斷某個key值(數組中的索引)是否存在對應的元素。

var colors = ['red', 'green', 'blue'];

1 in colors; // true
// 或者
'1' in colors; // true

// colors[3]沒有對應的元素
'3' in colors; // false

遍歷對象

其實for..in操做的主要目的就是遍歷對象的屬性,若是隻須要獲取對象的實例屬性,可使用hasOwnProperty()進行過濾。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.getName = function() {
  return this.name;
}

// 實例化
var jenemy = new Person('jenemy', 25);

for (var prop in Person) {
  console.log(prop); // name age getName
}

var hasOwn = Object.prototype.hasOwnProperty;
for (var prop2 in jenemy) {
  if (hasOwn.call(jenemy, prop2)) {
    console.log(prop2); // name age
  }
}

Object.keys()

Object.keys()用於獲取對象自身全部的可枚舉的屬性值,但不包括原型中的屬性,而後返回一個由屬性名組成的數組。注意它同for..in同樣不能保證屬性按對象原來的順序輸出。

// 遍歷數組
var colors = ['red', 'green', 'blue'];
colors.length = 10;
colors.push('yellow');
Array.prototype.demo = function () {};

Object.keys(colors); // 0 1 2 10

// 遍歷對象
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.demo = function() {};

var jenemy = new Person('jenemy', 25);

Object.keys(jenemy); // name age

注意在 ES5 環境,若是傳入的參數不是一個對象,而是一個字符串,那麼它會報 TypeError。在 ES6 環境,若是傳入的是一個非對象參數,內部會對參數做一次強制對象轉換,若是轉換不成功會拋出 TypeError。

// 在 ES5 環境
Object.keys('foo'); // TypeError: "foo" is not an object

// 在 ES6 環境
Object.keys('foo'); // ["0", "1", "2"]

// 傳入 null 對象
Object.keys(null); // Uncaught TypeError: Cannot convert undefined or null to object

// 傳入 undefined
Object.keys(undefined); // Uncaught TypeError: Cannot convert undefined or null to object

因爲Object.keys()爲ES5上的方法,所以對於ES5如下的環境須要進行polyfill

// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
if (!Object.keys) {
  Object.keys = (function() {
    'use strict';
    var hasOwn = Object.prototype.hasOwnProperty,
        hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'),
        dontEnums = [
          'toString',
          'toLocaleString',
          'valueOf',
          'hasOwnProperty',
          'isPrototypeOf',
          'propertyIsEnumerable',
          'constructor'
        ],
        dontEnumsLength = dontEnums.length;

      return function(obj) {
        if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
          throw new TypeError('Object.keys called on non-object');
        }

        var result = [], prop, i;

        for (prop in obj) {
          if (hasOwn.call(obj, prop)) {
            result.push(prop);
          }
        }

        if (hasDontEnumBug) {
          for (i = 0; i < dontEnumsLength; i++) {
            if (hasOwn.call(obj, dontEnums[i])) {
              result.push(dontEnums[i]);
            }
          }
        }
        return result;
      }
  }) ();
}

Object.getOwnPropertyNames()

Object.getOwnPropertyNames()方法返回對象的全部自身屬性的屬性名(包括不可枚舉的屬性)組成的數組,但不會獲取原型鏈上的屬性。

function A(a,aa) {
  this.a = a;
  this.aa = aa;
  this.getA = function() {
    return this.a;
  }
}
// 原型方法
A.prototype.aaa = function () {};

var B = new A('b', 'bb');
B.myMethodA = function() {};
// 不可枚舉方法
Object.defineProperty(B, 'myMethodB', {
  enumerable: false,
  value: function() {}
});

Object.getOwnPropertyNames(B); // ["a", "aa", "getA", "myMethodA", "myMethodB"]

 

補充for..of

for..of爲ES6新增的方法,主要來遍歷可迭代的對象(包括Array, Map, Set, arguments等),它主要用來獲取對象的屬性值,而for..in主要獲取對象的屬性名。

var colors = ['red', 'green', 'blue'];
colors.length = 5;
colors.push('yellow');

for (var i in colors) {
  console.log(colors[i]); // red green blue yellow
}

for (var j of colors) {
  console.log(j); // red green blue undefined undefined yellow
}

 

能夠看到使用for..of能夠輸出包括數組中不存在的值在內的全部值。

其實除了使用for..of直接獲取屬性值外,咱們也能夠利用Array.prototype.forEach()來達到一樣的目的。

var colors = ['red', 'green', 'blue'];
colors.foo = 'hello';

Object.keys(colors).forEach(function(elem, index) {
  console.log(colors[elem]); // red green blue hello
  console.log(colors[index]); // red green blue undefined
});

colors.forEach(function(elem, index) {
  console.log(elem); // red green blue
  console.log(index); // 0 1 2
})

 

總結

其實這幾個方法之間的差別主要在屬性是否可可枚舉,是來自原型,仍是實例

 

方法 適用範圍 描述
for..in 數組,對象 獲取可枚舉的實例和原型屬性名
Object.keys() 數組,對象 返回可枚舉的實例屬性名組成的數組
Object.getPropertyNames() 數組,對象 返回除原型屬性之外的全部屬性(包括不可枚舉的屬性)名組成的數組
for..of 可迭代對象(Array, Map, Set, arguments等) 返回屬性值

 

 

參考

 

 

 

 

 

若是這篇文章對您有幫助,您能夠打賞我

 

技術交流QQ羣:15129679

相關文章
相關標籤/搜索