es6入門(六)

一、Iterator

1.1 概述

Iterator(遍歷器)爲各種不同的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就可以完成遍歷操作。

  • Iterator 的作用:

1) 爲各種數據結構,提供一個統一的、簡便的訪問接口;
2)使得數據結構的成員能夠按某種次序排列;
3) ES6 創造了一種新的遍歷命令for…of循環,Iterator 接口主要供for…of消費;

1.2 Iterator的遍歷過程

(1)創建一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
(2)第一次調用指針對象的next方法,可以將指針指向數據結構的第一個成員。
(3)第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。
(4)不斷調用指針對象的next方法,直到它指向數據結構的結束位置。

每一次調用next方法,都會返回當前成員對象。該對象包含兩個值:value和done。value代表當前成員的值,done是一個布爾值,表示遍歷是否結束。

<script type="text/javascript">
	let iter = createIterator([11, 22, 33]);
	
	console.log(iter.next()); // Object {value: 11, done: false}
	console.log(iter.next()); // Object {value: 22, done: false}
	console.log(iter.next()); // Object {value: 33, done: false}
	console.log(iter.next()); // Object {value: undefined, done: true}
	
	// 創建遍歷器對象
	function createIterator(array) {
		let index = 0;
		return {
			next() {
				if (index < array.length) {
					return {
						value: array[index++],
						done: false
					}
				} else {
					return {
						value: undefined,
						done: true
					}
				}
			}
		}
	}
</script>

對於遍歷器對象來說,done: false和value: undefined屬性都是可以省略的,因此上面的createIterator函數可以簡寫成下面的形式。

function createIterator(array) {
	let index = 0;
	return {
		next() {
			if (index < array.length) {
				return {
					value: array[index++]
				}
			} else {
				return {
					done: true
				}
			}
		}
	}
}

由於 Iterator 只是把接口規格加到數據結構之上,所以,遍歷器與它所遍歷的那個數據結構,實際上是分開的,完全可以寫出沒有對應數據結構的遍歷器對象,或者說用遍歷器對象模擬出另外一個數據結構。

<script type="text/javascript">
	var it = idMaker();
	console.log(it.next().value); // 0
	console.log(it.next().value); // 1
	console.log(it.next().value); // 2
	// ...

	function idMaker() {
	  var index = 0;
	  return {
		next: function() {
		  return {value: index++, done: false};
		}
	  };
	}
</script>

上面的例子中,遍歷器生成函數idMaker,返回一個遍歷器對象(即指針對象)。但是並沒有對應的數據結構,或者說,遍歷器對象自己描述了一個數據結構出來。

1.3 默認Iterator接口

Iterator 接口的目的,就是爲所有數據結構,提供了一種統一的訪問機制,即for…of循環。當使用for…of循環遍歷某種數據結構時,該循環會自動去尋找 Iterator 接口。一種數據結構只要部署了 Iterator 接口,我們就稱這種數據結構是“可遍歷的”(iterable)。

ES6 規定,默認的 Iterator 接口部署在數據結構的[Symbol.iterator]屬性,或者說,一個數據結構只要具有Symbol.iterator屬性,就可以認爲是“可遍歷的”(iterable)。Symbol.iterator屬性本身是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。至於屬性名[Symbol.iterator],它是一個表達式,返回Symbol對象的iterator屬性,這是一個預定義好的、類型爲 Symbol 的特殊值,所以要放在方括號內。

const obj = {
	// 設置Symbol.iterator屬性,該屬性用於生成迭代其對象
  	[Symbol.iterator]: function () {
    	return {
      		next: function () {
        		return {
          			value: 1,
          			done: true
        		};
      		}
    	};
  	}
};

上面代碼中,對象obj是可遍歷的(iterable),因爲具有Symbol.iterator屬性。執行這個屬性,會返回一個遍歷器對象。該對象的根本特徵就是具有next方法。每次調用next方法,都會返回一個代表當前成員的信息對象,具有value和done兩個屬性。

ES6 的有些數據結構原生具備 Iterator 接口(比如數組),即不用任何處理,就可以被for…of循環遍歷。原因在於,這些數據結構原生部署了Symbol.iterator屬性。原生具備 Iterator 接口的數據結構如下:

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

下面是數組的[Symbol.Iterator]屬性:

<script type="text/javascript">
	let arr = ['a', 'b', 'c'];
	let iter = arr[Symbol.iterator]();

	iter.next() // { value: 'a', done: false }
	iter.next() // { value: 'b', done: false }
	iter.next() // { value: 'c', done: false }
	iter.next() // { value: undefined, done: true }
</script>

上面代碼中,變量arr是一個數組,原生就具有遍歷器接口,部署在arr的Symbol.iterator屬性上面。所以,調用這個屬性,就得到遍歷器對象。

對於原生部署 Iterator 接口的數據結構,不用自己寫遍歷器生成函數,for…of循環會自動遍歷它們。一個對象如果要具備可被for…of循環調用的 Iterator 接口,就必須在Symbol.iterator的屬性上部署遍歷器生成方法。

<script type="text/javascript">
	class RangeIterator {
		constructor(start, stop) {
			this.value = start;
			this.stop = stop;
		}
	
		[Symbol.iterator]() { 
			return this;
		}
	
		next() {
			var value = this.value;
			if (value < this.stop) {
				this.value++;
				return {done: false, value: value};
			}
			return {done: true, value: undefined};
		}
	}
	
	const rangeItrator = new RangeIterator(0, 3);
	console.log(rangeItrator.next());
	console.log(rangeItrator.next());
	console.log(rangeItrator.next());
