淺談ES6中迭代器和生成器

helios發表於2019-03-12

文章首發


本文已經預設你已經知道generator是什麼以及for...of和資料型別map怎麼用了。

前ES6的時代怎麼遍歷

先來一道思考題:

通過下面的變數

  1. 尋找xiaohong(假設名稱唯一)是否喜歡basketball
  2. 所有同學的名字

const students = {
    xiaohong: {
        age: '22',
        fav: ['sleep', 'basketball'],
        teachers: {
            english: 'daming',
            chinense: 'helios',
            math: ['helios2', 'helios3']
        }
    },
    xiaoming: {
        age: '22',
        fav: ['sleep', 'basketball', 'football'],
        teachers: {
            english: 'daming',
            chinense: 'helios',
            math: ['helios2', 'helios3']
        }
    },
}

複製程式碼

對於第一個問題來說,我們可以使用各種迴圈語句: for/while


for (let i =0 ;i < students[xiaoming].fav.length; i++) {
  if (students[xiaoming].fav[i] === 'basketball') console.log(true)
}


let i = 0;
while(i++ < students[xiaoming].fav.length) {
  if (students[xiaoming].fav[i] === 'basketball') console.log(true)
}

複製程式碼

for...of


for (let item of students[xiaoming].fav ) {
  if (item === 'basketball') console.log(true)
}

複製程式碼

那麼對於第二個問題來說,因為for/while是不能遍歷物件的,所以行不通,但是物件有一個專屬的遍歷方法 for...in

我們來看一下怎麼通過for...in來遍歷:


for (let stu in students) {
   console.log(stu)
}

複製程式碼

你可能會想了,通過for...in去遍歷陣列會怎樣呢? 我們看一下通過for...in去遍歷:

for (let item in students[xiaoming].fav) {
   console.log(item)
  // 或者去判斷
}

複製程式碼

哎呀,通過for...in不也照樣能實現陣列的遍歷麼,那為什麼不歸結到陣列的遍歷裡面去呢! 這裡面還有一些細節需要去了解(這也是上面的“物件有一個專屬的遍歷方法”為什麼加粗),我們通過一段程式碼去解釋:


const num = [5, 6, 7]
for (let i in  num) {console.log(i + 1)}

// 01
// 11
// 21

複製程式碼

這是因為for-in 是為普通物件({key: value})設計的,所以只能遍歷到字串型別的鍵。

還有下面這個雖然不常用,但是也是不得不說的:


const arr = [5, 6, 7]
arr.foo = function() {}
for (let i in arr) {
    console.log(i)
}

// 5
// 6
// 7
// foo !!!

複製程式碼

foo屬於arr上面的方法,被遍歷出來是說的過去的。

那麼用for...of我們來看看會怎麼樣


for (let stu of students){}
// Uncaught TypeError: students is not iterable

複製程式碼

is not iterable,這個iterable是神馬東西,我們接下來下面一步步的看。

先從可迭代(iterable)和迭代器(iterator)說起

iterable是ES6對iteration(迭代/遍歷)引入的介面。

如果一個物件被視為iterable(可迭代)那麼它一定有一個Symbol.iterator屬性,這個屬性返回一個iterator(迭代器)方法,這個方法返回一個規定的物件(這個後面會說)。也就是說iterableiterator的工廠,iterable能夠建立iteratoriterator是用於遍歷資料結構中元素的指標。

兩者之間的關係

Axel Rauschmaye大神的圖簡直不能再清晰了。

image

資料消費者: javascript本身提供的消費語句結構,例如for...of迴圈和spread operator (...) 資料來源: 資料消費者能夠通過不同的源(Array,string)得到供資料消費者消費的值;

資料消費者支援所有的資料來源這是不可以行的,因為還可能增加新的資料消費者資料來源。因此ES6引入了Iterable介面資料來源去實現,資料消費者去使用

可迭代協議(iterable protocol)和迭代器協議(iterator protocol)

