JavaScript迭代器的含義及用法
什么是迭代器
迭代器就是為實(shí)現(xiàn)對(duì)不同集合進(jìn)行統(tǒng)一遍歷操作的一種機(jī)制,只要給需要遍歷的數(shù)據(jù)結(jié)構(gòu)部署Iterator接口,通過調(diào)用該接口,或者使用消耗該接口的API實(shí)現(xiàn)遍歷操作。
迭代器模式
在接觸迭代器之前,一起先了解什么是迭代器模式,回想一下我們生活中的事例。我們?cè)趨⒂^景區(qū)需要買門票的時(shí)候,售票員需要做的事情,他會(huì)對(duì)排隊(duì)購票的每一個(gè)人依次進(jìn)行售票,對(duì)普通成人,對(duì)學(xué)生,對(duì)兒童都依次售票。售票員需要按照一定的規(guī)則,一定順序把參觀人員一個(gè)不落的售完票,其實(shí)這個(gè)過程就是遍歷,對(duì)應(yīng)的就是計(jì)算機(jī)設(shè)計(jì)模式中的迭代器模式。迭代器模式,提供一種方法順序訪問一個(gè)聚合對(duì)象中的各種元素,而又不暴露該對(duì)象的內(nèi)部表示。
為什么要有迭代器
回憶在我們的javascript中,可遍歷的結(jié)構(gòu)以及方式有很多。JavaScript 原有的表示“集合”的數(shù)據(jù)結(jié)構(gòu),主要是數(shù)組(Array)和對(duì)象(Object),ES6 又添加了Map和Set,這樣就有了四種數(shù)據(jù)集合,而遍歷這四種結(jié)構(gòu)都有不同的方法。舉個(gè)栗子,服務(wù)端提供數(shù)據(jù)給前端,前端進(jìn)行數(shù)據(jù)可視化工作,對(duì)數(shù)據(jù)進(jìn)行遍歷展示使用的for,但是由于業(yè)務(wù)的變化,使得后端返回的數(shù)據(jù)結(jié)構(gòu)發(fā)生變化,返回對(duì)象或者是set,map,導(dǎo)致前端遍歷代碼大量重寫。而迭代器的目的就是要標(biāo)準(zhǔn)化迭代操作。
如何部署迭代器接口
ES6為迭代器引入了一個(gè)隱式的標(biāo)準(zhǔn)化接口。Javascript許多內(nèi)建的數(shù)據(jù)結(jié)構(gòu),例如Array、Map、Set、String、TypedArray、函數(shù)的 arguments 對(duì)象、NodeList 對(duì)象都具備 Iterator 接口??梢酝ㄟ^在控制臺(tái)打印一個(gè)Array實(shí)例,查看其原型上具有一個(gè)Symbol.iterator屬性(Symbol.iterator其實(shí)是Symbol('Symbol.iterator')的簡(jiǎn)寫,屬性名是Symbol類型代表著這個(gè)屬性的唯一以及不可重寫覆蓋),它就是迭代器函數(shù),執(zhí)行這個(gè)函數(shù),就會(huì)返回一個(gè)迭代器對(duì)象。
雖然Javascript許多內(nèi)建的數(shù)據(jù)結(jié)構(gòu)已經(jīng)實(shí)現(xiàn)了該接口,還有些結(jié)構(gòu)是沒有迭代器接口的(比如對(duì)象),那怎么辦,我們需要寫迭代器,那么就需要知道迭代器是如何工作的。下面代碼實(shí)現(xiàn)的一個(gè)簡(jiǎn)單迭代器:
//迭代器就是一個(gè)函數(shù),也叫迭代器生成函數(shù) function Iterator(o){ let curIndex = 0; let next = () => { return { value: o[curIndex], done: o.length == ++curIndex } } //返回迭代對(duì)象,該對(duì)象有next方法 return { next } } let arr = [1,2] let oIt = Iterator(arr) oIt.next();//{value:1,done:false} oIt.next();//{value:2,done:false} oIt.next();// {value: undefined, done: true} oIt.next();// {value: undefined, done: true}
調(diào)用迭代器函數(shù),返回一個(gè)對(duì)象,該對(duì)象就是迭代器對(duì)象,對(duì)象上擁有next方法,每一次調(diào)用next方法,都會(huì)返回?cái)?shù)據(jù)結(jié)構(gòu)的當(dāng)前成員的信息。具體來說,就是返回一個(gè)包含value和done兩個(gè)屬性的對(duì)象。其中,value屬性是當(dāng)前成員的值,done屬性是一個(gè)布爾值,表示遍歷是否結(jié)束。
next()迭代
在上面調(diào)用next方法的栗子中,需要注意的是:
在獲得數(shù)組最后一位元素的時(shí)候,迭代器不會(huì)報(bào)告done:true,這時(shí)候需要再次調(diào)用next(),越過數(shù)組結(jié)尾的值,才能得到完成信號(hào)done:true。
通常情況下,在已經(jīng)迭代完畢的迭代器對(duì)象上繼續(xù)調(diào)用next方法會(huì)繼續(xù)返回{value: undefined, done: true}而不會(huì)報(bào)錯(cuò)。
可選的return()和throw()
遍歷器對(duì)象除了必須具有next方法,還可以具有可選的return方法和throw方法。
return方法被定義為向迭代器發(fā)送一個(gè)信號(hào),表明不會(huì)在消費(fèi)者中再提取出任何值。
Object.prototype[Symbol.iterator] = function () { let curIndex = 0; let next = () => { return { value: this[curIndex], done: this.length == curIndex++ } } return { next, return() { console.log('執(zhí)行return啦') return {} } } } let obj = { 0: 'a', 1: 'b', 2: 'c' } //自動(dòng)調(diào)用---遇到對(duì)迭代器消耗提前終止的條件 for (let item of obj) { if (item == 'c') { break } else { console.log(item) } } //自動(dòng)調(diào)用---拋出異常 for (let item of obj) { if (item == 'c') { throw new Error('Errow') } else { console.log(item) } } //手動(dòng)調(diào)用 let ot = obj[Symbol.iterator]() console.log(ot.return())
上面代碼中,throw方法的執(zhí)行可以在某種情況下自動(dòng)被調(diào)用,也可以手動(dòng)調(diào)用。throw方法主要向迭代器報(bào)告一個(gè)異常/錯(cuò)誤,一般配合生成器使用。
迭代器分類
迭代器分為內(nèi)部迭代器和外部迭代器。
- 內(nèi)部迭代器:本身是函數(shù),該函數(shù)內(nèi)部定義好迭代規(guī)則,完全接受整個(gè)迭代過程,外部只需要一次調(diào)用。例如Array.prototype.forEach方法、jQuery.each都是內(nèi)部迭代器。
- 外部迭代器:本身是函數(shù),執(zhí)行返回迭代對(duì)象,迭代下一個(gè)元素必須顯式調(diào)用。使用forEach遍歷,只可以一次性把數(shù)據(jù)全部拉取消耗,而迭代器可以用于以一次一步的方式控制行為,使得迭代過程更加靈活可控。
迭代器使用
實(shí)現(xiàn)迭代器接口后,如何進(jìn)行使用?
let arr = ['a', 'b']; let iter = arr[Symbol.iterator](); iter.next() // { value: 'a', done: false } iter.next() // { value: 'b', done: false } iter.next() // { value: undefined, done: true }
除了像上述代碼這樣單獨(dú)使用外,實(shí)現(xiàn)該接口的目的,就是為所有數(shù)據(jù)結(jié)構(gòu),提供一種統(tǒng)一的訪問機(jī)制。實(shí)現(xiàn)了該接口,就可以調(diào)用ES6中新增的通過調(diào)用Iterator 接口實(shí)現(xiàn)的API,例如for..of就是典型的消耗迭代器的API。下面具體看看for..of的實(shí)現(xiàn)原理:
let arr = [1,2,3]; for(let num of arr){ console.log(num); }
輸出結(jié)果為:1,2,3
for-of 循環(huán)首先會(huì)調(diào)用 arr 數(shù)組中Symbol.iterator 屬性對(duì)象的函數(shù),就會(huì)獲取到該數(shù)組對(duì)應(yīng)的迭代器,接下來 iterator.next()被調(diào)用,迭代器結(jié)果對(duì)象的 value 屬性會(huì)被放入到變量 num 中。數(shù)組中的數(shù)據(jù)項(xiàng)會(huì)依次存入到變量num 中,直到迭代器結(jié)果對(duì)象中的 done 屬性變成 true 為止,循環(huán)就結(jié)束。
for-of 循環(huán)完全刪除了for循環(huán)中追蹤集合索引的需要,更能專注于操作集合內(nèi)容。
ES6 規(guī)定,默認(rèn)的 Iterator 接口部署在數(shù)據(jù)結(jié)構(gòu)的Symbol.iterator屬性,或者說,一個(gè)數(shù)據(jù)結(jié)構(gòu)只要具有Symbol.iterator屬性,就可以認(rèn)為是“可遍歷的”(iterable)。就可以使用上述默認(rèn)會(huì)調(diào)用Iterator函數(shù)的API,而如果該數(shù)據(jù)結(jié)構(gòu)沒有提供實(shí)現(xiàn)這個(gè)接口(例如對(duì)象)又該怎么樣達(dá)到最大化的互操作性呢?那么就可以自己構(gòu)建符合這個(gè)標(biāo)準(zhǔn)的迭代器。
下面是一個(gè)為對(duì)象添加 Iterator 接口的例子:
let obj = { 0: 'a', 1: 'b', 2: 'c', length: 3, [Symbol.iterator]: function () { let curIndex = 0; let next = () => { return { value: this[curIndex], done: this.length == curIndex++ } } return { next } } } for (let item of obj) { console.log(item) }
如果把該對(duì)象的[Symbol.iterator]屬性刪除,那么就會(huì)報(bào)錯(cuò)Uncaught TypeError: obj is not iterable,告訴我們obj是不可被遍歷。
除了上面展示的for..of循環(huán)可以一個(gè)一個(gè)的消耗迭代器之外,還有其它ES6結(jié)構(gòu)也可以用來消耗迭代器。例如spread運(yùn)算符:
function f(x, y, z) { console.log(x, y, z) } f(...[2, 3, 1])
以及結(jié)構(gòu)賦值也可以部分或者完全消耗一個(gè)迭代器:
let arr = [1, 2, 3, 4, 5] var it = arr[Symbol.iterator]() //部分消耗 var [x, y] = it console.log(x, y) //打印1 2 //完全消耗 var [y, ...z] = it console.log(y, z) //打印3 [4,5]
JavaScript 默認(rèn)產(chǎn)生迭代器的API
產(chǎn)生迭代器對(duì)象,我們可以通過定義迭代器函數(shù)來生產(chǎn)迭代器對(duì)象,還可以調(diào)用JavaScript在內(nèi)置數(shù)據(jù)結(jié)構(gòu)中定義好的迭代器函數(shù)來生產(chǎn)。除此之外,對(duì)于數(shù)組以及ES6新增的幾個(gè)新的數(shù)據(jù)結(jié)構(gòu)MAP、Set,這些集合不僅本身已部署迭代器接口,還提供了API方法來產(chǎn)生迭代器對(duì)象。ES6 的數(shù)組、Set、Map 都部署了以下三個(gè)方法,調(diào)用后都返回遍歷器對(duì)象。
- entries() 返回一個(gè)遍歷器對(duì)象,用來遍歷[鍵名, 鍵值]組成的數(shù)組。
- keys() 返回一個(gè)遍歷器對(duì)象,用來遍歷所有的鍵名。
- values() 返回一個(gè)遍歷器對(duì)象,用來遍歷所有的鍵值。
數(shù)組的迭代器使用實(shí)例
下面是數(shù)組的迭代器接口使用:
let arr = [1,2,3,4] let arrEntires = arr.entries() arrEntires.next() //{value: [0, 1], done: false} let arrKeys = arr.keys() //對(duì)于數(shù)組,索引值就是鍵值 arrKeys.next() //{value: 0, done: false} let arrValues = arr.values() arrValues.next() //{value: 1, done: false}
下面代碼可以看出數(shù)組的for…of 遍歷的默認(rèn)迭代器接口是values
for(let item of [1,2,3]) { console.log(item)// [1,2,3] }
Set的迭代器使用實(shí)例
下面是Set的迭代器接口使用:
let set = new Set([1,2,3,4]) let setEntires = set.entries()//對(duì)于 Set,鍵名與鍵值相同。 setEntires.next() //{value: [1, 1], done: false} let setKeys = set.keys() setKeys.next() //{value: 1, done: false} let setValues = set.values() setValues.next() //{value: 1, done: false}
如下可以看出Set的默認(rèn)迭代器接口[Symblo.iterator]是values
for(let item of new Set([1,2,3,4])){ console.log(item)// [1,2,3,4] }
Map的迭代器使用實(shí)例
下面是Map的迭代器接口使用:
let map = new Map([[1,2],[3,4]]) let mapEntires = map.entries() mapEntires.next() //{value: [1, 2], done: false} let mapKeys = map.keys() mapKeys.next() //{value: 1, done: false} let mapValues = map.values() mapValues.next() //{value: 2, done: false}
Map 的默認(rèn)迭代器接口[Symblo.iterator]是 entries;
for(let item of new Map([[1,2],[3,4]])){ console.log(item)// [1,2] [3,4] }
為什么對(duì)象沒有內(nèi)置迭代器接口
在上面中,我們提及到對(duì)象沒有設(shè)置可迭代的默認(rèn)方法,是不可迭代對(duì)象,表現(xiàn)為其沒有[Symbol.iterator]屬性。雖然對(duì)象對(duì)我們來說,是鍵值存儲(chǔ)的一種方式,盡管沒有 map 那么好,key只可以是字符串,但是有的時(shí)候?qū)ο笠彩切枰坏模菫槭裁床唤o對(duì)象設(shè)置可迭代的默認(rèn)方法?
原因是因?yàn)椋瑢?duì)于對(duì)象的遍歷,需要考慮到遍歷是對(duì)象自身的屬性還是遍歷對(duì)象自身上的可枚舉屬性還是遍歷原型上的屬性還是遍歷原型上的可枚舉屬性還是連[Symbol.iterator]也希望遍歷出來。鑒于各方意見不一,并且現(xiàn)有的遍歷方式可以滿足,于是標(biāo)準(zhǔn)組沒有將[Symbol.iterator]加入。
生成迭代器對(duì)象的方法
在上面,我們嘗試過了為一個(gè)對(duì)象添加了Symbol.iterator方法,該方法就是該對(duì)象的遍歷器生成函數(shù),調(diào)用該函數(shù)會(huì)返回該對(duì)象的一個(gè)遍歷器對(duì)象。
除了上面在為對(duì)象添加遍歷器生成函數(shù)的這種根據(jù)迭代器協(xié)議直接生成迭代器對(duì)象的方式外,還有什么方式可以生成迭代器對(duì)象呢?有,它是一種特殊的函數(shù),叫生成器。
var it = {}; it[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; }; //可以被...遍歷,說明已經(jīng)部署成功 console.log([...it])// [1, 2, 3] let myIterator = it[Symbol.iterator]() console.log(myIterator.next())//{value: 1, done: false} console.log(myIterator.next())//{value: 2, done: false} console.log(myIterator.next())//{ value: 3, done: false } console.log(myIterator.next())//{ value: undefined, done: true }
上面代碼中,生成器函數(shù)沒有過多的代碼,只需要使用關(guān)鍵字yeild來返回每次next()的值。
生成器是一種特殊的函數(shù)形式,生成器函數(shù)的聲明語法為:
function *bar(){ // ... }
*前后可以有空格也可以沒有空格。生成器函數(shù)的聲明雖然和普通函數(shù)有區(qū)別,但是執(zhí)行和普通函數(shù)一樣,一樣可以傳參數(shù)。那它們的主要區(qū)別是什么呢?
函數(shù)是一段執(zhí)行特定任務(wù)的代碼塊,所以函數(shù)執(zhí)行,相當(dāng)于這一段代碼塊被執(zhí)行。函數(shù)開始執(zhí)行,在它執(zhí)行完之前不會(huì)被打斷,這段代碼塊將被全部執(zhí)行完。在ES6引入生成器之前函數(shù)的確是這樣執(zhí)行的,但是前面介紹到外部迭代器可以相比內(nèi)部迭代器對(duì)迭代過程進(jìn)行控制,什么時(shí)候需要消耗,迭代器對(duì)象再next一下即可。類似迭代過程,函數(shù)的執(zhí)行過程一樣可以控制,函數(shù)可以不需要一次性執(zhí)行完畢。
生成器函數(shù)的執(zhí)行會(huì)返回一個(gè)迭代器對(duì)象來控制該生成器函數(shù)執(zhí)行其代碼。因此,函數(shù)的執(zhí)行變得可控。還可以在生成器中使用新的關(guān)鍵字yield,用來標(biāo)示一個(gè)暫停點(diǎn)。迭代器除了可以控制函數(shù)執(zhí)行外,還可以在每一次暫停中雙向傳遞信息,暫停的時(shí)候生成器函數(shù)會(huì)返回一個(gè)值,恢復(fù)執(zhí)行的時(shí)候迭代器可以通過向next方法傳參向函數(shù)內(nèi)部傳遞一個(gè)值??梢岳斫鉃槎啻蝹鲄?,多個(gè)返回值。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Javascript同時(shí)聲明一連串(多個(gè))變量的方法
這篇文章主要給大家介紹了利用Javascript聲明一連串(也就是多個(gè))變量的方法,需要的朋友可以參考借鑒,下面來一起看看吧。2017-01-01JS基于正則實(shí)現(xiàn)數(shù)字千分位用逗號(hào)分隔的方法
這篇文章主要介紹了JS基于正則實(shí)現(xiàn)數(shù)字千分位用逗號(hào)分隔的方法,涉及javascript正則表達(dá)式操作數(shù)字的相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-06-06使用javascript將時(shí)間轉(zhuǎn)換成今天,昨天,前天等格式
這篇文章主要介紹了使用javascript將時(shí)間轉(zhuǎn)換成今天,昨天,前天等格式的相關(guān)資料,需要的朋友可以參考下2015-06-06