</script>

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

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

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

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

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

var obj = {};

obj[Symbol.iterator] = () => 1;

[...obj] // TypeError: [] is not a function

上面代碼中,變量obj的Symbol.iterator方法對應的不是遍歷器生成函數,因此報錯。

1.4 調用Iterator接口的場合

除了一會兒介紹的for…of循環以外,還有其他一些場合需要調用Iterator接口。

1.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'];

1.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 接口,就可以對它使用擴展運算符,將其轉爲數組。

1.4.3 其他場合

由於數組的遍歷會調用遍歷器接口,所以任何接受數組作爲參數的場合,其實都調用了遍歷器接口。下面是一些例子。

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

1.5 字符串的Iterator接口

字符串是一個類似數組的對象,也原生具有 Iterator 接口。

<script>
	var someString = "hi";
	// 獲取遍歷器
	var iterator = someString[Symbol.iterator]();
	// 移動指針,獲取對象的值
	console.log(iterator.next());  // { value: "h", done: false }
	console.log(iterator.next());  // { value: "i", done: false }
	console.log(iterator.next());  // { value: undefined, done: true }
</script>

二、for…of循環

一個數據結構只要部署了Symbol.iterator屬性,就被視爲具有 iterator 接口,就可以用for…of循環遍歷它的成員。也就是說,for…of循環內部調用的是數據結構的Symbol.iterator方法。

for…of循環可以使用的範圍包括數組、Set 和 Map 結構、某些類似數組的對象。

2.1 數組

數組原生具備iterator接口,for…of循環本質上就是調用這個接口產生的遍歷器。

for…of循環可以代替數組實例的forEach方法。

const arr = ['red', 'green', 'blue'];

arr.forEach(function (element, index) {
  console.log(element); // red green blue
  console.log(index);   // 0 1 2
});

for…of循環調用遍歷器接口,數組的遍歷器接口只返回具有數字索引的屬性。

let arr = [3, 5, 7];
arr.foo = 'hello';

for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}

上面代碼中,for…of循環不會返回數組arr的foo屬性。

2.2 Set和Map結構

Set 和 Map 結構也原生具有 Iterator 接口,可以直接使用for…of循環。

// 遍歷Set
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
  console.log(e);
}

// 遍歷Map
var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
  console.log(name + ": " + value);
}

值得注意的是,遍歷順序是按照各個成員被添加進數據結構的順序。其次,Set 結構遍歷時,返回的是一個值,而 Map 結構遍歷時,返回的是一個數組,該數組的兩個成員分別爲當前 Map 成員的鍵名和鍵值。

2.3 計算生成的數據結構

有些數據結構是在現有數據結構的基礎上計算生成的。比如:

  • entries() 返回一個遍歷器對象,用來遍歷[鍵名, 鍵值]組成的數組。對於數組,鍵名就是索引值;對於 Set,鍵名與鍵值相同。對於Map,默認就是調用entries方法。
  • keys() 返回一個遍歷器對象,用來遍歷所有的鍵名。
  • values() 返回一個遍歷器對象,用來遍歷所有的鍵值。

2.4 類似數組對象

類似數組的對象有字符串、DOM NodeList對象、函數內的arguments對象等。

使用for…of遍歷字符串:

let str = "hello";

for (let s of str) {
  console.log(s); // h e l l o
}

使用for…of遍歷NodeList對象:

let pNodes = document.querySelectorAll("p");

for (let p of pNodes) {
  p.classList.add("test");
}

使用for…of變量arguments對象:

function printArgs() {
  for (let x of arguments) {
    console.log(x);
  }
}
printArgs('a', 'b', 'c');

並不是所有類似數組的對象都具有 Iterator 接口,一個簡便的解決方法,就是使用Array.from方法將其轉爲數組,例如:

let arrayLike = { length: 2, 0: 'a', 1: 'b' };

// 報錯
for (let x of arrayLike) {
  console.log(x);
}

// 正確
for (let x of Array.from(arrayLike)) {
  console.log(x);
}

2.5 對象

對於普通的對象,for…of結構不能直接使用,會報錯,必須部署了 Iterator 接口後才能使用。

let es6 = {
  edition: 6,
  committee: "TC39",
  standard: "ECMA-262"
};

for (let e in es6) {
  console.log(e);
}
// edition
// committee
// standard

for (let e of es6) {
  console.log(e);
}
// TypeError: es6[Symbol.iterator] is not a function

上面代碼表示,對於普通的對象,for…in循環可以遍歷鍵名,for…of循環會報錯。

一種解決方法是,使用Object.keys方法將對象的鍵名生成一個數組,然後遍歷這個數組。

for (var key of Object.keys(someObject)) {
  console.log(key + ': ' + someObject[key]);
}

2.6 不同遍歷方法的比較

以數組爲例,JavaScript提供了多種遍歷的語法。

2.6.1 普通for循環

for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}

這種寫法比較麻煩。

2.6.2 forEach循環

myArray.forEach(function (value) {
  console.log(value);
});

這種寫法的問題在於,無法中途跳出forEach循環,break命令或return命令都不能奏效。

2.6.3 for…in循環

for (var index in myArray) {
  console.log(myArray[index]);
}

for…in循環主要是爲遍歷對象而設計的,不適用於遍歷數組。

2.6.4 for…of循環

for (let value of myArray) {
  console.log(value);
}

for…of循環的特點:

  • 有着同for…in一樣的簡潔語法,但是沒有for…in那些缺點。
  • 不同於forEach方法,它可以與break、continue和return配合使用。
  • 提供了遍歷所有數據結構的統一操作接口。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章