Iterator:訪問數據集合的統一接口

導語

遍歷器 Iterator是 ES6 爲訪問數據集合提供的統一接口。任何內部部署了遍歷器接口的數據集合,對於用戶來說,都可以使用相同方式獲取到相應的數據結構。如果使用的是最新版 Chrome瀏覽器,那麼你要知道——我們所熟悉的數組小姐,已悄悄的打開了另一扇可抵達她心扉的小徑。

1 正題

某個數據集合部署了 Iterator接口,是指其 Symbol.iterator屬性指向一個能返回 Iterator接口的函數。任何默認使用遍歷器訪問數據集合的方法,都會調用此屬性以得到遍歷器對象,再按照設定的順序依次訪問該數據結構的成員(關於 Symbol.iterator請看最後一節的延伸閱讀)。比如原生數組的遍歷器爲 [][Symbol.iterator],也可以直接通過其構造函數的原型獲取 Array.prototype[Symbol.iterator]。

1.1 基本行爲

調用 Iterator接口會返回一個新的遍歷器對象(指針對象)。 對象中必然有 next方法,用於訪問下一個數據成員。指針初始時指向當前數據結構的起始位置。

第一次調用對象的 next方法,指針指向數據結構的第一個成員。 第二次調用對象的 next方法,指針指向數據結構的第二個成員。 不斷的調用對象的 next方法,直到它指向數據結構的結束位置。

每次調用 next方法,都會返回相同的數據結構: { value, done }。 其中 value表示當前指向成員的值,沒有則爲 undefined。 其中 done是一個布爾值,表示遍歷是否結束,結束爲 true,否則 false。

遍歷器接口的標準十分簡潔,不提供諸如:操作內部指針、判斷是否有值等等方法。只需要一直不斷的調用 next方法,當 done爲 false時獲取當時的 value, done爲 true時停止即可。第一次接觸遍歷器的行爲模式是在 2016 的冬天,那時底蘊不夠雞毛也沒長全,理解不了簡潔性的適用和強大。直到現在——在即將打包被迫離開公司的前夕才驀然的醒覺。多麼痛的領悟啊。

  1. let iterator = [1, 2, 3][Symbol.iterator]();
  2. console.log( iterator.next() ); // {value: 1, done: false}
  3. console.log( iterator.next() ); // {value: 2, done: false}
  4. console.log( iterator.next() ); // {value: 3, done: false}
  5. console.log( iterator.next() ); // {value: undefined, done: true}

1.2 簡單實現

面向不同的數據結構,有不同的遍歷器實現方法,我們簡單的實現下數組的遍歷器方法。

  1. let res = null;
  2. let iterator = myIterator([3, 7]);
  3. console.log( iterator.next() ); // {value: 3, done: false}
  4. console.log( iterator.next() ); // {value: 7, done: false}
  5. console.log( iterator.next() ); // {value: undefined, done: true}
  6. function myIterator(array = []) {
  7. let index = 0;
  8. return {
  9. next() {
  10. return index < array.length
  11. ? { value: array[index++], done: false }
  12. : { value: undefined, done: true };
  13. }
  14. };
  15. }

1.3 return & throw

除了爲遍歷器對象部署 next方法,還可以有 return和 throw方法。其中 return方法會在提前退出 for of循環時(通常是因爲出錯,或觸發了 break語句)被調用。而 throw方法主要是配合 Generator函數使用,一般的遍歷器對象用不到這個方法,所以不予介紹。

  1. let obj = {
  2. [Symbol.iterator]() {
  3. let index = 0;
  4. let array = [1, 2, 3];
  5. return {
  6. next() {
  7. return index < array.length
  8. ? { value: array[index++], done: false }
  9. : { value: undefined, done: true };
  10. },
  11. return() {
  12. console.log('Trigger return.');
  13. return {};
  14. }
  15. };
  16. }
  17. };
  18. for (let v of obj) {
  19. console.log(v); // 打印出:1, 2, 3,沒觸發 return 函數。
  20. }
  21. for (let v of obj) {
  22. if (v === 2) break;
  23. console.log(v); // 打印出:1,之後觸發 return 函數。
  24. }
  25. for (let v of obj) {
  26. if (v === 3) break;
  27. console.log(v); // 打印出:1, 2,之後觸發 return 函數。
  28. }
  29. for (let v of obj) {
  30. if (v === 4) break;
  31. console.log(v); // 打印出:1, 2, 3,沒觸發 return 函數。
  32. }
  33. for (let v of obj) {
  34. if (v === 2) throw Error('error');
  35. console.log(v); // 打印出:1,之後觸發 return 函數,並報錯停止執行。
  36. }

2 原生支持

2.1 默認持有遍歷器

