完全理解 for of 和 Iterator

本文主要來講下ES6Iterator,目的在於理解它的概念、做用、以及現有的應用,最後學以至用。
javascript

Iterator能夠說是ES6內至關重大的一個特性,也是不少其餘特性運行的基石。html

爲何Iterator地位如此之高呢?前端

從一個變量提及

var arr = ['紅','綠','藍'];

上面是一個普通的數組,若是我要獲取他的每一項數據,應該怎麼作?java

這個還不簡單,直接來個 for循環,若是你以爲循環 low,那就來個 forEach 唄。es6

ok,馬上擼串代碼web

//for 循環
for(var i=0;i<arr.length;i++){
    console.log(arr[i]);
}

//forEacth
arr.forEach(item=>{
    console.log(item);
});

// 結果 略

ok,沒毛病。後端

那我們繼續,請看下面代碼。給定一個字符串,若是我想輸出每個字符怎麼作?api

var str='1234567890';

有麼有搞錯,這簡單的讓人發笑。數組

能夠用 for in,也能夠用 for 循環,按照類數組方式來處理。微信

馬上擼串代碼

//for in
for(var s in str){
    console.log(str[s]);//s 是 屬性名稱【key】
}

//轉爲數組
for(var i =0;i<str.length;i++){
console.log(str[i]);
}

//或者轉換爲數組
Array.prototype.slice.call(str);

不過 for in 不是用來獲取數據的,他會遍歷對象上全部可枚舉的屬性,包括自身的和原型鏈上的,因此這個不保險。

emmm....沒啥問題,那我們繼續。

請看下面代碼,給定一個map對象,而後輸出它每一項數據。

var map = new Map();
map.set('zhang','1000w');
map.set('liu','200w');
map.set('sun','100w');

forEach 就妥妥的了。

map.forEach((val,key)=>{
    console.log(val,key);
})

到這裏看了這麼多如此簡單到使人髮指的提問,估計都有些坐不住了,要掀桌子走人了。請稍後,慢慢往下看。

發現問題

好了,在上一步幾個簡單的問題中,咱們的操做都是得到他們的每一項數據。

固然方法有不少種,實現方式也有不少,for 循環forEachfor in 啦。

可是有沒有發現一個問題,或者咱們站在一個更高的維度去看待,其實這些方法都不能通用,也就是說上面的這幾種集合數據不能使用統一的遍歷方法來進行數據獲取。

誰說的,能統一呀,均可以用 forEach來遍歷,數組和map 自己就支持,字符串我直接轉爲數組後能夠了。

ok,這沒什麼毛病。

可是每次都要轉換,還要封裝,還有可能要侵入原型。

那有沒有一種更好的,通用方法,讓開發者用的更舒服,更爽呢?

答案是確定的,es5的時候還沒出現,升級到 es6就有了。

NB的 for of,扒一扒

這個能夠對不一樣數據結構進行統一遍歷的方式就是 es6for of 循環。

for of 循環 和 古老的for 循環很像呀。不就是個新增語法麼。

引用阮大佬書中一句話,相信一看便知。

ES6 借鑑 C++、Java、C# 和 Python 語言,引入了for...of循環。做爲遍歷全部數據結構的統一的方法。

關鍵在於統一,另外一個就是直接取值,簡化操做,不須要在聲明和維護什麼變量、對數據作轉換。

原來for of 這麼牛,for 循環搞不定的你能夠搞定。

爲何 for of 能具有這個能力,能夠爲不一樣的數據結構提供一種統一的數據獲取方式。

for of 真的這麼強大嗎,他背後的機制是什麼?

其實for of 並非真的強大,他只是一種ES6新的語法而已。

並非全部的對象都能使用 for of,只有實現了Iterator接口的對象纔可以使用 for of 來進行遍歷取值。

因此說 for of 只是語法糖,真正的主角是Iterator

What ? Iterator.....

主角登場- Iterator 迭代器

Iterator 是一種接口,目的是爲不一樣的數據結構提供統一的數據訪問機制。也能夠理解爲 Iterator 接口主要爲 for of 服務的,供for...of進行消費。

其實在不少後端語言多年前早已存在 Iterator 這個特性,如 java、C++、C#等。

既然他是一種接口,那咱們應該怎樣實現這個接口呢?實現規則是什麼樣的呢?

由於 javascript 語言裏沒有接口的概念,這裏咱們能夠理解成它是一種特殊的對象 - 迭代器對象,返回此對象的方法叫作迭代器方法。