可迭代協議(iterable protocol)

可迭代協議(iterable protocol) 是允許js物件能夠自定義自己的迭代行為。

簡單的說只要物件有Symbol.iterator這個屬性就是可迭代的,我們可以通過重寫(一些物件實現了iterable,比如Array,string)/新增(對於沒有實現iterable的物件比如object,可以新增這個屬性,使之變為可迭代的)該熟悉使之變為可迭代的。

當一個物件需要被迭代(for...of 或者 spread operator )的時候,他的Symbol.iterator函式被呼叫並且無引數,然後返回一個迭代器。

迭代器協議(iterator protocol)

迭代器協議定義了一種標準的方式來產生一個有限或無限序列的值。

當一個物件被認為是一個迭代器的時候,它會至少實現next()方法,next()方法返回兩個屬性value(d迭代器返回的值)和done(迭代時候已經結束)。

還有幾個可選的方法可以被實現,具體請看:sec-iterator-interface

iterable協議iterator協議還有next之間的關係

image

來源於網路

然後談談ES6中的for...of說起

再文章的最開始我們已經說了再前ES6的時候,如何去遍歷。 現在我們說說ES6新增的for...of的作用。

for...in

在前面也已經說了,在ES6之前遍歷object的時候用for...in迴圈,for...in會遍歷物件上所有可列舉的值(包括原型(prototype)上的屬性),比如下面這樣:

function foo() {
    this.name = 'helios'
}

foo.prototype.sayName = function() {
    return this.name;
}
var o = new foo();
for (var i in o) {
    console.log(i)
}
// name
// sayName
複製程式碼

如果我們只想遍歷物件自身的屬性,可以使用hasOwnProperty,如下:


function foo() {
    this.name = 'helios'
}

foo.prototype.sayName = function() {
    return this.name;
}
var o = new foo();
for (var i in o) {
    if (!o.hasOwnProperty(i)) continue;
    console.log(i)
}
複製程式碼

如果我們不想讓一個物件的屬性,在for...in中不被遍歷出來,可是使用Object.defineProperty來定義物件上的屬性是否可別列舉(更多的屬性請看:Object.defineProperty()),具體如下面程式碼:

var obj = {name: 'helios'}

Object.defineProperty(obj, 'age', {
    enumerable: false
})

for (var i in obj) {
    console.log(i)
}

複製程式碼

在這一小節的最後我們來說說for...in中的in操作符的含義:

prop in obj

  • 含義: 判斷prop是否在obj中
  • prop:物件的key屬性的型別(string / Symbol)
  • 返回值: boolean

我們來看一組例子:

var o = {
    name: 'heliso'
}
console.log('name' in o) // true
console.log(Symbol.iterator in o) // false
console.log('toString' in o)  // true

複製程式碼

這個操作符雖然也適用於陣列,但是儘量還是不要用在陣列中,因為會比較奇怪,如下程式碼:

var arr = [6, 7,8]

console.log(7 in arr)  // false
console.log(1 in arr)  // true
console.log('length' in arr)  // true

複製程式碼

主要是前兩個比較奇怪對不對,因為對於陣列prop代表的是陣列的索引而為其存在的值。 按照這樣的思路,正在看的讀者你能思考一下in操作符在字串中是怎麼的模式麼?

for...of能遍歷的集合

只要是實現了Interable介面的資料型別都能被遍歷。

javascript內部實現的有:

  • Array
  • String
  • Map
  • Set
  • arguments
  • DOM data structures

並不是所有的iterable內容都來源於資料結構,也能通過在執行中計算出來,例如所有ES6的主要資料結構有三個方法能夠返回iterable物件。

  • entries() 返回一個可遍歷的entries
  • keys() 返回一個可遍歷的 entries 的keys。
  • values() 返回一個可遍歷的 entries 的values。

如果for...of不能遍歷怎麼辦

那就資料結構(資料來源)去實現iterable就可以了。

