深入JS物件的遍歷
概述
在Javascript程式設計時,經常需要遍歷物件的鍵、值,ES5提供了for...in用來遍歷物件,然而其涉及物件屬性的“可列舉屬性”、原型鏈屬性等,總會讓人多少摸不著頭腦。
本文將由Object物件本質探尋各種遍歷物件的方法,並區分常用方法的特點。
本文所提的物件,特指Object的例項,不包含Set、Map、Array等資料集物件。
剝開Object的“偽裝”
Javascript的物件,每一個屬性都有其“屬性描述符”,主要有兩種形式:資料描述符和存取描述符。
可以通過 Object.getOwnPropertyDescriptor
與 Object.getOwnPropertyDescriptors
兩個方法獲取物件的屬性描述符。
以下通過示例說明:
var obj = {
name: '10',
_age: 25,
get age(){
return this._age;
},
set age(age){
if(age<1){
throw new Error('Age must be more than 0');
}else{
this._age = age;
}
}
};
var des = Object.getOwnPropertyDescriptors(obj);
console.log(des);
/**
* des: {
* name: {
* configurable: true,
* enumerable: true,
* value: "10",
* writable: true,
* __proto__: Object
* },
* _age: {
* configurable: true,
* enumerable: true,
* value: 25,
* writable: true,
* __proto__: Object
* },
* age: {
* configurable: true,
* enumerable: true,
* get: f age(),
* set: f age(age),
* __proto__: Object
* },
* __proto__: Object
* }
*/
可以看到,
- name、_age擁有
'configurable'
、'enumerable'
、'value'
、'writable'
四個屬性描述符,統稱資料描述符 - age擁有
'configurable'
、'enumerable'
、'get'
、'set'
四個屬性描述符,統稱存取描述符
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
資料描述符 | Yes | Yes | Yes | Yes | No | No |
存取描述符 | Yes | Yes | No | No | Yes | Yes |
物件的屬性描述符,可以通過Object.defineProperty
和Object.defineProperties
來修改(configurable
為true
的條件下)
詳細內容可以參考:MDN手冊 Object.defineProperty
瞭解了這個之後,與今天主題相關的,也就是 'enumerable'
這個屬性描述符啦,其值為 true
時,我們稱其為“可列舉的”,屬性是否可列舉影響了我們在使用原生方法遍歷物件時的結果,在本文後面,將詳細說明。
掌握屬性描述符,無論是對自己以後的程式碼編寫,還是學習開源框架原始碼,都是十分基礎而且重要的,在此處僅作介紹,並著重關注與本文主題相關的屬性。
常用遍歷方法
for..in..遍歷
遍歷自身及原型鏈上所有可列舉的屬性
示例程式碼:
var Person = function({name='none', age=18, height=170}={}){
this.name = name;
this.age = age;
this.height = height;
}
Person.prototype = {
type: 'Animal'
}
var qiu = new Person()
// 將height屬性設定為 不可列舉
Object.defineProperty(qiu, 'height', {
enumerable: false
})
for(let n in qiu){
console.log(n);
}
// output: name age type
如以上程式碼所示,使用for..in..遍歷,會將物件自身及其原型鏈上的所有可列舉屬性全部遍歷出來。
而往往我們並不需要將原型鏈上的屬性也遍歷出來,因此常常需要如下處理:
for(let n in qiu){
// 判斷是否例項自身擁有的屬性
if(qiu.hasOwnProperty(n)){
console.log(n)
}
}
因為for..in..在執行的時候,還進行了原型鏈查詢,當只需要遍歷物件自身的時候,效能上會收到一定影響。
Object.keys遍歷
返回一個陣列,包括物件自身的(不含繼承的)所有可列舉屬性
示例程式碼:
var Person = function({name='none', age=18, height=170}={}){
this.name = name;
this.age = age;
this.height = height;
}
Person.prototype = {
type: 'Animal'
}
var qiu = new Person()
// 將height屬性設定為 不可列舉
Object.defineProperty(qiu, 'height', {
enumerable: false
})
var keys = Object.keys(qiu);
console.log(keys)
// output: ['name', 'age']
通過上述程式碼,我們可以看到,Object.keys僅遍歷物件本身,並將所有可列舉的屬性組合成一個陣列返回。
在很多情況下,其實我們需要的,也就是這樣一個功能。
例如以下,將鍵值型別的查詢param轉換成url的query,不僅程式碼量少、邏輯清晰,而且可以通過鏈式的寫法使得整體更加優雅。
const searchObj = {
title: 'javascript',
author: 'Nicolas',
publishing: "O'RELLY",
language: 'cn'
}
let searchStr = Object.keys(searchObj)
.map(item => `${item}=${searchObj[item]}`)
.join('&');
let url = `localhost:8080/api/test?${searchStr}`
遍歷鍵值對的資料時,使用Object.keys真是不二之選。
Object.getOwnPropertyNames遍歷
返回一個陣列,包含物件自身(不含繼承)的所有屬性名
示例程式碼:
var Person = function({name='none', age=18, height=170}={}){
this.name = name;
this.age = age;
this.height = height;
}
Person.prototype = {
type: 'Animal'
}
var qiu = new Person()
// 將height屬性設定為 不可列舉
Object.defineProperty(qiu, 'height', {
enumerable: false
})
var keys = Object.getOwnPropertyNames(qiu);
console.log(keys)
// output: ['name', 'age', 'height']
與Object.keys的區別在於Object.getOwnPropertyNames會把不可列舉的屬性也返回。除此之外,與Object.keys的表現一致。
說好的for..of..,為什麼無效
在ES6中新增了迭代器與for..of..的迴圈語法,在陣列遍歷、Set、Map的遍歷上,十分方便。然而當我應用在物件(特指Object的例項 )上時(如下程式碼),瀏覽器給我拋了一個異常:Uncaught TypeError: searchObj is not iterable
。
const searchObj = {
title: 'javascript',
author: 'Nicolas',
publishing: "O'RELLY",
language: 'cn'
}
for(let n of searchObj){
console.log(n)
}
// Uncaught TypeError: searchObj is not iterable
沒錯...這是一個錯誤的演示,在ES6中,物件預設下並不是可迭代物件,表現為其沒有[Symbol.iterator]屬性,可以通過以下程式碼對比:
const searchObj = {
title: 'javascript',
author: 'Nicolas'
};
const bookList = ['javascript', 'java', 'c++'];
const nameSet = new Set(['Peter', 'Anna', 'Sue']);
console.log(searchObj[Symbol.iterator]); // undefined
console.log(bookList[Symbol.iterator]); // function values(){[native code]}
console.log(nameSet[Symbol.iterator]); // function values(){[native code]}
// 注,Set、Map、Array的[Symbol.iterator]都是其原型物件上的方法,而非例項上的,這點需要注意
而for..of..迴圈,實際上是依次將迭代器(或任何可迭代的物件,如生成器函式)的值賦予指定變數並進行迴圈的語法,當物件沒有預設迭代器的時候,當然不可以進行迴圈,而通過給物件增加一個預設迭代器,即[Symbol.iterator]屬性,就可以實現,如下程式碼:
Object.prototype[Symbol.iterator] = function *keys(){
for(let n of Object.keys(this)){ // 此處使用Object.keys獲取可列舉的所有屬性
yield n
}
}
const searchObj = {
title: 'javascript',
author: 'Nicolas',
publishing: "O'RELLY",
language: 'cn',
};
for(let key of searchObj){
console.log(key)
}
// output: title author publishing language
以上程式碼確實獲得了物件的所有鍵名,在生成器函式內,我們使用的是Object.keys獲得所有可列舉的屬性值,然而這並不是所有人都期望的,也許小明期望不可列舉的屬性值也被遍歷,而小新可能連[Symbol.iterator]也希望遍歷出來,於是,這裡產生了一些分歧,如何遍歷有以下幾種因素:
總結起來,物件的property至少有三個方面的因素:
- 屬性是否可列舉,即其 enumerable屬性描述符 的值;
- 屬性的型別,是字串型別、還是Symbol型別;
- 屬性所屬,包含原型,還是僅僅包含例項本身;
鑑於各方意見不一,並且現有的遍歷方式可以滿足,於是標準組沒有將[Symbol.iterator]加入。
關於ES6迭代器、生成器的更多知識,可以參考:ES6中的迭代器(Iterator)和生成器(Generator)
相關文章
- JS 物件的遍歷JS物件
- JS遍歷物件的方式JS物件
- js物件遍歷順序JS物件
- JS遍歷物件的幾種方法JS物件
- JS中遍歷陣列、物件的方式JS陣列物件
- JS遍歷物件屬性的7種方式JS物件
- js的map遍歷和array遍歷JS
- JS中陣列與物件的遍歷方法例項JS陣列物件
- JS中的遍歷JS
- vue遍歷map物件Vue物件
- 遍歷陣列物件陣列物件
- js遍歷多重json的方法JSON
- ES6遍歷物件物件
- Unity遍歷物件serialized的屬性Unity物件Zed
- 跋山涉水 —— 深入 Redis 字典遍歷Redis
- JS遍歷方法總結JS
- js 遍歷陣列方式JS陣列
- js使用經驗--遍歷JS
- Java遍歷Map物件的四種方式Java物件
- JavaScript遍歷物件方法總結JavaScript物件
- Python字典的遍歷,包括key遍歷/value遍歷/item遍歷/Python
- JS筆記(2) JS中的迴圈遍歷JS筆記
- 深度優先遍歷,廣度優先遍歷實現物件的深拷貝物件
- js可列舉屬性的遍歷JS
- 遍歷物件和陣列的方法總結物件陣列
- 遍歷陣列和物件的方法都有哪些?陣列物件
- 遍歷物件鍵值對的兩種方法物件
- 【Java中遍歷Map物件的4種方法】Java物件
- js遞迴遍歷講解JS遞迴
- js資料處理——遍歷JS
- 遍歷方法 js jquery 我 常用JSjQuery
- JavaScript遍歷物件屬性順序JavaScript物件
- JS 基礎篇(七):JS中的遍歷函式JS函式
- js實現深度優先遍歷和廣度優先遍歷JS
- 深入深入再深入 js 深拷貝物件JS物件
- js 跳出迴圈/結束遍歷的方法JS
- JS中陣列的遍歷方法(3種)JS陣列
- Java中如何遍歷Map物件的4種方法Java物件
- JS迴圈遍歷方法總結JS