首先他做爲一個對象,此對象具備一個next方法,每次調用 next 方法都會返回一個結果值。

這個結果值是一個 object,包含兩個屬性,valuedone

value表示具體的返回值,done 是布爾類型的,表示集合是否遍歷完成或者是否後續還有可用數據,沒有可用數據則返回 true,不然返回 false

另外內部會維護一個指針,用來指向當前集合的位置,每調用一次 next 方法,指針都會向後移動一個位置(能夠想象成數組的索引)。

看下代碼實現

function getIterator(list) {
    var i = 0;
    return {
        next: function() {
            var done = (i >= list.length);
            var value = !done ? list[i++] : undefined;
            return {
                donedone,
                value: value
            };
        }
    };
}
var it = getIterator(['a''b''c']);
console.log(it.next()); // {value: "a"donefalse}
console.log(it.next()); // {value: "b"donefalse}
console.log(it.next()); // {value: "c"donefalse}
console.log(it.next()); // "{ value: undefined, done: true }"
console.log(it.next()); // "{ value: undefined, done: true }"
console.log(it.next()); // "{ value: undefined, done: true }"

上面代碼即是根據迭代器的基本概念衍生出來的一個模擬實現。

  • getIterator方法返回一個對象 - 可迭代對象
  • 對象具備一個 next 方法, next 方法內部經過閉包來保存指針 i 的值,每次調用 next 方法 i 的值都會 +1.
  • 而後根據 i 的值從數組內取出數據做爲 value,而後經過索引判斷獲得 done的值。
  • i=3的時候,超過數組的最大索引,無可用數據返回,此時done 爲 true,遍歷完成。

可迭代對象

到這裏咱們已經大概瞭解了 Iterator, 以及如何建立一個迭代器對象。可是他和 for of 有什麼關係呢?

for of 運行機制

for of執行的時候,循環過程當中引擎就會自動調用這個對象上的迭代器方法, 依次執行迭代器對象的 next 方法,將 next 返回值賦值給 for of 內的變量,從而獲得具體的值。

我以爲上面一句話包含了一個重要的信息- 「對象上的迭代器方法」。

實現可迭代對象

對象上怎麼會有迭代器方法呢?

ES6裏規定,只要在對象的屬性上部署了Iterator接口,具體形式爲給對象添加Symbol.iterator屬性,此屬性指向一個迭代器方法,這個迭代器會返回一個特殊的對象 - 迭代器對象。

而部署這個屬性而且實現了迭代器方法後的對象叫作可迭代對象

此時,這個對象就是可迭代的,也就是能夠被 for of 遍歷。

Symbol.iterator,它是一個表達式,返回Symbol對象的iterator屬性,這是一個預約義好的、類型爲 Symbol 的特殊值。

舉個例子

普通的對象是不能被 for of 遍歷的,直接食用會報錯。

var obj = {};

for(var k of obj){
    
}

obj 不是可迭代的對象。

那麼咱們來,讓一個對象變成可迭代對象,按照協議也就是規定來實現便可。

iterableObj 對象上部署 Symbol.iterator屬性,而後爲其建立一個迭代器方法,迭代器的規則上面咱們已經說過啦。

var iterableObj = {
    items:[100,200,300],
    [Symbol.iterator]:function(){
    var self=this;
    var i = 0;
    return {
        next: function() {
            var done = (i >= self.items.length);
            var value = !done ? self.items[i++] : undefined;
            return {
                donedone,
                value: value
            };
        }
    };
    }
}

//遍歷它
for(var item of iterableObj){
    console.log(item); //100,200,300
}

就這麼簡單,上面這個對象就是可迭代對象了,能夠被 for of 消費了。

Iterator 原生應用場景

咱們再回到最開始使用 for of 來進行遍歷字符串、數組、map,咱們並無爲他們部署Iterator接口,仍然可使用 for of 遍歷。

這是由於在 ES6中有些對象已經默認部署了此接口,不須要作任何處理,就可使用 for of 來進行遍歷取值。

不信?咿,你好難搞,我不要你說 - 信,我要我說 - 信。

看看能不能拿到它們的迭代器。

數組

//數組
var arr=[100,200,300];

var iteratorObj=  arr[Symbol.iterator]();//獲得迭代器方法,返回迭代器對象

console.log(iteratorObj.next());
console.log(iteratorObj.next());
console.log(iteratorObj.next());
console.log(iteratorObj.next());


字符串

