【JS學習筆記】認識 for...in 與他的兄弟 for...of


Posted by Annie-Chien on 2022-12-08

我常常把 for...in 與 for...of 相互混淆,所以決定寫一篇文章來好好認識他們兩位!


嗨,for...in

for...in 可以用來針對 object 內屬性進行迴圈。

const obj = {
  a: 'apple',
  b: 'banana',
  c: 'cat',
  d: 'dog',
  e: 'egg',
};

for (let key in obj) {
  console.log(key); //a,b,c,d,e
}

看程式碼說故事:

  1. 先宣告一個 looping variable(如範例中的 key),變數可依自己喜好命名。這個 looping variable 代表著 object 內的 key 值。
  2. in 後面放上欲進行迴圈的 object(如範例中的 obj)。
  3. console.log(key) 依序印出 obj 內的 key 值。

你說什麼?你想要的不是 key,而是 value 啊?
簡簡單單~善用 square bracket notation 就能輕鬆辦到囉!

for (const key in obj) {
  console.log(obj[key]); //apple, banana, cat, dog, egg
}

基本上,for...in 時取出的 key 值的順序會根據原本物件內屬性的撰寫順序。但凡事總有例外,請看...

const orderedObj = {
  '1': 'one',
  '10': 'ten',
  '5': 'five',
  '6': 'six',
  '2': 'two',
};

for (let key in orderedObj) {
  console.log(key); // 結果竟然是 1,2,5,6,10!!
}

結果出乎意料竟然沒有按照物件內屬性的撰寫順序(1, 10, 5, 6, 2),而是被刻意地「由小到大」排列!

原因就出在 integer properties,它指的是「字串被轉型成整數時保持不變,且反之亦然」。字串 1 轉換成整數數字時為數字 1,字串 10 轉換成整數數字時為數字 10,但字串 1.2 轉換成整數數字則是數字 1,並不屬於 integer property。

