ES6基礎(四)Iterator和for...of

Iterator(遍歷器) 和 for…of 循環

遍歷器(Iterator)就是這樣一種機制。它是一種接口,爲各類不一樣的數據結構提供統一的訪問機制
任何數據結構只要部署 Iterator 接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)
html

1、迭代器和 for…of 淺談

1.1 傳統 for 循環

先來看一段標準的 for 循環的代碼:前端

var arr = [1,2,3];

for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}
// 1 2 3

注意,咱們拿到了裏面的元素,但卻多作了不少事:git

  • 咱們聲明瞭 i 標索引;
  • 肯定了邊界,一旦多層嵌套;
function unique(array) { 
  var res = [];
  for (var i = 0, arrayLen = array.length; i < arrayLen; i++) { 
    for (var j = 0, resLen = res.length; j < resLen; j++) { 
      if (array[i] === res[j]) { 
        break;
      }
    }
    if (j === resLen) { 
      // 把首次出現的加入到新數組中
      res.push(array[i]);
    }
  }
  return res;
}

爲了消除這種複雜度以及減小循環中的錯誤(好比錯誤使用其餘循環中的變量),ES6 提供了迭代器和 for of 循環共同解決這個問題。es6

1.2 terator(迭代器)

迭代器的描述:github

  • 是爲各類數據結構,提供一個統一的、簡便的訪問接口,是用於遍歷數據結構元素的指針
  • 二是使得數據結構的成員可以按某種次序排列;
  • 三是 ES6創造的一種遍歷命令 for…of 循環,Iterator 接口主要供 for…of 消費。

迭代的過程以下:web

  • 經過 Symbol.iterator 建立一個迭代器,指向當前數據結構的起始位置
  • 隨後經過 next 方法進行向下迭代指向下一個位置:
    • next 方法會返回當前位置的對象,對象包含了 valuedone 兩個屬性;
    • value 是當前屬性的值;
    • done 用於判斷是否遍歷結束,done 爲 true 時則遍歷結束;

迭代的內部邏輯應該是:數組

var it = makeIterator(["a", "b"]);

it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true }

function makeIterator(array) { 
  let index = 0;
  const iterator = { };
  iterator.next = function() { 
    if (index < array.length) return {  value: array[index++], done: false };
    return {  value: undefined, done: true };
  };
  return iterator;
}

1.3 什麼是 for…of?

注意這裏咱們僅說起了 forof 與迭代器的關係。數據結構

for…of 的描述:函數

  • for…of 語句在可迭代對象上建立一個迭代循環,調用自定義迭代鉤子,併爲每一個不一樣屬性的值執行語句——MDN
  • 一個數據結構只要部署了Symbol.iterator屬性,就被視爲具備 iterator 接口,就能夠用for...of循環遍歷它的成員。

看到這裏你會發現for...of和迭代器老是在一塊兒, for...of循環內部調用的是數據結構的Symbol.iterator方法。學習

舉個例子:

const obj = { 
  value: 1,
};

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

咱們直接 for of 遍歷一個對象,會報錯,然而若是咱們給該對象添加 Symbol.iterator 屬性:

const obj = { 
  value: 1,
};

obj[Symbol.iterator] = function() { 
  return createIterator([1, 2, 3]);
};

for (value of obj) { 
  console.log(value);
}
// 1
// 2
// 3

由此,咱們也能夠發現 for...of 遍歷的實際上是對象的 Symbol.iterator 屬性。

JavaScript 原有的 for…in 循環,只能得到對象的鍵名,不能直接獲取鍵值。ES6 提供 for…of 循環,容許遍歷得到鍵值。

var arr = ["a", "b", "c", "d"];

for (let a in arr) { 
  console.log(a); // 0 1 2 3
}

for (let a of arr) { 
  console.log(a); // a b c d
}

上面代碼代表:

  • for…in 循環讀取鍵名
  • for…of 循環讀取鍵值

