iT邦幫忙

2024 iThome 鐵人賽

DAY 17
0
JavaScript

Don't make JavaScript Just Surpise系列 第 17

ES 6 的新資料結構與迭代器(Iterator)

  • 分享至 

  • xImage
  •  

作為一個開發者,遍歷一個字串,一個物件,一個陣列,確認裡面的屬性與值,是再常見不過的場景。
在 ES 6 以前,這些場景和 for 相關的語法總脫不了關係,比如

let str = "string"
let newStr = "";
for(let i = 0; i < str.length; i++) newStr += str[i] + ","
console.log(newStr);//"s,t,r,i,n,g,"

但為了應對更全面的場景,ES 6 推出了一些新的資料結構如:

  1. Map
    類似於 Object,主要處理鍵值對類型的資料,差異在 Map 的鍵可以為任意型別,而 Object 的鍵只能為字串或 Symbol。同時,Map 的鍵插入操作序會影響其中的儲存順序,Object 則是依自己的一套邏輯來排序鍵。此外,Map 提供了一些方便的屬性,如 size 等可以直接存取鍵數量的屬性。
    let demoMap = new Map();
    demoMap.set(1,'val1');
    demoMap.set({},'val2');
    console.log(demoMap.size);//2
    
  2. Set
    面試/演算法的 101 題,列出陣列中不重複的內容 - 在實際開發中也是個非常常見的場景。ES 6 推出的 Set 就是一個類似 Array,但保證內容不重複的陣列。同時因其底層實作更貼近使用雜湊表(Hash Table)的方式,在新增,插入,刪除,查找的時間複雜度接近 O(1)(陣列是 O(n)),效能更好。
    有個小缺點是 Set 不提供像陣列透過索引查詢值(arr[0])的方式,同時 Set 也不提供排序功能,需要有特定順序的集合時,還是得使用 array
    也常常會有使用 Setarray 做交互轉換的情況。

這兩種資料結構都基於 ES 6 推出的迭代器規範來實作的。
接下來,讓我們看看實作迭代器規範是怎麼一回事。

迭代器(Iterator)概念

如何遍歷一個可能含有多個值,多個屬性的資料結構,通常是一個資料結構的實作重點之一。
ES 6 的迭代器定義了一個介面,所有的迭代器物件都要符合這些規則和屬性。

迭代這個詞可能對比較沒看過的人有點拗口,迭代沒有上下文的時候指的是「交換替代」(Ref. 教育部國語辭典),用在數學和科學領域,這個詞一般指重複進行某個過程,且每次的操作都基於前一次的結果。

用資料集合的概念就是,每次做一樣的拿取行為,但資料結構自身會記憶訪問位置,每次訪問拿到移動後位置的值,且更新記憶的位置。透過不斷使用相同的拿取方法,可以依此方式不斷訪問取值直至迭代內容的終點。

來看看一個最基本的迭代器會長怎麼樣。(Code Source:MDN)

function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;

  const rangeIterator = {
    next() {
      let result;
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
        return result;
      }
      return { value: iterationCount, done: true };
    },
  };
  return rangeIterator;
}
//Use
const iter = makeRangeIterator(1, 10, 2);

let result = iter.next();
while (!result.done) {
  console.log(result.value); // 1 3 5 7 9
  result = iter.next();
}

console.log("Iterated over sequence of size:", result.value); // [5 numbers returned, that took interval in between: 0 to 10]

這是一個基本的迭代器,內部透過 nextIndex 記住了下一次 next() 方法訪問的對象(配合 step 定義如何更新指向位置)。
建構的時候通過 startend 定義迭代器的資料範圍,當 next() 訪問超過 end 的時候就是到達迭代範圍的盡頭。
(順便提一下,nextIndexiterationCount 即是透過閉包留存的,有注意到嗎?想了解更多關於閉包,請看 Day 12 的內容)

所以上面的例子最後印出來的結果會是 1 3 5 7 9,因為建構時設置 step 為 2,起始為 1,終點為 10。每次 +2,會在 9 停下。

一般來說,在實作迭代器的 next() 方法時,會回傳一個帶有兩個屬性的物件:{done:boolean, value: the value}
done 會回傳一個布林值,表示目前迭代器是否已到迭代範圍的終點,無法提供下一個值,value 則回傳這次呼叫 next() 時指向的值。
如果 donetrue,表示迭代已達終點,value 可不回傳或回傳 undefined(無論哪種,訪問時都會回傳 undefined)。

使用迭代器遍歷時,就是使用 next() 方法持續呼叫直到到達迭代範圍的終點。

可迭代物件(Iterables / Iterable object)

有個詞叫做可迭代物件(Iterables),指的是該物件具有能被迭代方法 for of 訪問的方式,如 ArrayMap 都有,但像 Object 就沒有。
要是一個可迭代物件,自身或原型鏈上的物件必須持有 Symbol.iterator 屬性,同時該屬性對應一個方法 [Symbol.iterator]() ,該方法需返回一個迭代器物件

這段敘述可以看出可迭代物件迭代器不同的東西,迭代器更像是一種物件的實作規範,可迭代物件是另一種物件實作規範,其中可迭代物件規範了其一個屬性必須要回傳一個迭代器物件。

JS 中的常見可迭代物件包含 StringArraySetMapArguments ... 等等,還有下一篇會提到的 Generator 都是可迭代物件。

我們以陣列為例:

let arr = [1,2,3,4,5];
let iterator = arr[Symbol.iterator]();
console.log(iterator.toString());//[object Array Iterator]
console.log(iterator.next());//{done: false,value: 1}
console.log(iterator.next());//{done: false,value: 2}

