本文主要來深刻剖析ES6的Iterator(迭代器/遍歷器),在瞭解它以前,咱們首先要知道爲何須要Iterator? 它出現的緣由是什麼?javascript
平時開發中,咱們常常會用到循環,拿最基本的來講,好比循環一個數組:html
// for循環
var arr = [1, 2, 3, 4];
for(let i = 0; i < arr.length; i++) {
console.log(arr[i]); // 1 2 3 4
}
// 也能夠用forEach循環
arr.forEach(item => {
console.log(item); // 1 2 3 4
})
// 也能夠用for in循環
for(let i in arr) {
console.log(arr[i]); // 1 2 3 4
}
複製代碼
若是是循環輸出字符串的每個字符java
// for循環
var str = 'abcde';
for(var i = 0; i < str.length; i++) {
console.log(str[i]); // a b c d e
}
// for in循環
for(var k in str){
console.log(str[k]); // a b c d e
}
// forEach不能遍歷字符串,若是硬要用則只能先把字符串轉爲數組再用數組方式循環
複製代碼
若是是循環輸出一個map對象呢, 妥妥的就一個foreach循環,普通的for循環和for...in都不行node
var map = new Map();
map.set('first', '第一');
map.set('second', '第二');
map.set('third', '第三');
// forEach循環
map.forEach((val,key) => {
console.log(val, key);
// 第一 first
// 第二 second
// 第三 third
});
複製代碼
對於上述各集合數據,咱們發現沒有一個循環方法是能夠一次性解決的。數組
雖然forEach循環不能循環字符串,但字符串能夠轉爲數組再使用forEach便可輸出,但這操做並不舒服每次使用都要轉換。並且forEach循環存在缺點:不能使用break,continue語句跳出循環,或者使用return從函數體返回
。bash
而for循環在有些狀況寫代碼會增長複雜度,並且不能循環對象。數據結構
相比下,for...in的缺點是不只遍歷數字鍵名,還會遍歷手動添加的自定義鍵,甚至包括原型鏈上的鍵。for...in主要仍是爲遍歷對象而設計的,並不太適用於遍歷數組。閉包
以下代碼函數
Array.prototype.protoValue = 'hello';
var arr = [1, 2, 3, 4];
arr.test = 'test';
for(let i in arr) {
console.log(arr[i]); // 1 2 3 4 test hello
}
複製代碼
話說回來,有沒有一種更好的循環能一統上述循環問題? ES6就有了,用for...of循環ui
它的出現解決了什麼問題呢?首先是固然是能解決了上述咱們的問題
對上述數據進行循環輸出
for(let v of arr) {
console.log(v); // 1 2 3 4
}
for(let v of str) {
console.log(v); // a b c d e
}
for(let v of map) {
console.log(v);
// (2) ["first", "第一"]
// (2) ["second", "第二"]
// (2) ["third", "第三"]
}
複製代碼
來看看它的優勢:
阮一峯老師的《ECMAScript 6 入門》提到:
ES6中引入了for...of循環,做爲遍歷全部數據結構的統一的方法。
Iterator是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)。
一個數據結構只要具備 iterator 接口,就能夠用for...of循環遍歷它的成員。
也就是說,並非全部的對象都能使用for...of循環,只有實現了Iterator接口的對象,纔可以for...of來進行遍歷取值。
那麼,Iterator是什麼呢?咱們接着說
它是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)。
如今明白了,Iterator的產生主要是爲了使用for...of方法。但具體Iterator概念仍是有些抽象,若是要直接具體的描述的話:
Iterator其實就是一個具備 next()方法的對象,而且每次調用next()方法都會返回一個結果對象,這個結果對象有兩個屬性, 以下
{
value: 表示當前的值,
done: 表示遍歷是否結束
}
複製代碼
這裏多了一些概念,咱們來梳理一下:
Iterator是一個特殊的對象:
value
和done
。value
表示具體的返回值;done是布爾類型,表示集合是否完成遍歷,沒有則返回true,不然返回false根據上述描述,咱們來模擬一個迭代器,代碼以下
function createIterator(items) {
var i = 0;
return {
next: function() {
var done = (i >= item.length);
var value = !done ? items[i++] : undefined;
return {
done: done,
value: value
};
}
};
}
// iterator 就是一個迭代器對象
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: false, value: 3 }
console.log(iterator.next()); // { done: true, value: undefined }
console.log(iterator.next()); // { done: true, value: undefined }
console.log(iterator.next()); // { done: true, value: undefined }
複製代碼
過程:
i
的值,每次調用i
都會+1
,指向下一個。故第一次指針指向數據結構的第一個成員,輸出1。看到這裏你們大概知道Iterator
了,但Iterator接口主要供for...of消費。咱們試着用for...of循環上面建立Iterator對象
var iterator = makeIterator([1, 2, 3]);
for (let value of iterator) {
console.log(value); // Uncaught TypeError: iterator is not iterable
}
複製代碼
結果報錯,說明咱們生成的 iterator 對象並非可遍歷的,這樣的結構還不可以被for...of循環
那什麼樣的結構纔是可遍歷的呢?
ES6還引入了一個新的Symbol對象,symbol值是惟一的。
一個數據結構只要部署了Symbol.iterator屬性,就被視爲具備 iterator 接口;調用這個接口,就會返回一個遍歷器對象。這樣的數據結構才能被稱爲可迭代對象(Iterator),該對象可被for...of遍歷。
照着上面的規定來建立一個可迭代對象
var arr = [1, 2, 3];
arr[Symbol.iterator] = function() {
var _this = this;
var i = 0;
return {
next: function() {
var done = (i >= _this.length);
var value = !done ? _this[i++] : undefined;
return {
done: done,
value: value
};
}
};
}
// 此時能夠for...of遍歷
for(var item of arr){
console.log(item); // 1 2 3
}
複製代碼
由此,咱們也能夠知道 for...of 遍歷的實際上是對象的 Symbol.iterator 屬性。
在ES6中,全部的集合對象,包括數組,類數組對象(arguments對象、DOM NodeList 對象),Map和Set,還有字符串都是可迭代的,均可以被for...of遍歷,由於他們都有默認的迭代器。
下面就挑其中幾個類型來舉例子:
var arr = [1, 2, 3];
var iteratorObj = arr[Symbol.iterator]();
console.log(iteratorObj.next());
console.log(iteratorObj.next());
console.log(iteratorObj.next());
console.log(iteratorObj.next());
複製代碼
輸出結果:
由上述對迭代器的概念認識,輸出結果也徹底符合咱們預期。咱們知道普通對象是默認沒有部署這個接口的,因此arguments這個屬性沒有在原型上,而是在對象自身的屬性上。
function test(){
var obj = arguments[Symbol.iterator]();
console.log(arguments);
console.log(obj.next());
console.log(obj.next());
console.log(obj.next());
}
test(1, 2, 3);
複製代碼
輸出結果:
<div class="test">1</div>
<div class="test">2</div>
<div class="test">3</div>
複製代碼
const nodeList = document.getElementsByClassName('test')
for (const node of nodeList) {
console.log(node);
}
複製代碼
輸出結果:
此次直接從原型上找來證實
console.log(Map.prototype.hasOwnProperty(Symbol.iterator)); // true
複製代碼
至於其餘類型的可迭代對象你們可觸類旁通。
有一些場合會默認調用 Iterator 接口(即Symbol.iterator方法)
let set = new Set().add('a').add('b').add('c');
let [x, y] = set;
console.log(x, y); // a b
複製代碼
擴展運算符(...)也會調用默認的 Iterator 接口,能夠將當前迭代對象轉換爲數組。
var str = 'hello';
console.log([...str]); // ["h", "e", "l", "l", "o"]
let arr = ['b', 'c'];
console.log(['a', ...arr, 'd']); // ["a", "b", "c", "d"]
複製代碼
yield*後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
複製代碼
關於yield以後會再寫一篇專題
例如:for...of、Set()、Map()、Array.from()等。咱們主要是爲了證實調用上面這些函數時着實是用到了Iterator接口。
咱們貼出上面的實現可迭代對象的代碼,進行改動。想想,儘管咱們沒有手動添加 Symbol.iterator屬性,但由於 ES6 默認部署了 Symbol.iterator,數組仍是能夠遍歷成功,那固然咱們也能夠手動修改這個屬性,從新部署也能夠。
若是咱們手動修改生效,影響了輸出,證實某方法調用須要用到Iterator接口
var arr = [1, 2, 3];
arr[Symbol.iterator] = function() {
var _this = this;
var i = 0;
return {
next: function() {
var done = (i >= _this.length);
var value = !done ? _this[i++] : undefined;
return {
done: done,
value: value + ' 手動添加的屬性' // 添加自定義值
};
}
};
}
// 輸出結果顯示手動修改生效,證實for...of調用了Iterator接口
for(var item of arr){
console.log(item);
// 1 手動添加的屬性
// 2 手動添加的屬性
// 3 手動添加的屬性
}
var set = new Set(arr);
console.log(set); // Set(3) {"1 手動添加的屬性", "2 手動添加的屬性", "3 手動添加的屬性"}
複製代碼
上述其餘方法證實也是如此,就不一一列出了。
有些數據結構是在現有數據結構的基礎上,計算生成的。好比,ES6 的數組、Set、Map 都部署瞭如下三個方法,調用後都返回遍歷器對象。
var arr = ["first", "second", "third"];
for (let index of arr.keys()) {
console.log(index);
}
// 0
// 1
// 2
for (let color of arr.values()) {
console.log(color);
}
// first
// second
// third
for (let item of arr.entries()) {
console.log(item);
}
// [ 0, "first" ]
// [ 1, "second" ]
// [ 2, "third" ]
複製代碼
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
console.log(isIterable(new Set())); // true
console.log(isIterable(new Map())); // true
console.log(isIterable([1, 2, 3])); // true
console.log(isIterable("hello world")); // true
複製代碼
梳理了半天,回到最初,Iterator的產生主要是爲了使用for...of方法。而對象不像數組的值是有序的,遍歷的時候根本不知道如何肯定他們的前後順序,因此要注意的是for...of循環不支持遍歷對象。
若是非要遍歷對象,同理對象也必須包含[Symbol.iterator]的屬性並實現迭代器方法,能夠經過手動利用Object.defineProperty方法添加該屬性。
var obj = { a: 2, b: 3 }
for (let i of obj) {
console.log(i) // Uncaught TypeError: obj is not iterable
}
複製代碼
關於Iterator就寫到這裏了,下一篇寫ES6的Generator生成器。
若是該文對你有幫助,點個贊哦,若有錯誤請指正~~~