iT邦幫忙

2022 iThome 鐵人賽

DAY 6
1
Modern Web

前端蛇行撞牆記系列 第 6

Day6 前端蛇行撞牆記 - 迭代(iteration)與她的姐妹們

  • 分享至 

  • xImage
  •  

前言

標題會這樣下是因為光是講迭代這個大標題一定會提到可迭代協議、迭代器、迭代器協議等等,所以仿造一個舞台劇的劇名感XD

當初會想研究迭代是因為我發現 array有 iterable protocol所以可以被迭代,擁有 iterable protocol就等於有了[Symbol.iterator],但 iterator protocol也是因為有了[Symbol.iterator],誒誒那不是等於array就是iterator嗎?可是卻無法直接使用next()方法?

研究的時候覺得天啊真是個大坑啊~~但就是遇到了就遇到了,愛上了就愛上了
就要研究個清楚啊!

為什麼會選擇這個「迭代」?
迭代、疊代有兩種寫法,一直在猶豫要用哪種寫法,後來參考這篇文章:請問到底是在「疊代」或是在「迭代」?
"「疊代」:累進取代,不斷重複進行後者加上前者、替換掉前者的動作;"
"「迭代」:替換取代,表示幾個固定的物件彼此輪替取代的動作;"
我們說的迭代應該比較符合「迭代」吧!

第一幕:迭代(iteration)

迭代有好幾種方法,讓我們來慢慢介紹:

for...in

在ES6之前,所有的object都可以用for...in來迭代。
如果是陣列就會取到 index 值,如果是物件就會取到 key 值。

陣列如果擁有 index值也同樣可以拿到 value:

let array = [2,3,4,5,6,7]

for(const index in array) {
  console.log(array[index]);
}
//2
//3
//4
//5
//6
//7

物件擁有了 key值,也可以拿到 value:

let obj = { name:"Jade", age: 28, city: "taipei" }

for(let key in obj) {
    console.log(obj[key])
}
// Jade
// 28
// taipei

for...of

然而ES6出了一個新的方法叫做for...of,用在陣列身上的話會直接取到 value,不用再用 index的方式去找。

然而這個方法卻不能用在物件的身上。
why?
讓我們直接來試試看物件:

const obj = { name:"Jade", age: 28, city: "taipei" }

for(let value of obj) {
  console.log(value)
}

// TypeError: obj is not iterable

發現真的無法用在物件身上,但我們得到了一個新的詞 iterable
所以物件不是iterable,但陣列是iterable 是因為陣列有iterable protocol可迭代協議

可迭代協議(iterable protocol)

Array.prototype有個很奇妙的東西長這樣:Array.prototype[@@iterator](),這一段其實是等於可以被這樣使用:

array[Symbol.iterator]()

意思是陣列擁有Symbol.iterator屬性的方法可以去使用。而且擁有了這個屬性,就等於有可迭代協議(iterable protocol) 了。

來試試看有加array[Symbol.iterator]()的效果

var array = [1,2,3,4];
var array1 = array[Symbol.iterator]();

for (let value of array1) {
  console.log(value);
}
//1
//2
//3
//4

跟直接使用的效果一樣:

var array = [1,2,3,4];

for (let value of array) {
  console.log(value);
}
//1
//2
//3
//4

再回到上一篇文章,為什麼只有陣列有,物件沒有,我想跟儲存方式有很大的關係:

  • 陣列:
    • 循序式容器 ,每個 value 的背後是有順序的 index。
    • 可以直接用 for...of 去拜訪每一個 index所對應的 value。
  • 物件:
    • 關聯式容器key儲存的位置是沒有順序排列的,所以無法「照順序」去拜訪 key對應的value,因為根本也不知道從哪裡開始,所以就沒有可迭代協議。

物件沒辦法直接被遍歷 value 是因為陣列跟物件儲存的方式就不一樣,所以沒辦法用for...of去迭代 value。

不過object還是可以透過我們替他加上 [Symbol.iterator]() 方法(就是讓他變成迭代器),讓我們去替他定義要從哪裡開始遍歷 value。

第二幕:迭代器(iterator)

