點燈坊

失くすものさえない今が強くなるチャンスよ

ECMAScript 之 Array-like Object

Sam Xiao's Avatar 2020-01-18

ECMAScript 有個很特殊的 Array-like Object,本質是 Object,但用起來很像 Array,但卻又不是 Array,因此有些特性不能使用,必須靠一些特殊方式。

Version

macOS Mojave 10.15.2
VS Code 1.41.1
Quokka 1.0.274
ECMAScript 2015

Definition

Array-like Object
有 index 與 length property 的 object,可使用 for loop 與 [],用起來很像 array,但無法使用 for of loop 與 Array.prototype 下的 method

For Loop

let data = {
  0: 'Sam',
  1: 'Kevin',
  2: 'John',
  length: 3
}

let objToArr = obj => {
  let result = []

  for(let i = 0; i < obj.length; i++) {
    result.push(obj[i])
  }

  return result
}

objToArr(data) // ?

第 1 行

let data = {
  0: 'Sam',
  1: 'Kevin',
  2: 'John',
  length: 3
}

obj 為典型 array-like object,以 012 … index 為 property,且還有 length property。

11 行

for(let i = 0; i < obj.length; i++) {
  result.push(obj[i])
}

可使用 [] 存取 obj,也能透過 length 使用 for loop,obj 用起來很像 array,所以稱為 array-like object。

array000

For Of Loop

let data = {
  0: 'Sam',
  1: 'Kevin',
  2: 'John',
  length: 3
}

let objToArr = obj => {
  let result = [];

  for(let x of obj) {
    result.push(x);
  }

  return result
}

objToArr(obj) // ?

普通 array 都可以使用 for of loop 取代 for loop,但若對 array-like object 使用 for of loop 會出現錯誤訊息。

array001

map()

let obj = {
  0: 'Sam',
  1: 'Kevin',
  2: 'John',
  length: 3
}

// objToArr :: {a} -> [b]
let objToArr = obj => obj.map(x => `Mr. ${x}`);
  
objToArr(obj); // ?

普通 array 都可以使用 Array.prototype.map(),但若對 array-like object 使用 map() 會出現錯誤訊息。

array002

由於 array-like object 無法使用 for of loop 與 map(),可以發現 array-like object 與真正 array 還是有些差別。

Application

實務上有 3 處是 array-like object:

  • Arguments
  • DOM node list
  • String

Arguments

function sum() {
  let result = 0
  
  for(let i = 0; i < arguments.length; i++) {
    result += arguments[i]
  }
  
  return result
}

sum(1, 2, 3) // ?

當使用 function declaration 時,可使用 arguments 存取所有 argument,其中 arguments 是 array-like object,可使用 for loop 存取。

array004

let sum = function() {
  let result = 0
  
  for(let i = 0; i < arguments.length; i++) {
    result += arguments[i]
  }
  
  return result
}

sum(1, 2, 3) // ?

當使用 function expression 時,也可使用 arguments 存取所有 argument,其中 arguments 是 array-like object,可使用 for loop 存取。

array003

let sum = () => {
  let result = 0;
  
  for(let i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }
  
  return result;
}

sum(1, 2, 3); // ?

當使用 arrow function 時,則無法使用 arguments

array005

DOM Node List

let nodes = document.getElementsByTagName('h3')

for(let i = 0; i < nodes.length; i++) {
  nodes[i]; // ?
}

document.getElementsBy*() 所回傳的都是 array-like object,可使用 for loop 存取。

String

let name = 'Sam'

for(let i = 0; i < name.length; i++) {
  name[i] // ?
}

String 也是 array-like object,可使用 for loop 存取。

array006

在 ES5 時代,arguments、DOM node list 與 string 都只是 array-like object,只能使用 for loop 存取,但 ES6 之後,這三者全都升級成 iterable,因此可使用 for of loop,但依然無法使用 Array.prototyoe 下的 method,因為 iterable object 仍然不是 array。

為了要讓 array-like object 能使用 Array.prototype 下的 method,有兩種方式:

  • 將 array-like object 轉成真正 array
  • 借用 Array.prototype 下的 method

Convert to Array

let sum = function() {
  return Array.prototype.slice.call(arguments)
  .reduce((a, x) => a + x, 0)
}

sum(1, 2, 3) // ?

在 ES5,我們可借用 Array.prototype.slice() 將 array-like object 轉成 array。

call() 來自於 Function.prototype.call(),其第一個 argument: thisArg 為取代 slice() 中的 this,傳入 arguments 取代 this 後,相當於 arguments 有了 slice()

既然 slice() 傳回真正 array,就可以安全使用 reduce() 了。

array007

let sum = function() {
  return [].slice.call(arguments).reduce((a, x) => a + x, 0)
}

sum(1, 2, 3) // ?

也可以使用 [] 取代 Array.prototype,因為 empty array 會繼承 Array.prototype 下所有 method。

array008

let sum = function() {
  return Array.from(arguments).reduce((a, x) => a + x, 0)
}

sum(1, 2, 3) // ?

在 ES6 可以使用 Array.from() 直接將 array-like object 轉成 array,語意更清楚。

array009

let sum = function() {
  return Array(...arguments).reduce((a, x) => a + x, 0)
}

sum(1, 2, 3) // ?

ES6 的 Array.from() 有個等效寫法,就是透過 spread operator 展開 array-like object,然後透過 Array() 轉成 array。

array010

Generic Method

let sum = function() {
  return [].reduce.call(arguments, (a, x) => a + x, 0)
}

sum(1, 2, 3) // ?

既然能借用 slice(),為什麼不借用 reduce() 呢 ?

直接使用 [].reduce.call() 借用 reduce(),好似 arguments.reduce() 一般。

array011

Conclusion

  • Documents、DOM node list、string 在 ES5 為 array-like object,只能使用 for loop,但在 ES6 全部升級成 iterable,因此也可使用 for of loop,但仍然不能使用 Array.prototype 下的 method
  • Array-like object 最大的缺點是不能使用 Array.prototype 下的 method,ES5 可藉由 slice() 轉成 array,或由 ES6 的 Array.from() 轉成 array,或乾脆使用 call() 直接借用 generic method

Reference

Dr. Axel Rauschdayer, Array-Like Objects and Generic Methods