# What language constructions do you use for iterating over object properties and array items? (會使用甚麼方法來遍歷物件屬性跟陣列) ## 前情提要 ### JavaScript 物件屬性的特性 我們對物件屬性做 新增,修改,刪除,是靠著屬性內部有更細緻的『特性』,在預設情況下這些屬性的『三大特性』設定開啟,才可以如此輕鬆操作。 ```+ let person = { name: 'derek' }; person.name; // READ person.age = 28; // CREATE person.name = 'DEREK'; // UPDATE delete person.age; // DELETE ``` **💡Enumerable: 可以用 Object.keys(person) 將他們『列舉』出來 (READ) 💡Writable: 可以對屬性做『修改』(UPDATE) 💡Configurable: 有權限調整屬性的上述兩個特性。此外也可以用 delete 來移除某屬性(DELETE)** [三大特性介紹](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) ### Object.getOwnPropertyNames()& Object.keys() & Object.values() & Object.entries() **1.Object.keys()** * 一般來說我們關注的屬性皆為可列舉屬性(Enumerable),開發上常用 `Object.keys()`來遍歷。 ```+ let person = { name: 'derek' }; person.sex = 'male'; person['color'] = 'Red'; console.log(Object.keys(person)); // ['name', 'sex', 'color'] ``` **2.Object.getOwnPropertyNames()** * `Object.getOwnPropertyNames()` 會列出所有非繼承而來的屬性鍵,不論是否為 Enumerable ```+ let person = { name: 'derek' }; // 利用Object.defineProperty(obj, prop, descriptor)定義物件屬性 Object.defineProperty(person, 'age', { enumerable: false }); person.age = 28; console.log(Object.getOwnPropertyNames(person)); // [ 'name', 'age'] console.log(Object.keys(person)); // [ 'name' ] ``` **3.Object.values()** * `Object.values()` 取得 property value,並以陣列回傳。 ```+ let object = {a: 1, b: 2, c: 3}; console.log(Object.values(object)); // [1, 2, 3] ``` **4.Object.entries()** * `Object.entries()` 取得 property 的 key 和 value,並以陣列回傳。 ```+ let object = {a: 1, b: 2, c: 3}; console.log(Object.entries(object)); // [ // ["a", 1], // ["b", 2], // ["c", 3] // ] ``` ### 表格: | | 回傳屬性的 | 顯示不可列舉屬性 | 版本 | | -------------------------- | ------------------ | ------------------ | ---- | | Object.getOwnPropertyNames | keys | 是 | ES5 | | Object.keys() | keys | 否 | ES5 | | Object.values() | values | 否 | ES8 | | Object.entries() | [key, value] pairs | 否 | ES8 | ## JavaScript 常見的遍歷方法 1. for (let index = 0; index < array.length; index += 1) {} 1. for (const key in object) {} 1. for (const interator of object) {} 1. array.forEach((item, index, array) => {} ) ### **1.for** 作用對象:陣列 ```+ let arr = ['Eric', 'Allen', 'Owen']; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); // Eric 、 Allen 、 Owen } ``` for 迴圈非常實用,但若只用來遍歷陣列時,與其他方法相比較冗長。 ### **2.for...in** 作用對象:物件、陣列(不建議) 遍歷對象:鍵(key) ```+ let arr = ['Eric', 'Allen', 'Owen']; //遍歷陣列 for (const key in arr) { console.log(key); // 0 、 1 、 2 } let obj = { name: 'Danny', age: 26, height: 180, weight: 72, }; //遍歷物件 for (const key in obj) { console.log(obj[key]); // Danny 、 26 、 180 、 72 } ``` ### **3.for...of** 作用對象:陣列 遍歷對象:值(value) ```+ let arr = ['Eric', 'Allen', 'Owen']; //遍歷陣列 for (const value of arr) { console.log(value); // Eric 、 Allen 、 Owen } //遍歷陣列:搭配Array.prototype.entries()- ES6 新增 for (const iterator of arr.entries()) { console.log(iterator); // [ 0, 'Eric' ] 、 [ 1, 'Allen' ] 、 [ 2, 'Owen' ] } let obj = { name: 'Danny', age: 26, height: 180, weight: 72, }; //遍歷物件:搭配 Object.values() - ES8 新增 for (const value of Object.values(obj)) { console.log(value); // Danny 、 26 、 180 、 72 } //遍歷物件:搭配 Object.entries() - ES8 新增 for (const [key, value] of Object.entries(obj)) { console.log(key); // name 、 age 、 height 、 weight console.log(value); // Danny 、 26 、 180 、 72 } ``` ### **4.forEach** 作用對象:陣列 遍歷對象:鍵(key)、值(value)、作用對象(array) ```+ let arr = ['Eric', 'Allen', 'Owen']; //遍歷陣列 arr.forEach((item, index, array) => { console.log(item); // Eric 、 Allen 、 Owen console.log(index); // 0 、 1 、 2 console.log(array); // [ 'Eric', 'Allen', 'Owen' ] }); let obj = { name: 'Danny', age: 26, height: 180, weight: 72, }; //遍歷物件:搭配 Object.entries() - ES8 新增 Object.entries(obj).forEach((item) => { let [key, value] = item; console.log(key); // name 、 age 、 height 、 weight console.log(value); // Danny 、 26 、 180 、 72 }); ``` ## 使用情境: ### 是否遍歷自定義屬性和原型鏈上的屬性 > 遍歷陣列時,應該避免使用 for in,因為使用 for in 時,會連同自定義屬性和原型鏈上的屬性也一起遍歷(可以用obj.hasOwnProperty(property)事先檢查) > > 💡` <obj>.hasOwnProperty(<key>)`:只要是非繼承而來的屬性鍵都回傳 true,不論是否為 enumerable * **不會忽略自定義屬性和原型鏈上的屬性:for in** ```+ let arr = ['red', 'blue', 'yellow']; Array.prototype.inh = 'black'; for (let key in arr) { console.log(arr[key]); // red 、 blue 、 yellow 、 black if (arr.hasOwnProperty(key)) { console.log(arr[key]); // red 、 blue 、 yellow } } ``` * **會忽略自定義屬性和原型鏈上的屬性:for、for of、forEach** ```+ let arr = ['red', 'blue', 'yellow']; Array.prototype.inh = 'black'; arr.newPrototype = 'value'; /* --- for --- */ for (let i = 0; i < arr.length; i++) { console.log(arr[i]); // red 、 blue 、 yellow } /* --- for of --- */ for (const value of arr) { console.log(value); // red 、 blue 、 yellow } /* --- forEach --- */ arr.forEach((item) => { console.log(item); // red 、 blue 、 yellow }); ``` ### 陣列的空元素 > for in、forEach 遇到空元素會直接跳過,for、for of 則不會 * **跳過空元素:for in、forEach** ```+ let arr = ['red', , 'blue']; arr[4] = 'black'; /* --- for in --- */ for (const key in arr) { console.log(arr[key]); // red 、 blue 、 black } /* --- forEach --- */ arr.forEach((item) => { console.log(item); // red 、 blue 、 black }); ``` * **不會跳過空元素:for、for of** ```+ let arr = ['red', , 'blue']; arr[4] = 'black'; /* --- for --- */ for (let i = 0; i < arr.length; i += 1) { console.log(arr[i]); // red 、 undefined 、 blue 、 undefined 、 black } /* --- for of --- */ for (const value of arr) { console.log(value); // red 、 undefined 、 blue 、 undefined 、 black } ``` ### this 的指向 > 基本上都是指向外部的 window 物件(forEach例外) * **保留外部作用域:for、for in、for of** ```+ let arr = ['red']; /* --- for --- */ for (let i = 0; i < arr.length; i += 1) { console.log(this); // window } /* --- for in --- */ for (const key in arr) { console.log(this); // window } /* --- for of --- */ for (const value of arr) { console.log(this); // window } ``` * **指向特定的對象:forEach** >forEach 所指向的 this 是根據thisArg 參數所提供,如果 thisArg 參數未定義或為 null,this 將根據設定模式指向對應的對象,嚴謹模式下為 undefined,非嚴謹模式下為 window。 * 非嚴謹模式 ```+ let arr = ['red']; /* --- 定義 thisArg 參數 --- */ arr.forEach(function(item) { console.log(this); // 30 }, 30); /* --- 未定義 thisArg 參數 --- */ arr.forEach(function(item) { console.log(this); // window }); /* --- 箭頭函式 --- */ arr.forEach((item) => { console.log(this); // window }); ``` * 嚴謹模式 ```+ 'use strict'; let arr = ['red']; /* --- 定義 thisArg 參數 --- */ arr.forEach(function(item) { console.log(this); // 30 }, 30); /* --- 未定義 thisArg 參數 --- */ arr.forEach(function(item) { console.log(this); // undefined }); /* --- 箭頭函式 --- */ arr.forEach((item) => { console.log(this); // window }); ``` ### 中斷迴圈 * **中斷迴圈成功:for、for in、for of** ```+ let arr = ['red', 'blue', 'black']; /* --- for --- */ for (let i = 0; i < arr.length; i += 1) { if (arr[i] === 'blue') { break; } console.log(i); // 0 } /* --- for in --- */ for (const key in arr) { if (arr[key] === 'blue') { break; } console.log(key); // 0 } /* --- for of --- */ for (const [index, value] of arr.entries()) { if (value === 'blue') { break; } console.log(index); // 0 } ``` * **中斷迴圈失敗:forEach** > forEach 使用 break 會發生錯誤,使用 return 最多只能中斷當前遍歷項目,最後依然會遍歷後面的項目。 ```+ /* --- break --- */ let arr = ['red', 'blue', 'black']; arr.forEach((item, index) => { if (item === 'blue') { break; } console.log(index); // SyntaxError: Illegal break statement }); /* --- return --- */ let arr = ['red', 'blue', 'black']; arr.forEach((item, index) => { if (item === 'blue') { return; } console.log(index); // 0 、 2 }); ``` ### 表格: | | 作用對象 | 遍歷對象 | 是否遍歷自定義屬性和原型鏈上的屬性 | 是否跳過空元素 | this 的指向 | 中斷迴圈 | 版本 | | ------- | -------------- | ----------------- | ---------------------------------- | -------------- | -------------- | -------- | --- | | for | 陣列 | 無 | 否 | 否 | 保留外部作用域 | 可以 | ES1 | | for in | ~~陣列~~、物件 | key | 是 | 是 | 保留外部作用域 | 可以 | ES1 | | for of | 陣列 | value | 否 | 否 | 保留外部作用域 | 可以 | ES6 | | forEach | 陣列 | key、value、array | 否 | 是 | 指向特定的對象 | 不行 | ES5 | ## 結論 1. for...in 輸出的是屬性名稱(key),for...of 輸出的是值(value)。 1. 在遍歷物件屬性時,使用 for...in。 1. 在遍歷陣列時,使用 for...of,它比for迴圈簡潔,並且沒有for...in和forEach()那麼多奇怪的特例。 1. for 迴圈雖然冗長,但他比較彈性。 ~~每篇結論推崇的不一樣,方法沒有特別的好壞,依據當下的需求跟習慣做使用。~~ ## 補充:效能大比拚 測試方法:遍歷所有元素,存取裡的數值,然後看哪個方法速度最快。 測試結果:依序為快→慢 1.for 2.for...in 3.forEach 4.for...of [使用兩種資料測試這四種方法的效能實驗](https://tigercosmos.xyz/post/2021/06/js/js-array-for-methods/) ## 參考資料: [[筆記] 3 種 JavaScript 物件屬性的特性](https://medium.com/@liuderchi/%E7%AD%86%E8%A8%98-3-%E7%A8%AE-javascript-%E7%89%A9%E4%BB%B6%E5%B1%AC%E6%80%A7%E7%9A%84%E7%89%B9%E6%80%A7-3b982f4c5695) [JavaScript 之旅 (4):Object.keys() & Object.values() & Object.entries()](https://titangene.github.io/article/javascript-object-keys-values-entries.html) [ES proposal: Object.entries() and Object.values()](https://2ality.com/2015/11/stage3-object-entries.html) [JavaScript 容易混淆的遍歷方法](https://awdr74100.github.io/2019-11-28-javascript-traverse/) [Looping over Arrays: for vs. for-in vs. .forEach() vs. for-of](https://2ality.com/2021/01/looping-over-arrays.html)