由於字符串自己的值是有序的,而且具備類數組的特性,支持經過索引訪問,因此也默認部署了iterator接口。

var str='abc';

var strIteratorObj = str[Symbol.iterator]();//獲得迭代器

console.log(strIteratorObj.next());
console.log(strIteratorObj.next());
console.log(strIteratorObj.next());
console.log(strIteratorObj.next());

或者直接看原型上是否部署了這個屬性便可。

arguments 類數組

函數內的arguments 是一個類數組,他也支持 for of,由於他內部也部署了Iterator 接口。

咱們都知道對象是默認沒有部署這個接口的,因此arguments這個屬性沒有在原型上,而在在對象自身的屬性上。

function test(){
    var obj = arguments[Symbol.iterator]();
   console.log( arguments.hasOwnProperty(Symbol.iterator));
   console.log( arguments);
   console.log(obj.next());
}

test(1,2,3);

總結來講,已默認部署 Iterator 接口的對象主要包括數組、字符串、Set、Map 、相似數組的對象(好比arguments對象、DOM NodeList 對象)。

代碼驗證略,都是一個套路,很少說。

Iterator 另一個做用

Iterator除了能夠爲不一樣的數據結構提供一種統一的數據訪問方式,還有沒有發現其餘的做用?

那就是數據可定製性,由於咱們能夠隨意的控制迭代器的 value 值。

好比:數組自己就是一個可迭代的,咱們能夠覆蓋他的默認迭代器。

var arr=[100,200,300];

for(var o of arr){
    console.log(o);
}

for of 數組默認輸出以下

通過咱們的改造

var arr=[100,200,300];
arr[Symbol.iterator]=function(){

    var self=this;
    var i = 0;
    return {
        next: function() {
            var done = (i >= self.length);
            var value = !done ? self[i++] : undefined;
            return {
                donedone,
                value: value
            };
        }
    };
}

for(var o of arr){
    console.log(o);
}

對象爲何沒有默認部署

對象可能有各類屬性,不像數組的值是有序的。

因此遍歷的時候根本不知道如何肯定他們的前後順序,因此須要咱們根據狀況手動實現。

擴展

for of 中斷

咱們都知道普通的 for 循環是能夠隨時中斷的,那 for of 是否能夠呢?

答案是確定的,for of機制兼顧了forforEach

迭代器除了必須next 方法外,還有兩個可選的方法 returnthrow方法。

若是 for of 循環提早退出,則會自動調用 return 方法,須要注意的是 return 方法必須有返回值,且返回值必須是 一個object

ps:以拋出異常的方式退出,會先執行 return 方法再拋出異常。

var iterableObj = {
    items:[100,200,300],
    [Symbol.iterator]:function(){
    var self=this;
    var i = 0;
    return {
        next: function() {
            var done = (i >= self.items.length);
            var value = !done ? self.items[i++] : undefined;
            return {
                donedone,
                value: value
            };
        },
        return(){
            console.log('提早退出');
            return {//必須返回一個對象
                done:true
            }
        }
    };
    }

}

for(var item of iterableObj){
    console.log(item);
    if(item===200){
        break;
    }
}

for(var item of iterableObj){
    console.log(item);
    throw new Error();
}

ps:throw 方法這裏先不說,這裏不是他的用武之地,後續文章見。

不止 for of

除了 for of 執行的時候會自動調用對象的Iterator方法,那麼ES6裏還有沒有其餘的語法形式?

解構賦值

對可迭代對象進行解構賦值的時候,會默認調用Symbol.iterator方法。

//字符串
var str='12345';
var [a,b]=str;
console.log(a,b); // 1 2

//map
var map = new Map();
map.set('我','前端');
map.set('是','技術');
map.set('誰','江湖');
map.set('做者','zz_jesse');

var [d,e]=map;
console.log(d,e);
//["我""前端"] ["是""技術"]
....

一樣若是對一個普通對象進行解構,則會報錯。

由於普通對象不是可迭代對象。

var [d,e]={name:'zhang'};

從一個自定義的可迭代對象進行解構賦值。

var iterableObj = {
    items:['紅','綠','藍'],
    [Symbol.iterator]:function(){
    var self=this;
    var i = 0;
    return {
        next: function() {
            var done = (i >= self.items.length);
            var value = !done ? self.items[i++] : undefined;
            return {
                donedone,
                value: value
            };
        }
     };
   }
}

var [d,e]=iterableObj;
console.log(d,e);//紅 綠 

解構賦值的變量的值就是迭代器對象的 next 方法的返回值,且是按順序返回。