現在我們知道了iteration, iterable protocol,現在要來介紹 iterator。

維基百科:迭代器(iterator),是確使使用者可在容器物件(container,例如連結串列或陣列)上遍訪的物件。

呈上一個,只有陣列有[Symbol.iterator]()方法,所以就直接以陣列來講了。

陣列

在閱讀陣列方法的時候,發現這些方法回傳值會是 「Array Iterator object」

  • array.prototype.keys()
  • array.prototype.entries()
  • array.prototype.values()

這次用array.prototype.entries()來試試看會發生什麼事:

const arr = ['j', 'a', 'd', 'e'];
const ArrayIterator = arr.entries();

console.log(ArrayIterator);
// Object [Array Iterator] {}

居然變數ArrayIterator已經變成一個迭代器物件了,所以不能像是object.entries()直接幫你列出來,就要使用iterator內建的 next() 方法,把值印出來。

迭代器協議(iterator protocol)

而使用next()方法就是產生了一個 iterator protocol(迭代器協議)

他要使用next()方法才能回傳物件包含value、done的屬性:
{value: current value , done: true/ false }

正確使用array.entries()的方式

const arr = ['j', 'a', 'd', 'e'];
const iterator = arr.entries();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

//{ value: [ 0, 'j' ], done: false }
//{ value: [ 1, 'a' ], done: false }
//{ value: [ 2, 'd' ], done: false }
//{ value: [ 3, 'e' ], done: false }
//{ value: undefined, done: true } 表示遍歷結束

第三幕: iterable protocol v.s iterator protocol

這裡是當初我最不懂的地方,後來在MDN iterable protocol看到這段:

...Whenever an object needs to be iterated (such as at the beginning of a for...of loop), its @@iterator method is called with no arguments, and the returned iterator is used to obtain the values to be iterated.

(點下中文翻譯)

...每當物件需要被迭代時(比如在一個開始的 for..of 迴圈中),物件的 @@iterator 方法會被以不傳入引數的方式呼叫,並會使用其回傳的**迭代器(iterator)**來獲得被迭代出來的值。

於是我困惑的結局就是:
雖然陣列有iterable protocol(可迭代協議),不等於他可以直接使用next(),只是在迭代的當下會自動轉換成要用next()才能回傳的的value來回傳,所以我們就只會看到 value而已。

所以現在做了const iterator = arr.entries()就是把他真的變成了迭代器,就必須按照迭代器的next()方法才能拿到value。

const arr = ['j', 'a', 'd', 'e'];
const iterator = arr[Symbol.iterator]();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

//
{ value: 'j', done: false }
{ value: 'a', done: false }
{ value: 'd', done: false }
{ value: 'e', done: false }
{ value: undefined, done: true }

這個就是迭代器出來的樣子。

當陣列沒有直接使用[Symbol.iterator](),還是可以回傳使用[Symbol.iterator]()才能有的value!但如果我想使用next()並想知道什麼時候done,就要真的把陣列變成迭代器才做得到了。

總結

  • 物件無法被for...of迭代。
  • 陣列擁有iterable protocol,是因為原型練有[Symbol.iterator]()屬性,所以只要使用for...of要迭代的話就會要他去做本來要用next()才能回傳的value。
  • 直接使用[Symbol.iterator]()屬性的話就可以使用next()來找到下一個值,現在就等於有了iterator protocol

如果還沒有很懂的話可以去看MDN的Iteration protocols

什麼?跟我說 Iterator 還可以講到 Generator?
先讓我喘口氣我再考慮看看...

打完元氣大傷,謝謝大家~明天見!


參考資料:
[筆記] 談談 JavaScript 中 for ... in 這個 function
Iterator 和 for...of 循环
Iteration protocols
JavaScript iterators and generators: A complete guide
JavaScript ES6 Iterables/Iterators 迭代器
迭代器(Iterator)
What does @@ ("at at") mean in ES6 JavaScript?


上一篇
Day5 前端蛇行撞牆記 - 你有聽過循序式容器 / 關聯式容器嗎?
下一篇
Day7 前端蛇行撞牆記 - 親愛的 forEach()
系列文
前端蛇行撞牆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言