1. 程式人生 > 其它 >JS 基礎:一次搞懂 for、for...in、for...of 迴圈

JS 基礎:一次搞懂 for、for...in、for...of 迴圈

技術標籤:javascript/html/cssjsgeneratorobjecttraversaliterator

JS 基礎:一次搞懂 for、for…in、for…of 迴圈

文章目錄

簡介

在 JavaScript 裡面,for 迴圈有三種使用方式:forfor...in

for...of(ES6 新增),我們們還可以透過陣列的 forEach 等方法進行另一種遍歷。本篇暫時不加入 Array.prototype 的方法,單純討論 for 關鍵字的不同使用形式。

參考

JavaScript for 迴圈-菜鳥教程https://www.runoob.com/js/js-loop-for.html
for...in-MDNhttps://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

以下有兩種解決方案:

  1. 使用 IIFE 立即執行函式表示式
for (var i = 0; i < 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i)
    }, 0)
  }(i))
}
  1. 使用 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

然而遍歷屬性還是存在某些條件的,我們總不能每次迴圈連 toStringlength 啥的邊緣屬性都打印出來,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.getOwnPropertyNamesObject.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 可以看:

for…of 小結

for...of 迴圈呼叫的是物件的 Symbol.iterator 生成器函式,相當於使用內建的迭代器,也可以自己構造一個迭代器

結語

到此結束啦,三種 for 迴圈的使用方式和相關知識整理,供大家參考。