原生默認持有遍歷器接口的數據結構有: 基本類型: Array, Set, Map(四種基本數據集合: Array, Object, Set 和 Map)。 類數組對象: arguments, NodeList, String。

  1. let iterator = '123'[Symbol.iterator]();
  2. console.log( iterator.next() ); // {value: "1", done: false}
  3. console.log( iterator.next() ); // {value: "2", done: false}
  4. console.log( iterator.next() ); // {value: "3", done: false}
  5. console.log( iterator.next() ); // {value: undefined, done: true}

遍歷器與先前的遍歷方法 一個數據集合擁有遍歷器接口,並不意味着所有遍歷它的方法都是使用此接口。實際上,只有 ES6 新增的幾種方式和某些方法會使用,下面會有介紹。以數組來說,對其使用 for和 for of雖然可訪問到相同的成員,但是實際的操作方式卻不同。

  1. // 改變數組默認的遍歷器接口。
  2. Array.prototype[Symbol.iterator] = function () {
  3. let index = 0;
  4. let array = this;
  5. console.log('Use iterator');
  6. return {
  7. next() {
  8. return index < array.length
  9. ? { value: array[index++], done: false }
  10. : { value: undefined, done: true };
  11. }
  12. }
  13. };
  14. let arr = [1, 2];
  15. for (let v of arr) {
  16. console.log(v); // 打印出 Use iterator, 1, 2。
  17. }
  18. for (let i = 0; i < arr.length; i++) {
  19. console.log(arr[i]); // 打印出 1, 2。
  20. }
  21. arr.forEach(d => {
  22. console.log(d); // 打印出 1, 2。
  23. });

對象沒有默認的遍歷器接口 爲什麼對象沒有默認的遍歷器接口?這要從兩方面說明。一爲遍歷器是種線性處理結構,對於任何非線性的數據結構,部署了遍歷器接口,就等於部署一種線性轉換。二是對象本來就是一個無序的集合,如果希望其有序,可以使用 Map代替。這即是各有其長,各安其職。屎殼郎如果不滾糞球而去採蜜,那,呃,花妹妹可能就遭殃咯。

自行生成的類數組對象(擁有 length屬性),不具備遍歷器接口。這與 String等原生類數組對象不同,畢竟人家是親生的,一出生就含着金鑰匙(也不怕誤吞)。不過我們可以將數組的遍歷器接口直接應用於自行生成的類數組對象,簡單有效無副作用。

  1. let obj = {
  2. 0: 'a',
  3. 1: 'b',
  4. length: 2,
  5. [Symbol.iterator]: Array.prototype[Symbol.iterator]
  6. };
  7. let iterator = obj[Symbol.iterator]();
  8. console.log( iterator.next() ); // {value: "a", done: false}
  9. console.log( iterator.next() ); // {value: "b", done: false}
  10. console.log( iterator.next() ); // {value: undefined, done: true}

爲對象添加遍歷器接口,也不影響之前不使用遍歷器的方法,比如 for in, Object.keys等等(兩者不等同)。

  1. let obj = {
  2. 0: 'a',
  3. 1: 'b',
  4. length: 2,
  5. [Symbol.iterator]: Array.prototype[Symbol.iterator]
  6. };
  7. console.log( Object.keys(obj) ); // ["0", "1", "length"]
  8. for (let v of obj) {
  9. console.log(v); // 依次打印出:"a", "b"。
  10. }
  11. for (let k in obj) {
  12. console.log(k); // 依次打印出:"0", "1", "length"。
  13. }

2.2 默認調用遍歷器

for of for of是專門用來消費遍歷器的,其遍歷的是鍵值( for in遍歷的是鍵名)。

  1. for (let v of [1, 2, 3]) {
  2. console.log(v); // 依次打印出:1, 2, 3。
  3. }

擴展運算符 無論是解構賦值或擴展運算都是默認調用遍歷器的。

  1. let [...a] = [3, 2, 1]; // [3, 2, 1]
  2. let b = [...[3, 2, 1]]; // [3, 2, 1]

yield* 在 Generator函數中有 yield*命令,如果其後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。

  1. for (let v of G()) {
  2. console.log(v); // 依次打印出:1, 2, 3, 4, 5
  3. }
  4. function* G() {
  5. yield 1;
  6. yield* [2,3,4];
  7. yield 5;
  8. }

其它場合 有些接受數組作爲參數的函數,會默認使用數組的遍歷器接口,所以也等同於默認調用。比如 Array.from(), Promise.all()。

延伸閱讀

關於 ES6 的 Symbol:

https://segmentfault.com/a/1190000015244917#articleHeader3

作者:wmaker

https://segmentfault.com/a/1190000015701263

相關文章