用通俗的話說就是,你如果要遍歷一個物件的話,有一下幾個步驟:

  1. 物件如果沒實現Symbol.iterator那就去實現
  2. 物件的Symbol.iterator函式要返回一個iterator
  3. iterator返回一個物件,物件中至少要包含一個next方法來獲取
  4. next方法返回兩個值valuedone

現在說說怎麼使object變為可迭代的

上面我們已經鋪墊了這麼多了,我們說了javascript中object是不能被迭代了,因為沒有實現iterable,現在讓我們來實踐一下讓object變的可迭代。

第一步: 先嚐試著使用for...of遍歷object

下面這樣寫肯定是不行的


const obj = {
    name: 'helios',
    age: 23
}

for (let it of obj) {
    console.log(it)
}
// TypeError: obj is not iterable
複製程式碼

第二步: 讓object實現iterable介面


const obj = {
    name: 'helios',
    age: 23,
    [Symbol.iterator]: function() {
        let age = 23;
        const iterator = {
            next() {
                return {
                    value: age,
                    done: age++ > 24
                }
            }
        }
        return iterator
    }
}

複製程式碼

如果iterableiterable是一個物件的話,上面的程式碼可以簡化為:


function iterOver() {
    let age = 23;
    const iterable = {
        [Symbol.iterator]() {return this},
        next() {
            return {
                value: age,
                done: age++ > 24
            }
        }
    }

    return iterable
}

for (let i of iterOver()) {
    console.log(i)
}
複製程式碼

現在生成器(generator)可以出場了

我們如果每次想把一個不能迭代的物件變為可迭代的物件,在實現Symbol.iterator的時候,每次都要寫返回一個物件,物件裡面有對應的next方法,next方法必須返回valua和done兩個值。

這樣寫的話每次都會很繁,好在ES6提供了generator(生成器)能生成迭代器,我們來看簡化後的程式碼:


const obj = {
    name: 'helios',
    age: 23,
    [Symbol.iterator]: function* () {
        while (this.age <= 24) yield this.age++
    }
}

for (let it of obj) {
    console.log(it)
}

複製程式碼

讓object可迭代真的有意義麼

知乎的這個回答是很有水平的了:為什麼es6裡的object不可迭代?

在stackoverflow中也有很高質量的回答:Why are Objects not Iterable in JavaScript?

在上面的回答中從技術方面說了為什麼Object不能迭代(沒有實現iterable),還說了以什麼樣的方式去遍歷Object是個難題,所以把如何迭代的方式去留給了開發者。

但是還是要思考的一個問題就是:我們真有必要去迭代物件字面量麼?

想一下我們要迭代物件字面量的什麼呢?是keys還是values亦或者是entries,這三種方式在ES6提供的新的資料型別map裡面都有呀,完全是可以代替object的。在這裡不說objectmap的區別,只是說說在ES6以後我們想把兩個事物關聯起來的時候,不一定要非得是用物件字面量了,map支援的更好一下。

對於什麼時候用物件字面量(object)什麼時候使用map我們可以做一下總結:

  • 物件字面量(object)應該是靜態的,也就是說我們應該已經知道了裡面有多少個,和物件的屬性有什麼

  • 使用物件字面量(object)的一般場景有:

    • 不需要去遍歷物件字面量(object)的所有屬性的時候
    • 我們知道了裡面有多少個屬性和物件的屬性是什麼的時候
    • 需要去JSON.stringifyJSON.parse時候
  • 其他的情況用map,其他的情況包括:

    • key不是字串或者symbol的時候
    • 需要去遍歷的時候
    • 要得到長度的時候
    • 遍歷的時候對順序有要求的(物件字面量(object)可能不是按照你寫的順序)

也並不說是map就肯定比物件字面量(object)好,map也有如下的缺點:

  • 不能使用物件解構
  • 不能JSON.stringify/JSON.parse

參考

相關文章