可以看到例子中的 arr[Symbol.iterator]() 返回的 iterator 物件就是一個迭代器,而 arr 是一個可迭代物件。

相對陣列而言,迭代器是一個更高層的概念,透過迭代器的規範,我們能更好地去訪問實作了迭代器規範的物件內容。

可枚舉(enumerable) v.s. 可迭代(iterable)

想要特別提一下有一個和「可迭代」聽起來很像,但其實不一樣的觀念「可枚舉」。
枚舉一詞指的是逐一列出枚舉物件可枚舉的所有屬性或方法的過程。
針對物件的屬性或方法,有一個屬性 enumerable 可以被設定,原型鏈上的方法一般預設為 false(大多討論枚舉的情況都是針對屬性,後面我會忽略方法,都簡稱為可枚舉屬性),一般情況下屬性則是預設為 true。可枚舉指的是當進行枚舉行為時會列出來的東西,即 enumerabletrue 的屬性。

class Human {
	constructor( name ) {
		this.name = name;
	}
	hello() {
		console.log(`${this.name} says Hello`);
	}
}
class Classmate extends Human {
    constructor(name, studentId) {
        super(name); //使用 Human 的建構方法建構
        this.studentId = studentId;
    }
    showId() {
        console.log(`My student Id is ${this.studentId}`);
    }
}

let friend2 = new Classmate('Ryu','10000');
console.log(friend2.propertyIsEnumerable('name')); // true
console.log(friend2.propertyIsEnumerable('hello')); // false
console.log(friend2.propertyIsEnumerable('showId')); // false

for in 就是一個基於可枚舉實現的方法,當一個物件是可枚舉的,就可以使用 for in 來遍歷他的可枚舉屬性。
可枚舉的物件不一定可迭代,可迭代的物件也不一定可枚舉。

  1. 可枚舉不可迭代,如 Object
    let obj = {k1:'val1', k2:'val2'};
    for (let key in obj) {
    console.log(key);//k1, k2
    }
    for (let value of obj) {
    console.log(value); //TypeError: obj is not iterable
    }
    
  2. 可迭代不可枚舉,如 Set
    let set = new Set([1,2,3]);
    for (let value of set) {
        console.log(value); //1 2 3
    }
    
    for (let key in set) {//不會報錯,但也不會執行,不會印出任何值
        console.log(key); 
    }
    
  3. 可枚舉也可迭代,如 Array
    let arr = ['a','b','c'];
    for (let value of arr) {
        console.log(value); //"a" "b" "c"
    }
    
    for (let key in arr) {
        console.log(key); //0 1 2
    }
    

通常討論可枚舉跟可迭代就是以 for infor of 來舉例。
可以看到 for in 對應可枚舉,且以回傳索引為主,for of 則是回傳值、對應可迭代。

針對陣列的訪問,會更建議使用 for of 而非 for in,因為 for in 會列出所有可枚舉對象,而 for of 則只會列出可迭代對象,更貼近我們一般遍歷陣列內容時希望的場景。

const arr = [10, 20, 30];
arr.foo = "bar";//非陣列索引的一環,附在陣列上的屬性

for (let key in arr) {
  console.log(key); // "0" "1" "2" "foo"
}

for (let val of arr){
	console.log(val);//10 20 30
}

那些基於 Iterator 標準實作的方法

一旦一個物件實作了迭代器規範,則表示該物件能夠使用下列函式 / 運算子。

  • for of
    上面提到的,也是可迭代物件的定義。
    需要能夠執行 for of 來遍歷才是一個可迭代物件。

  • 展開運算子(...)

    let a = [1, 2, 3];
    let b = [0,...a]
    console.log(b);//[0, 1, 2, 3]
    
  • 解構運算子(let [a,b] = [1,2])
    要順便提到的是除了可迭代物件能使用解構運算子之外,一般物件也能使用,但兩者的原理是不同的。
    可迭代物件是基於他本身的可迭代性,依其順序進行迭代提值進行解構;物件則是針對鍵值對進行解構,解構出來的必須要對應鍵的名稱,與順序無關。

    //可迭代的解構
    let set = new Set([1, 2, 3, 4]);
    let [first, second] = [...set];
    console.log(first);  // 1
    console.log(second); // 2
    
    //一般物件的鍵值對解構
    let obj = {foo:'foo', bar:'bar'};
    let {foo, bar} = obj;
    console.log(foo);//"foo"
    console.log(bar);//"bar"
    
  • Array.form()
    用於將 類陣列物件(Array-Like) 或 可迭代物件(Iterables) 轉為陣列的靜態方法。
    類陣列物件又與可枚舉,可迭代兩詞有出入,類陣列的先決條件是:

    1. 具 length 屬性
    2. 可以透過索引值訪問元素

    因為只是類陣列,類陣列也不保證能夠使用陣列的方法。總是是一個額外的類別定義。

    總之類陣列物件或可迭代物件都可以使用這個方法來轉為陣列,轉換完便能夠使用陣列的那些方法,也具有陣列的那些屬性。

    let set = new Set([3, 1, 2, 3, 3]);
    console.log(set[0]);//undefined,Set 不是一個枚舉物件
    
    let arr = Array.form(set);
    console.log(arr[0]);//3,陣列是可枚舉物件,可透過索引訪問元素
    console.log(arr);//[3, 1, 2]
    

有了迭代器的觀念,我們可以接著討論生成器(Generator)和相關的語法了。


上一篇
異步(Async)中的Promise 物件
下一篇
生成器(Generator)
系列文
Don't make JavaScript Just Surpise31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言