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 與lengthproperty 的 object,可使用forloop 與[],用起來很像 array,但無法使用for ofloop 與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,以 0、1、2 … 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。

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 會出現錯誤訊息。

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() 會出現錯誤訊息。

由於 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 存取。

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 存取。

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。

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 存取。

在 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() 了。

let sum = function() {
return [].slice.call(arguments).reduce((a, x) => a + x, 0)
}
sum(1, 2, 3) // ?
也可以使用 [] 取代 Array.prototype,因為 empty array 會繼承 Array.prototype 下所有 method。

let sum = function() {
return Array.from(arguments).reduce((a, x) => a + x, 0)
}
sum(1, 2, 3) // ?
在 ES6 可以使用 Array.from() 直接將 array-like object 轉成 array,語意更清楚。

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。

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() 一般。

Conclusion
- Documents、DOM node list、string 在 ES5 為 array-like object,只能使用
forloop,但在 ES6 全部升級成 iterable,因此也可使用for ofloop,但仍然不能使用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