for…of 循環調用遍歷器接口,數組的遍歷器接口只返回具備數字索引的屬性。這一點跟 for…in 循環也不同。

2、默認的 Iterator 接口

Iterator 接口的目的,就是爲全部數據結構,提供了一種統一的訪問機制。當使用 for…of 循環遍歷某種數據結構時,該循環會自動去尋找 Iterator 接口。

原生具有 Iterator 接口的數據結構以下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函數的 arguments 對象
  • NodeList 對象

拿數組舉例:

const item = [1, 2, 3][Symbol.iterator]();
item.next();
item.next();
item.next();
// {value: 1, done: false}
// {value: 2, done: false}
// {value: 3, done: false}
// {value: undefined, done: true}

對於原生部署Iterator接口的數據結構,不用本身寫遍歷器生成函數,for...of 循環會自動遍歷它們。除此以外,都須要本身在 Symbol.iterator 屬性上面部署。

本質上,遍歷器是一種線性處理,對於任何非線性的數據結構,部署遍歷器接口,就等於部署一種線性轉換。

對象(Object)之因此沒有默認部署 Iterator 接口,也是由於對象無法統一進行線性轉換

一個對象若是要具有可被 for…of 循環調用的 Iterator 接口,就必須在 Symbol.iterator 的屬性上部署遍歷器生成方法(原型鏈上的對象具備該方法也可)。

class newiterator { 
  constructor(start, stop) { 
    this.value = start;
    this.stop = stop;
  }
  // Iterator接口 返回自己
  [Symbol.iterator]() { 
    return this;
  }

  next() { 
    if (this.value < this.stop) { 
      return {  value: this.value++, done: false };
    }
    return {  value: undefined, done: true };
  }
}

const iterator = new newiterator(0, 3);

for (let key of iterator) { 
  console.log(key);
}
// 0 1 2

上面代碼是一個類部署 Iterator 接口的寫法。Symbol.iterator 屬性對應一個函數,執行後返回當前對象的遍歷器對象。

對於相似數組的對象(存在數值鍵名和 length 屬性),部署 Iterator 接口,有一個簡便方法,就是 Symbol.iterator 方法直接引用數組的 Iterator 接口。

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];

[...document.querySelectorAll("div")]; // 能夠執行了

NodeList 對象是相似數組的對象,原本就具備遍歷接口,能夠直接遍歷。上面代碼中,咱們將它的遍歷接口改爲數組的 Symbol.iterator 屬性,能夠看到沒有任何影響。

注意,普通對象部署數組的 Symbol.iterator 方法,並沒有效果。

let iterable = { 
  a: "a",
  b: "b",
  c: "c",
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator],
};
for (let item of iterable) { 
  console.log(item); // undefined, undefined, undefined
}

若是 Symbol.iterator 方法對應的不是遍歷器生成函數(即會返回一個遍歷器對象),解釋引擎將會報錯。

3、模擬實現的 for…of

其實模擬實現 for of 也比較簡單,就是利用它與 Symbol.iterator 的關係。

function forOf(obj, cb) { 
  let iterable, result;

  if (typeof obj[Symbol.iterator] !== "function")
    throw new TypeError(result + " is not iterable");
  if (typeof cb !== "function") throw new TypeError("cb must be callable");

  iterable = obj[Symbol.iterator]();

  result = iterable.next();
  while (!result.done) { 
    cb(result.value);
    result = iterable.next();
  }
}

4、使用 Iterator 接口的場景

有一些場合會默認調用 Iterator 接口(即 Symbol.iterator 方法),除了 for…of 循環,還有幾個別的場合。

4.1 解構賦值

對數組和 Set 結構進行解構賦值時,會默認調用 Symbol.iterator 方法。

let set = new Set()
  .add("a")
  .add("b")
  .add("c");

let [x, y] = set;
// x='a'; y='b'