簡單來說,當物件內的 key 值是字串整數或是數字整數,for...in 就會貼心地幫你由小到大排列(貼心嗎?我可不這麼認為 😟

for...in 與陣列(但最好別這麼做)

其實 for...in 也可以使用在陣列身上,畢竟陣列也算是物件的一種,取出的就會是陣列的 index。

const arr = ['a', 'b', 'c', 'd', 'e'];
for (let item in arr) {
  console.log(item); // 0,1,2,3,4
}

雖然不會報錯,但建議不要這麼做。原因有二:

  1. for...in 會輸出從 Array.prototype 所繼承的屬性。
    const arr = ['a', 'b', 'c', 'd', 'e'];
    Array.prototype.oops = 'See you again;)';
    for (let item in arr) {
    console.log(item); // 0,1,2,3,4,oops
    }
    
    除了印出陣列 arr 內的所有 index 之外,也印出了我們在 Array.prototype 加入的屬性 oops。
  2. for...in 當初就是專門為了物件進行迴圈所創,不是陣列。因此使用在陣列上,效能會較差。

要讓陣列跑迴圈還有很多其他更適合的方式!例如我們接下來要介紹的 for...of。

你好,for...of

for...of 用來針對 iterable object (可迭代物件),如陣列,進行迴圈,依序取出元素。

const colorsArr = ['pink', 'yellow', 'black'];
for (let color of colorsArr) {
  console.log(color); //pink, yellow, black
}

看程式碼說故事:

  1. 宣告一個 looping variable(如範例中的 color),變數可依自己喜好命名。這個 looping variable 代表著可迭代物件內的元素。
  2. of 後面放上欲進行迴圈的 iterable object(如範例中的 colorsArr)。
  3. console.log(color) 依序印出 colorsArr 內的的元素。

什麼是 iterable object (可迭代物件)?

iterable object 都擁有一個叫做 [@@iterator]() 的 method(其實長這樣:Symbol.iterator)。除了 Array 之外,String, TypedArray, Map, Set, NodeList, HTMLCollection 都屬於可迭代物件。

for...of 背後做了什麼事

When a for...of loop iterates over an iterable, it first calls the iterable's @@iterator method, which returns an iterator, and then repeatedly calls the resulting iterator's next() method to produce the sequence of values to be assigned to variable. --MDN

執行 for...of 時,發生了下列的事情:

  1. 呼叫 Symbol.iterator,而 Symbol.iterator 會回傳一個 iterator(迭代器)。
  2. 接著,這個迭代器會不斷地重複呼叫一個迭代器內內建的方法,叫做 next()。
  3. next() 會回傳一個物件,長得像這樣 { value: "XXX", done: true },重點在於它會有 value 和 done 兩個屬性。
    • done: 布林值,true 代表說已經迭代到底,沒有下一個值可以輸出了,false 則表示還有值可以繼續輸出。
    • value: 迭代器所回傳的值,這個值會被賦予給我們在 for...of 裡宣告的 looping variable。

覺得好虛幻嗎?我們實際把它印出來看看吧!

const colorsArr = ['pink', 'yellow', 'black'];
const iterator = colorsArr[Symbol.iterator](); //把迭代器儲存在變數中

//手動呼叫迭代器的 next() method

console.log(iterator.next()); //{ value: 'pink', done: false }
console.log(iterator.next()); //{ value: 'yellow', done: false }
console.log(iterator.next()); //{ value: 'black', done: false }
console.log(iterator.next()); //{ value: undefined, done: true }

在 done 為 true 的時候就代表迭代器已經完成工作了,因此也停止了 for...of 迴圈。

for...of 與物件

物件不是可迭代物件(物件沒有 [@@iterator]() method),因此 for...of 是無法應用在物件上的,會報錯。

// ☠️☠️☠️ 錯誤用法 ☠️☠️☠️
const obj = {
  a: 'apple',
  b: 'banana',
  c: 'cat'
};

for (let item of obj) {
  console.log(item);
}
// TypeError: obj is not iterable

雖然物件不是可迭代物件,但既然我們已經知道可迭代物件的魔法就在於 Symbol.iterator,我們是不是就能把物件變成可迭代物件,就能使用 for...of 了呢?
欸...說的是沒有錯啦...(捲袖子捲捲捲)那就來試試看吧!

我們現在有一個物件長這樣:let range = {from: 1, to: 5},希望可以把 range 變成可迭代物件,使用 for...of 依序印出 1, 2, 3, 4, 5。

const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]: function () {
    this.current = this.from; 
    return {
      next: () => ({
        done: this.current > this.to,
        value: this.current++,
      }),
    };
  },
};

for (let value of range) {
  console.log(value); //見證奇蹟的時刻:1,2,3,4,5 🎉
}

看程式碼說故事:

  1. 在 range 物件內加入 [Symbol.iterator] method。
  2. 我們在 range 裡面新增一個屬性 current,用來記錄迭代器回傳的值。
  3. [Symbol.iterator] 回傳一個物件,也就是迭代器,當中包含 next() method。
  4. next() 回傳一個物件,內含有 done 和 value 兩個屬性。
    4.1 done: 當 current 大於 range.to 就代表已經迭代到底了
    4.2 value: 每一次執行 next() 都替 current + 1,來達到我們要從數字 to 依序列印到數字 from 的目標

重點回顧

for...in for...of
使用對象 物件(陣列也 OK) 可迭代物件(如 Array, Map, Set, String)
做什麼 取得物件的 key 取得可迭代物件的 value
注意事項 使用在陣列時,會取出 index 物件不是可迭代物件,故無法使用

還在努力學習中,如有錯誤請不吝指正 🙇🏻‍♀️

參考資料

https://javascript.info/object#forin
https://javascript.info/array#loops
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of


#javascript







Related Posts

Web開發學習筆記18 — 開始使用Node.js、npm

Web開發學習筆記18 — 開始使用Node.js、npm

與 DDoS 奮戰:nginx, iptables 與 fail2ban

與 DDoS 奮戰:nginx, iptables 與 fail2ban

Python Monkey Patch 入門教學

Python Monkey Patch 入門教學


Comments