本文已經預設你已經知道generator
是什麼以及for...of
和資料型別map
怎麼用了。
前ES6的時代怎麼遍歷
先來一道思考題:
通過下面的變數
- 尋找xiaohong(假設名稱唯一)是否喜歡basketball
- 所有同學的名字
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(迭代器)方法,這個方法返回一個規定的物件(這個後面會說)。也就是說iterable
是iterator
的工廠,iterable
能夠建立iterator
。iterator
是用於遍歷資料結構中元素的指標。
兩者之間的關係
Axel Rauschmaye大神的圖簡直不能再清晰了。
資料消費者: 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之間的關係
然後談談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就可以了。
用通俗的話說就是,你如果要遍歷一個物件的話,有一下幾個步驟:
- 物件如果沒實現
Symbol.iterator
那就去實現 - 物件的
Symbol.iterator
函式要返回一個iterator
iterator
返回一個物件,物件中至少要包含一個next方法來獲取- next方法返回兩個值
value
和done
現在說說怎麼使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
}
}
複製程式碼
如果iterable
和iterable
是一個物件的話,上面的程式碼可以簡化為:
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的。在這裡不說object
和map
的區別,只是說說在ES6以後我們想把兩個事物關聯起來的時候,不一定要非得是用物件字面量
了,map
支援的更好一下。
對於什麼時候用物件字面量(object)什麼時候使用map我們可以做一下總結:
-
物件字面量(object)應該是靜態的,也就是說我們應該已經知道了裡面有多少個,和物件的屬性有什麼
-
使用物件字面量(object)的一般場景有:
- 不需要去遍歷物件字面量(object)的所有屬性的時候
- 我們知道了裡面有多少個屬性和物件的屬性是什麼的時候
- 需要去
JSON.stringify
和JSON.parse
時候
-
其他的情況用map,其他的情況包括:
- key不是字串或者symbol的時候
- 需要去遍歷的時候
- 要得到長度的時候
- 遍歷的時候對順序有要求的(物件字面量(object)可能不是按照你寫的順序)
也並不說是map
就肯定比物件字面量(object)好,map
也有如下的缺點:
- 不能使用物件解構
- 不能
JSON.stringify
/JSON.parse
參考
- 25.1.1.1 The Iterable Interface
- Maps vs Objects in ES6, When to use?
- Iterables and iterators in ECMAScript 6
- 【翻譯】Iterables and iterators in ECMAScript 6
- Maps vs Objects in ES6, When to use?
- 迭代協議
- ES6 你可能不知道的事 - 進階篇
- 深入淺出 ES6(二):迭代器和 for-of 迴圈
- 深入淺出 ES6(三):生成器 Generators
- Javascript ES6 Iterators建議指南(含例項)