let [first, ...rest] = set;
// first='a'; rest=['b','c'];

4.2 擴展運算符

擴展運算符(…)也會調用默認的 Iterator 接口。

// 例一
var str = "hello";
[...str]; // ['h','e','l','l','o']

// 例二
let arr = ["b", "c"];
["a", ...arr, "d"];
// ['a', 'b', 'c', 'd']

上面代碼的擴展運算符內部就調用 Iterator 接口。

實際上,這提供了一種簡便機制,能夠將任何部署了 Iterator 接口的數據結構,轉爲數組。也就是說,只要某個數據結構部署了 Iterator 接口,就能夠對它使用擴展運算符,將其轉爲數組。

4.3 yield*

yield*後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。

let generator = function*() { 
  yield 1;
  yield* [2, 3, 4];
  yield 5;
};

var iterator = generator();

iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 2, done: false }
iterator.next(); // { value: 3, done: false }
iterator.next(); // { value: 4, done: false }
iterator.next(); // { value: 5, done: false }
iterator.next(); // { value: undefined, done: true }

4.4 其餘場合

因爲數組的遍歷會調用遍歷器接口,因此任何接受數組做爲參數的場合,其實都調用了遍歷器接口。下面是一些例子。

  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(好比 new Map([[‘a’,1],[‘b’,2]]))
  • Promise.all()
  • Promise.race()

5、Iterator 接口與 Generator 函數

Symbol.iterator()方法的最簡單實現,仍是使用 ES6 新提出的 Generator 函數。

let myIterable = { 
  [Symbol.iterator]: function* () { 
    yield 1;
    yield 2;
    yield 3;
  }
};
[...myIterable] // [1, 2, 3]

// 或者採用下面的簡潔寫法

let obj = { 
  [Symbol.iterator]() { 
    yield 'hello';
    yield 'world';
  }
};

for (let x of obj) { 
  console.log(x);
}
// "hello"
// "world"

上面代碼中,Symbol.iterator()方法幾乎不用部署任何代碼,只要用 yield 命令給出每一步的返回值便可。

6、遍歷器對象的 return(),throw()

遍歷器對象除了具備 next()方法,還能夠具備 return()方法和 throw()方法。若是你本身寫遍歷器對象生成函數,那麼 next()方法是必須部署的,return()方法和 throw()方法是否部署是可選的。

return()方法的使用場合是,若是 for…of 循環提早退出(一般是由於出錯,或者有 break 語句),就會調用 return()方法。若是一個對象在完成遍歷前,須要清理或釋放資源,就能夠部署 return()方法。

function readLinesSync(file) { 
  return { 
    [Symbol.iterator]() { 
      return { 
        next() { 
          return {  done: false };
        },
        return() { 
          file.close();
          return {  done: true };
        },
      };
    },
  };
}

上面代碼中,函數 readLinesSync 接受一個文件對象做爲參數,返回一個遍歷器對象,其中除了 next()方法,還部署了 return()方法。下面的兩種狀況,都會觸發執行 return()方法。

// 狀況一
for (let line of readLinesSync(fileName)) { 
  console.log(line);
  break;
}

// 狀況二
for (let line of readLinesSync(fileName)) { 
  console.log(line);
  throw new Error();
}

上面代碼中:

  • 狀況一輸出文件的第一行之後,就會執行 return()方法,關閉這個文件;
  • 狀況二會在執行 return()方法關閉文件以後,再拋出錯誤。

參考

寫在最後

JavaScript 系列:

  1. 《JavaScript 內功進階系列》(已完結)
  2. 《JavaScript 專項系列》(持續更新)
  3. 《ES6 基礎系列》(持續更新)

關於我

  • 花名:餘光(沉迷 JS,虛心學習中)
  • WX:j565017805

其餘沉澱

這是文章所在 GitHub 倉庫的傳送門,您點的 star,就是對我最大的鼓勵 ~

相關文章
相關標籤/搜索