JS 基礎:一次搞懂 for、for...in、for...of 迴圈
技術標籤:javascript/html/cssjsgeneratorobjecttraversaliterator
JS 基礎:一次搞懂 for、for…in、for…of 迴圈
文章目錄
簡介
在 JavaScript 裡面,for 迴圈有三種使用方式:for
、for...in
for...of
(ES6 新增),我們們還可以透過陣列的 forEach
等方法進行另一種遍歷。本篇暫時不加入 Array.prototype 的方法,單純討論 for
關鍵字的不同使用形式。
參考
JavaScript for 迴圈-菜鳥教程 | https://www.runoob.com/js/js-loop-for.html |
for...in-MDN | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in |
js中in關鍵字的使用方法 | https://www.cnblogs.com/memphis-f/p/12073013.html |
正文
樣例
在介紹三種迴圈的意義之前,先給出三種形式的使用樣例:
// 待遍歷變數
const nums = [ 1, 2, 3, 4, 5 ]
const object = { a: 1, b: 2, c: 3 }
// 為 object 新增 Symbol.iterator 供 let...of 使用
object[Symbol.iterator] = function* () {
for (let prop in this) {
yield { [prop]: this[prop] }
}
}
/* 一般 for 迴圈 */
// 按下標訪問陣列
console.log('----- for: array -----' )
for (let i = 0; i < nums.length; i++) {
console.log(nums[i])
}
// 按自有屬性訪問物件鍵值對
console.log('----- for: object -----')
const objectProps = Object.getOwnPropertyNames(object)
for (let i = 0; i < objectProps.length; i++) {
console.log(object[objectProps[i]])
}
/* for...in */
// for...in 遍歷陣列
console.log('----- for...in: array -----')
for (let index in nums) {
console.log(`nums[${index}] = ${nums[index]}`)
}
// for...in 遍歷物件
console.log('----- for...in: object -----')
for (let prop in object) {
console.log(`object.${prop} = ${object[prop]}`)
}
/* for...of */
// for...of 遍歷陣列
console.log('----- for...of: array -----')
for (let num of nums) {
console.log(num)
}
// for...of 遍歷物件(透過 Symbol.iterator 方法)
console.log('----- for...of: object(through object[Symbol.iterator]) -----')
for (let item of object) {
console.log(item)
}
- 輸出
----- for: array -----
1
2
3
4
5
----- for: object -----
1
2
3
----- for...in: array -----
nums[0] = 1
nums[1] = 2
nums[2] = 3
nums[3] = 4
nums[4] = 5
----- for...in: object -----
object.a = 1
object.b = 2
object.c = 3
----- for...of: array -----
1
2
3
4
5
----- for...of: object(through object[Symbol.iterator]) -----
{ a: 1 }
{ b: 2 }
{ c: 3 }
接下來就來看看三個用法究竟是怎麼運作的吧
一般 for 迴圈
一般 for 迴圈就跟 C/C++、Java 等的陣列迴圈方式一模一樣,語法如下:
for ( initialize ; end-condition ; step-action ) {
// statements
}
第一塊 initialize 指定初始化條件;第二塊 end-condition 為終止條件(若為 false 則退出迴圈);第三塊 step-action 為每次迴圈後的固定操作(可能是遞增遞減或"下一步"等自定義操作)。使用範例如下:
// in JavaScript
const nums = [ 1, 2, 3, 4, 5 ]
console.log('----- normal for -----')
for (let i = 0; i < nums.length; i++) {
console.log(`nums[${i}]: ${nums[i]}`)
}
// in C
#define N 5
int nums[N] = { 1, 2, 3, 4, 5 };
for (let i = 0; i < N; i++) {
printf("nums[%d]: %d\n", i, nums[i]);
}
可以發現與 C 語言的 for 迴圈幾乎一樣
Scope 作用域
值得一提的是如果你還在使用 var
關鍵字,或是在一些比較老舊的程式碼裡面,由於 var
關鍵字的作用域和事件迴圈模型(Event Loop)
的聯合作用可能會產生意想不到的結果,以下為經典面試題:
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 0)
}
- 輸出
5
5
5
5
5
由於使用 var
宣告的 i 屬於全域性變數,使用 setTimeout 會將方法放入非同步佇列,直到同步的 for
迴圈操作結束才執行,而執行時 i 已經遞增到 5 了,所以列印結果為 5 個 5
以下有兩種解決方案:
- 使用 IIFE 立即執行函式表示式
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i)
}, 0)
}(i))
}
- 使用 ES6 的 let 關鍵字(塊級作用域)
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 0)
}
所謂的塊級作用域(block scope)
指的是每個塊(block)
有自己的區域性變數,也就是 for 迴圈的五次迴圈存在各自的 i,互不相干。
有關變數作用域、事件迴圈機制、塊級作用域可以參考我的前幾篇文章:
for…in 迴圈
第二種迴圈是 for...in
迴圈,對於陣列和物件有不同的表現:
const nums = [ 1, 2, 3, 4, 5 ]
console.log('----- for i in nums -----')
for (let i in nums) {
console.log(i)
}
const object = { a: 1, b: 2, c: 3 }
console.log('----- for i in object -----')
for (let i in object) {
console.log(i)
}
輸出
----- for i in nums -----
0
1
2
3
4
----- for i in object -----
a
b
c
我們可以看到對於陣列遍歷的是下標,而對於物件則是屬性。其實兩者本質上是一樣的,記得以前提過的類陣列物件
,由於陣列才在記憶體中的儲存模型是以下標為鍵的特別物件,所以本質上 for...in
就是遍歷"物件"的屬性
enumerable: false
然而遍歷屬性還是存在某些條件的,我們總不能每次迴圈連 toString
、length
啥的邊緣屬性都打印出來,JS 原本也不是這麼做的。
這邊就要牽涉到物件的屬性描述符(property description)
了,其中的 enumerable
屬性決定該屬性會不會被 for...in
迴圈訪問到,我們接著使用上面示例做一些修改:
const object = { a: 1, b: 2, c: 3 }
console.log('----- for i in object -----')
for (let i in object) {
console.log(i)
}
Object.defineProperty(object, 'a', {
enumerable: false
})
console.log('----- for i in object -----')
for (let i in object) {
console.log(i)
}
輸出
----- for i in object -----
a
b
c
----- for i in object -----
b
c
我們可以看到當我們將 a 屬性設為 enumerable: false
時,for...in
迴圈便會跳過 a 屬性
模仿 for…in
知道了 for…in 的規則,我們可以透過一些方法來以一般 for 迴圈來模擬 for…in 的作用:
// 用於檢視物件所有屬性的描述符
function showDetail (o, name) {
console.log(`----- show detail: ${name} -----`)
const propertyNames = Object.getOwnPropertyNames(o)
console.log(`Object.getOwnPropertyNames(${name})`)
console.log(propertyNames)
for (let i = 0; i < propertyNames.length; i++) {
console.log(`property: ${propertyNames[i]}`)
console.log(Object.getOwnPropertyDescriptor(o, propertyNames[i]))
}
}
showDetail(nums, 'nums')
showDetail(object, 'object')
console.log('----- for i in nums -----')
for (let i in nums) {
console.log(i)
}
// 模擬 for...in
console.log('----- fake for i in nums -----')
const numsProps = Object.getOwnPropertyNames(nums)
for (let i = 0; i < numsProps.length; i++) {
if (Object.getOwnPropertyDescriptor(nums, numsProps[i]).enumerable) {
console.log(numsProps[i])
}
}
console.log('----- for i in object -----')
for (let i in object) {
console.log(i)
}
// 模擬 for...in
console.log('----- fake for i in object -----')
const objectProps = Object.getOwnPropertyNames(object)
for (let i = 0; i < objectProps.length; i++) {
if (Object.getOwnPropertyDescriptor(object, objectProps[i]).enumerable) {
console.log(objectProps[i])
}
}
輸出
----- show detail: nums -----
Object.getOwnPropertyNames(nums)
[ '0', '1', '2', '3', '4', 'length' ]
property: 0
{ value: 1, writable: true, enumerable: true, configurable: true }
property: 1
{ value: 2, writable: true, enumerable: true, configurable: true }
property: 2
{ value: 3, writable: true, enumerable: true, configurable: true }
property: 3
{ value: 4, writable: true, enumerable: true, configurable: true }
property: 4
{ value: 5, writable: true, enumerable: true, configurable: true }
property: length
{ value: 5, writable: true, enumerable: false, configurable: false }
----- show detail: object -----
Object.getOwnPropertyNames(object)
[ 'a', 'b', 'c' ]
property: a
{ value: 1, writable: true, enumerable: false, configurable: true }
property: b
{ value: 2, writable: true, enumerable: true, configurable: true }
property: c
{ value: 3, writable: true, enumerable: true, configurable: true }
----- for i in nums -----
0
1
2
3
4
----- fake for i in nums -----
0
1
2
3
4
----- for i in object -----
b
c
----- fake for i in object -----
b
c
透過 Object.getOwnPropertyNames
和 Object.getOwnPropertyDescriptor
遍歷所有屬性並過濾描述符 enumerable: false
便實現了 for...in
的作用
有關物件的屬性描述符(property description)
可以看我之前的文章:
for…in 小結
關於 for...in
用法的小結:
- 對於陣列:遍歷下標
- 對於物件:遍歷
enumerable: true
的所有屬性
console.log('---- real meaning for array -----')
for (let index in nums) {
console.log(`nums[${index}]: ${nums[index]}`)
}
console.log('---- real meaning for object -----')
for (let attr in object) {
console.log(`object.${attr}: ${object[attr]}`)
}
輸出
---- real meaning for array -----
nums[0]: 1
nums[1]: 2
nums[2]: 3
nums[3]: 4
nums[4]: 5
---- real meaning for object -----
object.b: 2
object.c: 3
for…of 迴圈
第三種 for...of
迴圈是 ES6 的新特性,先看看用法:
const nums = [ 1, 2, 3, 4, 5 ]
console.log('----- for num of nums -----')
for (let num of nums) {
console.log(num)
}
const string = 'abcde'
console.log('----- for c of string -----')
for (let c of string) {
console.log(c)
}
const object = { a: 1, b: 2, c: 3 }
console.log('----- for item of object -----')
try {
for (let item of object) {
console.log(item)
}
} catch (err) {
console.log(err)
}
----- for num of nums -----
1
2
3
4
5
----- for c of string -----
a
b
c
d
e
----- for item of object -----
TypeError: object is not iterable
看起來好像是遍歷了所有成員,但是對物件卻報錯了,到底怎麼回事?
Symbol.iterator
其實 for...of
迴圈並不是真正的遍歷物件,而是呼叫了該物件的 Symbol.iterator
,不信我演給你看:
const fakeNums = {}
fakeNums[Symbol.iterator] = function* () {
yield 1
yield 2
yield 3
yield 4
return
}
console.log('----- for num of fakeNums -----')
for (let num of fakeNums) {
console.log(num)
}
----- for num of fakeNums -----
1
2
3
4
Symbol.iterator
是一個生成器函式(Generator)
,每次迭代返回一個值(透過 yield
關鍵字),所以我們如果想要使得物件能夠被遍歷,只要給他一個 Symbol.iterator 方法就得了:
const object = { a: 1, b: 2, c: 3 }
object[Symbol.iterator] = function* () {
for (let prop in this) {
yield { [prop]: this[prop] }
}
}
console.log('----- for item of object -----')
for (let item of object) {
console.log(item)
}
----- for item of object -----
{ a: 1 }
{ b: 2 }
{ c: 3 }
有關 Symbol.iterator
可以看:
- ES6特性:Symbol獨一無二的型別 中關於 Symbol.iterator 的描述
for…of 小結
for...of
迴圈呼叫的是物件的 Symbol.iterator
生成器函式,相當於使用內建的迭代器,也可以自己構造一個迭代器
結語
到此結束啦,三種 for 迴圈的使用方式和相關知識整理,供大家參考。