diff --git a/docs/iterator.md b/docs/iterator.md index db07920..6f2ef86 100644 --- a/docs/iterator.md +++ b/docs/iterator.md @@ -2,11 +2,15 @@ ## Iterator(遍历器) -### 语法 +### 概念 -遍历器(Iterator)是一种接口规格,任何对象只要部署这个接口,就可以完成遍历操作。它的作用有两个,一是为各种数据结构,提供一个统一的、简便的接口,二是使得对象的属性能够按某种次序排列。在ES6中,遍历操作特指for...of循环,即Iterator接口主要供for...of循环使用。 +JavaScript原有的数据结构,主要是数组(Array)和对象(Object),ES6又添加了Map和Set,用户还可以组合使用它们,定义自己的数据结构。这就需要一种统一的接口机制,来处理所有不同的数据结果。 -遍历器提供了一个指针,指向当前对象的某个属性,使用next方法,就可以将指针移动到下一个属性。next方法返回一个包含value和done两个属性的对象。其中,value属性是当前遍历位置的值,done属性是一个布尔值,表示遍历是否结束。下面是一个模拟next方法返回值的例子。 +遍历器(Iterator)就是这样一种机制。它属于一种接口规格,任何数据结构只要部署这个接口,就可以完成遍历操作,即依次处理该结构的所有成员。它的作用有两个,一是为各种数据结构,提供一个统一的、简便的接口,二是使得数据结构的成员能够按某种次序排列。在ES6中,遍历操作特指for...of循环,即Iterator接口主要供for...of消费。 + +遍历器的遍历过程是这样的:它提供了一个指针,默认指向当前数据结构的起始位置。第一次调用遍历器的next方法,可以将指针指向到第一个成员,第二次调用就指向第二个成员,直至指向数据结构的结束位置。每一次调用,都会返回当前成员的信息,具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。 + +下面是一个模拟next方法返回值的例子。 ```javascript @@ -28,9 +32,10 @@ it.next() // { value: "b", done: false } it.next() // { value: undefined, done: true } ``` -上面代码定义了一个makeIterator函数,它的作用是返回一个遍历器对象,用来遍历参数数组。next方法依次遍历数组的每个成员,请特别注意,next返回值的构造。 -下面是一个无限运行的遍历器例子。 +上面代码定义了一个makeIterator函数,它的参数是一个数组。调用该函数,就会返回一个对象。这个对象具有一个next方法,每次调用next方法,它的内部指针就会指向数组的下一个成员,并返回一个该成员信息的对象。请特别注意,next方法的返回值的构造:一个具有value和done两个属性的对象。通过这个返回值,我们就可以知道当前成员的值是什么,以及遍历是否结束。在这个例子中,makeIterator函数用来生成遍历器,它返回的那个具有next方法的对象就是遍历器,调用遍历器的next方法,就可以遍历事先给定的数组。 + +因为遍历器的作用,只是把接口规格加到数据结构之上。所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器,或者说用遍历器模拟出数据结构。下面是一个无限运行的遍历器例子。 ```javascript @@ -53,15 +58,37 @@ it.next().value // '2' ``` -上面的例子,说明了next方法返回值的结构:value和done两个属性。 +上面的例子中,idMaker函数返回的对象就是遍历器,但是并没有对应的数据结构,或者说遍历器自己描述了一个数据结构出来。 -### Iterator接口的部署 +总之,所谓Iterator接口,就是指调用这个接口,会返回一个遍历器对象。该对象具备next方法,每次调用该方法,会返回一个具有value和done两个属性的新对象,指向部署了Iterator接口的数据结构的一个成员。 -具有Iterator接口的对象,都能被for...of循环遍历(见后文的介绍)。所谓Iterator接口,就是指它会返回一个遍历器对象,该对象具备next方法,每次调用该方法,会依次返回一个具有上节提到的value和done两个属性的新对象,指向原对象的一个成员。 +### 默认的Iterator接口 -在ES6中,有三类数据结构原生具备Iterator接口:数组、类似数组的对象、Set和Map结构。除此之外,其他数据结构(主要是对象)的Iterator接口都需要自己部署。其他对象需要手动部署Iterator接口,让其返回一个遍历器。 +Iterator接口的开发目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环(见后文的介绍)。当使用for...of循环,遍历某种数据结构时,该循环会自动去寻找Iterator接口。 -一个对象如果要有Iterator接口,必须部署一个@@iterator方法(原型链上的对象具有该方法也可),该方法部署在一个键名为`Symbol.iterator`的属性上,对应的键值是一个函数,该函数返回一个遍历器对象。 +ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性。也就是说,调用Symbol.iterator方法,就会得到当前数据结构的默认遍历器。Symbol.iterator是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为Symbol的特殊值,所以要放在方括号内(请参考Symbol一节)。 + +在ES6中,有三类数据结构原生具备Iterator接口:数组、类似数组的对象、Set和Map结构。 + +```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 } + +``` + +上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器。 + +上面提到,原生就部署iterator接口的数据结构有三类,对于这三类数据结构,不用自己写遍历器,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的Iterator接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。 + +对象(Object)之所以没有默认部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作Map结构使用,ES5没有Map结构,而ES6原生提供了。 + +一个对象如果要有可被for...of循环调用的Iterator接口,就必须在Symbol.iterator的属性上部署遍历器方法(原型链上的对象具有该方法也可)。 ```javascript @@ -75,18 +102,16 @@ class MySpecialTree { ``` -上面代码是一个类部署Iterator接口的写法。`Symbol.iterator`是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为Symbol的特殊值,所以要放在方括号内(请参考Symbol一节)。这里要注意,@@iterator的键名是`Symbol.iterator`,键值是一个方法(函数),该方法执行后,返回一个当前对象的遍历器。 - -下面是一个例子。 +上面代码是一个类部署Iterator接口的写法。Symbol.iterator属性对应一个函数,执行后返回当前对象的遍历器。下面是一个例子。 ```javascript -function O(value){ +function Obj(value){ this.value = value; this.next = null; } -O.prototype[Symbol.iterator] = function(){ +Obj.prototype[Symbol.iterator] = function(){ var iterator = { next: next @@ -112,9 +137,9 @@ O.prototype[Symbol.iterator] = function(){ return iterator; } -var one = new O(1); -var two = new O(2); -var three = new O(3); +var one = new Obj(1); +var two = new Obj(2); +var three = new Obj(3); one.next = two; two.next = three; @@ -155,7 +180,7 @@ let obj = { ``` -对于类似数组的对象,部署Iterator接口,有一个简便方法,就是`Symbol.iterator`方法直接引用数值的Iterator接口。 +对于类似数组的对象(存在数值键名和length属性),部署Iterator接口,有一个简便方法,就是`Symbol.iterator`方法直接引用数值的Iterator接口。 ```javascript @@ -163,7 +188,7 @@ NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; ``` -如果`Symbol.iterator`方法返回的不是遍历器,解释引擎将会报错。 +如果Symbol.iterator方法返回的不是遍历器,解释引擎将会报错。 ```javascript @@ -175,7 +200,7 @@ obj[Symbol.iterator] = () => 1; ``` -上面代码中,变量obj的@@iterator方法返回的不是遍历器,因此报错。 +上面代码中,变量obj的Symbol.iterator方法返回的不是遍历器,因此报错。 ### 原生具备iterator接口的数据结构 @@ -277,7 +302,11 @@ for (let x of obj) { ## for...of循环 -ES6中,一个对象只要部署了@@iterator方法,就被视为具有iterator接口,就可以用for...of循环遍历它的值。也就是说,for...of循环内部调用是原对象的`Symbol.iterator`方法。 +ES6中,一个数据结构只要部署了Symbol.iterator方法,就被视为具有iterator接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。 + +for...of循环可以使用的范围包括数组、Set和Map结构、类似数组的对象(比如arguments对象、DOM NodeList对象)、后文的Generator对象,以及字符串。 + +### 数组 数组原生具备iterator接口。 @@ -298,8 +327,8 @@ for(let v of arr) { const arr = ['red', 'green', 'blue']; arr.forEach(function (element, index) { - console.log(element); // red green blue - console.log(index); // 0 1 2 + console.log(element); // red green blue + console.log(index); // 0 1 2 }); ``` @@ -309,6 +338,7 @@ JavaScript原有的for...in循环,只能获得对象的键名,不能直接 ```javascript var arr = ["a", "b", "c", "d"]; + for (a in arr) { console.log(a); // 0 1 2 3 } @@ -321,13 +351,15 @@ for (a of arr) { 上面代码表明,for...in循环读取键名,for...of循环读取键值。如果要通过for...of循环,获取数组的索引,可以借助数组实例的entries方法和keys方法,参见《数组的扩展》章节。 +### Set和Map结构 + Set和Map结构也原生具有Iterator接口,可以直接使用for...of循环。 ```javascript var engines = Set(["Gecko", "Trident", "Webkit", "Webkit"]); for (var e of engines) { - console.log(e); + console.log(e); } // Gecko // Trident @@ -346,7 +378,86 @@ for (var [name, value] of es6) { ``` -上面代码演示了如何遍历Set结构和Map结构,后者是同时遍历键名和键值。 +上面代码演示了如何遍历Set结构和Map结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set结构遍历时,返回的是一个值,而Map结构遍历时,返回的是一个数组,该数组的两个成员分别为当前Map成员的键名和键值。 + +```javascript + +let map = new Map().set('a', 1).set('b', 2); +for (let pair of map) { + console.log(pair); +} +// ['a', 1] +// ['b', 2] + +``` + +### 计算生成的数据结构 + +ES6的数组、Set、Map都部署了以下三个方法,调用后都返回遍历器。 + +- entries() 返回一个遍历器,用来遍历 [键名, 键值] 组成的数组。对于数组,键名就是索引值;对于Set,键名与键值相同。Map结构的iterator接口,默认就是调用entries方法。 +- keys() 返回一个遍历器,用来遍历所有的键名。 +- values() 返回一个遍历器,用来遍历所有的键值。 + +这三个方法调用后生成的遍历器,所遍历的都是计算生成的数据结构。 + +```javascript + +let arr = ['a', 'b', 'c']; +for (let pair of arr.entries()) { + console.log(pair); +} +// [0, 'a'] +// [1, 'b'] +// [2, 'c'] + +``` + +### 类似数组的对象 + +类似数组的对象包括好几类。下面是for...of循环用于字符串、DOM NodeList对象、arguments对象的例子。 + +```javascript + +// 字符串 +let str = "hello"; + +for (let s of str) { + console.log(s); // h e l l o +} + +// DOM NodeList对象 +let paras = document.querySelectorAll("p"); + +for (let p of paras) { + p.classList.add("test"); +} + +// arguments对象 +function printArgs() { + for (let x of arguments) { + console.log(x); + } +} +printArgs('a', 'b'); +// 'a' +// 'b' + +``` + +对于字符串来说,for...of循环还有一个特点,就是会正确识别32位UTF-16字符。 + +```javascript + +for (let x of 'a\uD83D\uDC0A') { + console.log(x); +} +// 'a' +// '\uD83D\uDC0A' + +``` + +### 对象 对于普通的对象,for...of结构不能直接使用,会报错,必须部署了iterator接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。 @@ -372,26 +483,4 @@ for (e of es6) { ``` -上面代码表示,for...in循环可以遍历键名,for...of循环会报错。 - -总结一下,for...of循环可以使用的范围包括数组、类似数组的对象(比如arguments对象、DOM NodeList对象)、Set和Map结构、后文的Generator对象,以及字符串。下面是for...of循环用于字符串和DOM NodeList对象的例子。 - -```javascript - -// 字符串的例子 - -let str = "hello"; - -for (let s of str) { - console.log(s); // h e l l o -} - -// DOM NodeList对象的例子 - -let paras = document.querySelectorAll("p"); - -for (let p of paras) { - p.classList.add("test"); -} - -``` +上面代码表示,对于普通的对象,for...in循环可以遍历键名,for...of循环会报错。