JavaScript可迭代對象詳細(xì)介紹
1、迭代器
迭代器是借鑒C++等語言的概念,迭代器的原理就像指針一樣,它指向數(shù)據(jù)集合中的某個元素,你可以獲取它指向的元素,也可以移動它以獲取其它元素。迭代器類似于數(shù)組中下標(biāo)的拓展,各種數(shù)據(jù)結(jié)構(gòu),如鏈表(List)、集合(Set)、映射(Map)都有與之對應(yīng)的迭代器。
JS中的迭代器是專門為了遍歷這一操作設(shè)計的。每次獲取到的迭代器總是初始指向第一個元素,并且迭代器只有next()
一種行為,直到獲取到數(shù)據(jù)集的最后一個元素。我們無法靈活移動迭代器的位置,所以,迭代器的任務(wù),是按某種次序遍歷數(shù)據(jù)集中的元素。
JS規(guī)定,迭代器必須實現(xiàn)next()
接口,它應(yīng)該返回當(dāng)前元素并將迭代器指向下一個元素,返回的對象格式為{value:元素值, done:是否遍歷結(jié)束}
,其中,done是一個布爾值。done
屬性為true的時候,我們默認(rèn)不會去讀取value
, 所以最后返回的經(jīng)常是{value: undifined, done: true}
,注意,返回類似{value: 2, done: true}
不會導(dǎo)致錯誤,但是因為done
設(shè)置為true,在for...of
等操作中都會忽略value
的值。因此,done:false
和value:undifined
可以省略。一個簡單的JS迭代器像這樣:
let iter = { i: 0, next() { if (this.i > 10) return { done: true }; return { value: this.i++ }; } } //手動使用迭代器 console.log(iter.next()); //{ value: 0 } console.log(iter.next()); //{ value: 1 } while (true) { let item = iter.next(); if (!item.done) { console.log(item.value); //打印從2到10 } else { break; } }
可以看到,迭代器與普通的JS對象沒有區(qū)別,它就是一個用于實現(xiàn)迭代的對象。手動操作迭代器并沒有什么實用性,迭代器的作用是附著在對象上,讓一個對象,或者數(shù)據(jù)結(jié)構(gòu)成為可迭代對象。
2、迭代器接口與可迭代對象
迭代器接口是我們獲取對象迭代器時默認(rèn)調(diào)用的接口,一個實現(xiàn)了迭代接口的對象即是可迭代對象。JS的默認(rèn)迭代接口是[Symbol.iterator]
, 一個對象實現(xiàn)了[Symbol.iterator]
接口就成為了可迭代對象。
[Symbol.iterator]
是一個特殊的Symbol
屬性,它用于JS內(nèi)部檢測一個對象是否為可迭代對象。接口一詞的含義代表它是一個函數(shù),其結(jié)果應(yīng)該放回一個迭代器。結(jié)合上面迭代器必須要有next()
操作,所以,對可迭代對象,調(diào)用鏈iterableObj[Symbol.iterator]().next()
應(yīng)該是可行的。數(shù)組是最具代表性的可迭代對象,讓我們拿數(shù)組測試一下:
arr = [1, '2', {a: 3}]; let arrIt = arr[Symbol.iterator](); //獲取數(shù)組迭代器 console.log(arrIt.next()); //{ value: 1, done: false } console.log(arrIt.next()); //{ value: '2', done: false } console.log(arrIt.next()); //{ value: { a: 3 }, done: false } console.log(arrIt.next()); //{ value: undefined, done: true }
可以看到,迭代器的next()
接口確實如愿工作,并且返回上述的結(jié)構(gòu)。
3、自定義可迭代對象
現(xiàn)在,讓我們來實現(xiàn)幾個可迭代對象,這十分簡單,只要:
- 實現(xiàn)對象的迭代器接口
[Symbol.iterator]()
,注意它是一個方法, - 在迭代器接口中返回一個迭代器對象,
- 確保迭代器對象具有
next()
接口,并且返回{value: v, done: bool}
的結(jié)構(gòu)。
3.1、可迭代的Range對象
作為第一個可迭代對象,我們來實現(xiàn)類似python中的range(from, to)
,不過這里使用Range
對象來封裝一個左閉右開的范圍[from, to)
。
function Range(from, to) { this.from = from; this.to = to; } Range.prototype[Symbol.iterator] = function () { //返回一個迭代器對象 return { cur: this.from, to: this.to, //保證next()中可以獲取 next() { return (this.cur < this.to) ? { value: this.cur++, done: false } : { value: undefined, done: true }; } } } let range = new Range(5, 11); //創(chuàng)建一個range對象 //使用for...of循環(huán) for (const num of range) { console.log(num); //依次打印5,6,7,8,9,10 } //使用 let arrFromRange = Array.from(range); console.log(arrFromRange); //[5,6,7,8,9,10]
3.2、使用Generator函數(shù)作為迭代器接口
因為Generator函數(shù)產(chǎn)生的generator對象是一種特殊的迭代器,所以我們可以很方法地使用Generator函數(shù)作為對象的迭代器接口。使用Generator函數(shù)改寫上面的迭代器接口:
Range.prototype[Symbol.iterator] = function* () { for (let i = this.from; i < this.to; i++) { yield i; } }
這種寫法更加簡潔易懂,是最為推薦的寫法,Generator函數(shù)中產(chǎn)生的值就是遍歷過程中得到的值。
3.3、可迭代的List
接下來,我們自定義一個鏈表節(jié)點List
,在此我們省去不必要的接口。
function ListNode(value) { this.value = value; this.nextNode = null; } function List(root) { this.cur = this.root = root; } //List的next接口 List.prototype.next = function () { if (this.cur) { //非尾哨兵節(jié)點 let curNode = { value: this.cur.value }; this.cur = this.cur.nextNode; return curNode; } else { return { done: true }; } }
List.next()
實現(xiàn)了將鏈表指針后移的操縱,并且返回了移動前節(jié)點的值,你可能注意到,我特意讓返回的對象格式與迭代器返回結(jié)果一致,下面你將看到這么做的原因?,F(xiàn)在我們讓List
變成可迭代,按照之前的做法,使得List[Symbol.iterator]().next()
能夠返回正確的{value: v, done: true}
格式。是的,我們已經(jīng)畫好龍了,就差一個點睛之筆:
List.prototype[Symbol.iterator] = function () { return this; }
隨手寫一個測試:
let a = new ListNode('a'), b = new ListNode('b'), c = new ListNode('c'); a.nextNode = b, b.nextNode = c; let list = new List(a); for (let i of list) { console.log(i); //a,b,c }
Perfect! List
的迭代器接口返回了它自己,利用了自身的next()
接口完成迭代操作,也就是說List
的迭代器是List
本身,我都為自己構(gòu)思的例子覺得巧妙。
3.3、可迭代的迭代器
上面的List
例子會讓人覺得有點牽強,list.next()
的返回值為了迎合迭代器的要求,讓平時不得不使用let curValue = list.next().value
來正確接收返回的節(jié)點值,確實。但是,這種做法在一種時候讓人覺得眼前一亮——讓迭代器稱為可迭代對象,因為自己就是可迭代器,讓自己成為自己的迭代器,就像1=1
一樣正確自然。
回到開頭埋下的雷,我們只需要稍加改動
let iter = { i: 0, next() { if (this.i > 10) return { done: true }; return { value: this.i++ }; }, //讓迭代器的迭代器接口返回自身 [Symbol.iterator]() { return this; } } //這樣,你就可以把迭代器用在任何可迭代對象的地方 for (let i of iter) { console.log(i); }
這樣,這個迭代器本身也是可迭代的。需要注意的是,內(nèi)置可迭代類型的迭代器也都是可迭代的,類似for(let i of arr[Symbol.iterator]())
的操作是可行,其原理是讓Array的迭代器繼承Array.prototype
。其它類型也有類似的繼承,如Generator與generator對象。
4、可迭代對象的意義
可迭代對象作為數(shù)組的擴充,具有非凡的意義。在以前,對一個需要操作一組數(shù)據(jù)的接口,只有數(shù)組這種結(jié)構(gòu)能支持,非數(shù)組對象必須通過某種方式轉(zhuǎn)化為數(shù)組,完成之后,還可能需要還原成原來的結(jié)構(gòu),這種繁瑣的來回切換很不理想。有了可迭代對象的概念,這類操作可以接受一個可迭代對象,數(shù)組是可迭代對象,所以之前的數(shù)組參數(shù)是仍然可行的,在此之上,任何實現(xiàn)了可迭代接口的對象,也能正常處理。考慮這個下面例子:
function sumInArray(arr){ let sum=0; for(let i = 0;i<arr.length;i++){ sum+=arr[i]; } return sum; } function sumInIterable(iterable){ let sum = 0; for(let num of iterable){ sum+=num; } return sum; }
sumInArray()
只對數(shù)組友好,而sumInIterable()
是所有可迭代對象通用的,例如我們前面的Range
對象,iter
對象。是的,數(shù)組到可迭代對象的提升,代表了接口的通用性的提升。這個道理太淺顯易懂,以至于你可能覺得我說廢話,那么,請問你在接口設(shè)計的時候,會考慮能否使用可迭代對象代替數(shù)組嗎?個人認(rèn)為這種提升很多時候是有益的,特別在一些應(yīng)用場景較多的接口,我發(fā)現(xiàn)很多ES6操作也是基于可迭代對象。如果有什么看法,也歡迎評論區(qū)探討。
5、使用可迭代對象
先認(rèn)識JS內(nèi)建的可迭代對象:
非weak的數(shù)據(jù)結(jié)構(gòu)
,包括Array
,Set
,Map
。DOM中的NodeList對象
。String對象
。函數(shù)的arguments屬性
。
再了解哪些操作是基于可迭代對象的:
for...of
語法...iterable
:展開語法和解構(gòu)賦值yield*
語法Map
,Set
,WeakMap
,WeakSet
的構(gòu)造器。為什么沒有Array?因為Array直接把它對象當(dāng)成元素了,但是有Array.from(iterable)
。Object.fromEntries(iterable)
,每次迭代結(jié)果應(yīng)該是一個對應(yīng)鍵值對的二元數(shù)組,與Map
的迭代結(jié)果吻合,常有let obj = Object.fromEntries(map)
實現(xiàn)從map到object的轉(zhuǎn)化。promise.all(iterable)
和promist.race(iterable)
.
我認(rèn)為對這些方法的具體使用不該放在這里,如果使用過它們,自然了解,只需要記住它們對任何可迭代對象都是支持的。如果不認(rèn)識它們我也說不完,你應(yīng)該一一學(xué)習(xí)去。
6、后記
到此這篇關(guān)于JavaScript可迭代對象詳細(xì)介紹的文章就介紹到這了,更多相關(guān)JS可迭代對象內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
對layer彈出框中icon數(shù)字參數(shù)的說明介紹
今天小編就為大家分享一篇對layer彈出框中icon數(shù)字參數(shù)的說明介紹,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09JavaScript雙向鏈表實現(xiàn)LRU緩存算法的示例代碼
本文主要介紹了JavaScript雙向鏈表實現(xiàn)LRU緩存算法的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01JavaScript循環(huán)_動力節(jié)點Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了JavaScript循環(huán)的相關(guān)資料,JavaScript的兩種循環(huán)方式,一種是for循環(huán),另while一種是循環(huán)具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06JS中的算法與數(shù)據(jù)結(jié)構(gòu)之隊列(Queue)實例詳解
這篇文章主要介紹了JS中的算法與數(shù)據(jù)結(jié)構(gòu)之隊列(Queue),結(jié)合實例形式詳細(xì)分析了javascript中隊列的概念、原理、定義及常見操作技巧,需要的朋友可以參考下2019-08-08javascript將json格式數(shù)組下載為excel表格的方法
下面小編就為大家分享一篇javascript將json格式數(shù)組下載為excel表格的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2017-12-12