擴展運算符

擴展運算符的執行(...)也會默認調用它的Symbol.iterator方法,能夠將當前迭代對象轉換爲數組。

//字符串
var str='1234';
console.log([...str]);//[1,2,3,4]  轉換爲數組


//map 對象
var map=new Map([[1,2],[3,4]]);
[...map] //[[1,2],[3,4]

//set 對象
var set=new Set([1,2,3]);
[...set] //[1,2,3]

通用普通對象是不能夠轉爲數組的。

var obj={name:'zhang'};
[..obj]//報錯

做爲數據源

做爲一些數據的數據源,好比某些api 方法的參數是接收一個數組,都會默認的調用自身迭代器。

例如:Set、Map、Array.from 等

//爲了證實,先把一個數組的默認迭代器給覆蓋掉

var arr=[100,200,300];

arr[Symbol.iterator]=function(){

    var self=this;
    var i = 0;
    return {
        next: function() {
            var done = (i >= self.length);
            var value = !done ? self[i++] : undefined;
            return {
                donedone,
                value: value+'前端技術江湖' //注意這裏
            };
        }
    };

}

for(var o of arr){
    console.log(o);
}

// 100前端技術江湖
// 200前端技術江湖
// 300前端技術江湖

已覆蓋完成

//生成 set  對象
var set  = new Set(arr);
//調用 Array.from方法
Array.from(arr);

yield*  關鍵字

yield*後面跟的是一個可遍歷的結構,執行時也會調用迭代器函數。

let foo = function* () {
  yield 1;
  yield* [2,3,4];
  yield 5;
};

yield 須要着重說明, 下一節再詳細介紹。

判斷對象是否可迭代

既然可迭代對象的規則必須在對象上部署Symbol.iterator屬性,那麼咱們基本上就能夠經過此屬來判斷對象是否爲可迭代對象,而後就能夠知道是否能使用 for of 取值了。

function isIterable(object) {
    return typeof object[Symbol.iterator] === "function";
}
console.log(isIterable('abcdefg')); // true
console.log(isIterable([1, 2, 3])); // true
console.log(isIterable("Hello")); // true
console.log(isIterable(new Map())); // true
console.log(isIterable(new Set())); // true
console.log(isIterable(new WeakMap())); // false
console.log(isIterable(new WeakSet())); // false

總結

ES6的出現帶來了不少新的數據結構,好比 Map ,Set ,因此爲了數據獲取的方便,增長了一種統一獲取數據的方式 for of 。而 for of 執行的時候引擎會自動調用對象的迭代器來取值。

不是全部的對象都支持這種方式,必須是實現了Iterator接口的才能夠,這樣的對象咱們稱他們爲可迭代對象。

迭代器實現方式根據可迭代協議,迭代器協議實現便可。

除了統一數據訪問方式,還能夠自定義獲得的數據內容,隨便怎樣,只要是你須要的。

迭代器是一個方法, 用來返回迭代器對象。

可迭代對象是部署了 Iterator 接口的對象,同時擁有正確的迭代器方法。

ES6內不少地方都應用了Iterator,平時能夠多留意觀察,多想一步。

是結束也是開始

到這裏咱們已經能夠根據迭代器的規則自定義迭代器了,但實現的過程有些複雜,畢竟須要本身來維護內部指針,有很多的邏輯處理,不免會出錯。

那有沒有更優雅的實現方式呢?

有,那就是-Generator -生成器 。

let obj = {
  * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
  }
};

for (let x of obj) {
  console.log(x);
}
// "hello"
// "world"

能夠看出它很是簡潔,無需過多代碼就能夠生成一個迭代器。

它除了能夠做爲生成迭代器的語法糖,他還有更多神奇的能力。

此次就先搞定Iterator,下次搞 Generator

練習

若是以爲本文有收穫的話,能夠試着作作下面的練習題,加深下理解,而後在評論內寫上你的答案。

  1. 寫一個迭代器(Iterator)對象 。
  2. 自定義一個可迭代對象。
  3. 說說你對 Iterator的理解,總結性輸出下。

參考

https://es6.ruanyifeng.com/#docs/iterator

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols

https://www.cnblogs.com/xiaohuochai/p/7253466.html

月初福利

每個月1號組織小夥伴們來抽獎。

給本文發評論或者點贊就能夠參與抽獎啦




僅限於關注本號小夥伴參與哦


點個『在看』支持下 


本文分享自微信公衆號 - 前端技術江湖(bigerfe)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索