JavaScriptの中身があるけど中身のない配列について

[空じゃないけど空の配列の話。（配列とかおれおれ Advent Calendar2018 – 22 日目） | Ginpen.com](https://ginpen.com/2018/12/22/empty-slots/) を読んで面白かったので、JavaScript の空じゃないけど空な配列について自分でも調べてみた。

## はじめに

JavaScript の配列で以下のように中身があるようで中身のない配列がある。

```js
const arr = [10, 20, 30];
arr[4] = 50;

console.log(arr);
// [ 10, 20, 30, <1 empty item>, 50 ]
```

MDN では`空の配列スロット`と呼んでいそうだったので、この記事内では`空の配列スロット`と呼ぶ。

> in 演算子は、空の配列スロットに対して false を返します。直接アクセスしても undefined が返されます。
>
> MDN より引用
> https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/in

## どんなときに空の配列スロットが生まれるのか？

Array コンストラクターや配列のサイズを超えたインデックスでの代入、配列初期化子、`delete`演算子、`length`の書き換えで生まれる。

```js
/* Arrayコンストラクター */
const arr = new Array(3);

console.log(arr);
// [ <3 empty items> ]
```

```js
/* 配列のサイズを超えたインデックスでの代入 */
const arr = [10, 20, 30];
arr[4] = 50;

console.log(arr);
// [ 10, 20, 30, <1 empty item>, 50 ]
```

```js
/* 配列初期化子 */
const arr = [10, 20, 30, , 50];

console.log(arr);
// [ 10, 20, 30, <1 empty item>, 50 ]
```

```js
/* delete演算子 */
const arr = [10, 20, 30];
delete arr[2];

console.log(arr);
// [ 10, 20, <1 empty item> ]
```

```js
/* lengthプロパティの書き換え */
const arr = [10, 20, 30];
arr.length = 5;

console.log(arr);
// [ 10, 20, 30, <2 empty items> ]
```

## 要素にアクセスするとどうなるのか？

`undefined`が返ってくる。

```js
const arr = [10, 20, 30];
arr[4] = 50;

console.log(arr);
// [ 10, 20, 30, <1 empty item>, 50 ]

console.log(arr[3]);
// undefined
```

配列も添字をプロパティ名として持つ Object として考えるとわかりやすい。

```js
const obj = {};
obj[0] = 10;
obj[1] = 20;
obj[2] = 30;
obj[4] = 50;

console.log(obj[3]);
// undefined
```

## 意図して`undefined`を入れている場合、どうやって区別するのか？

`Object.prototype.hasOwnProperty`、`in`演算子、`Object.keys`などを使うことで空の配列スロットかどうかを判断できる。
また、`Array.prototype.keys`では空の配列スロットの区別はつけれない。

`Object.key`では Own Property を見てるのに対し、`Array.prototype.keys`では見てないのが起因してそう。

- https://tc39.es/ecma262/#sec-object.keys
- https://tc39.es/ecma262/#sec-array.prototype.keys

```js
/* Object.prototype.hasOwnProperty */

const arr = [10, 20, undefined];
arr[4] = 50;

console.log(arr);
// [ 10, 20, undefined, <1 empty item>, 50 ]

for (let i = 0; i < arr.length; i++) {
  console.log(i, arr.hasOwnProperty(i));
}
// 0 true
// 1 true
// 2 true
// 3 false
// 4 true
```

```js
/* in演算子 */

const arr = [10, 20, undefined];
arr[4] = 50;

console.log(arr);
// [ 10, 20, undefined, <1 empty item>, 50 ]

for (let i = 0; i < arr.length; i++) {
  console.log(i, i in arr);
}
// 0 true
// 1 true
// 2 true
// 3 false
// 4 true
```

```js
/* Object.keys */

const arr = [10, 20, undefined];
arr[4] = 50;

console.log(arr);
// [ 10, 20, undefined, <1 empty item>, 50 ]

console.log(Object.keys(arr));
// [ '0', '1', '2', '4' ]
```

```js
/* Array.prototype.keys */

const arr = [10, 20, undefined];
arr[4] = 50;

console.log(arr);
// [ 10, 20, undefined, <1 empty item>, 50 ]

console.log(Array.from(arr.keys()));
// [ 0, 1, 2, 3, 4 ]
```

## forEach などのループ処理はどうなるか？

`Array.prototype.forEach`は Own Property を見てるため、空の配列スロットでコールバックが呼ばれない。

- https://tc39.es/ecma262/#sec-array.prototype.foreach

```js
/* Array.prototype.forEach */

const arr = [10, 20, 30];
arr[4] = 50;

console.log(arr);
// [ 10, 20, 30, <1 empty item>, 50 ]

arr.forEach((val, idx) => {
  console.log(val, idx);
});
// 10 0
// 20 1
// 30 2
// 50 4
```

`Array.prototype.map`も同様にコールバックは呼ばれないが、返り値の配列の length は同じままで、empty item が含まれている。

- https://tc39.es/ecma262/#sec-array.prototype.map

```js
/* Array.prototype.map */

const arr = [10, 20, 30];
arr[4] = 50;

console.log(arr);
// [ 10, 20, 30, <1 empty item>, 50 ]

const mappedArr = arr.map((val, idx) => {
  console.log(val, idx);
  return val * idx;
});
// 10 0
// 20 1
// 30 2
// 50 4

console.log(mappedArr);
// [ 0, 20, 60, <1 empty item>, 200 ]
```

`Array.prototype.filter`では、返り値の配列はイメージしていたものと一致する。
（コールバック自体が呼ばれないため、`true`判定にならない？）

- https://tc39.es/ecma262/#sec-array.prototype.filter

```js
/* Array.prototype.filter */

const arr = [10, 20, 30];
arr[4] = 50;

console.log(arr);
// [ 10, 20, 30, <1 empty item>, 50 ]

const filteredArr = arr.filter((val, idx) => {
  console.log(val, idx);
  return true;
});
// 10 0
// 20 1
// 30 2
// 50 4

console.log(filteredArr);
// [ 10, 20, 30, 50 ]
```

`Array.prototype.find`に関しては、Own Property を見てないので、空の配列スロットもコールバックが呼ばれる。

- https://tc39.es/ecma262/#sec-array.prototype.find

```js
/* Array.prototype.find */

const arr = [10, 20, 30];
arr[4] = 50;

console.log(arr);
// [ 10, 20, 30, <1 empty item>, 50 ]

const found = arr.find((val, idx) => {
  console.log(val, idx);
});
// 10 0
// 20 1
// 30 2
// undefined 3
// 50 4
```

`for...of`は空の配列スロットも含まれる。（わからない）

```js
/* for...of */

const arr = [10, 20, 30];
arr[4] = 50;

console.log(arr);
// [ 10, 20, 30, <1 empty item>, 50 ]

for (let val of arr) {
  console.log(val);
}
// 10
// 20
// 30
// undefined
// 50
```

## さいごに

普段、当たり前に使っている配列もよくよく調べてみると面白いですね。
実際に役に立つタイミングは少ないかもしれませんが、面白いと思っていただければ幸いです。

## Refs

- [空じゃないけど空の配列の話。（配列とかおれおれ Advent Calendar2018 – 22 日目） | Ginpen.com](https://ginpen.com/2018/12/22/empty-slots/)
- [JavaScript の疎な配列 - 配列 (Array) - JavaScript の基本 - JavaScript 入門](https://javascript.keicode.com/lang/array-sparse.php)
- MDN
  - [Array - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array)
  - [length と数値プロパティとの関係 - Array - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array#length_%E3%81%A8%E6%95%B0%E5%80%A4%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%A8%E3%81%AE%E9%96%A2%E4%BF%82)
  - [配列の要素の削除 - delete 演算子 - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/delete#%E9%85%8D%E5%88%97%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%AE%E5%89%8A%E9%99%A4)
  - [Object.prototype.hasOwnProperty() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty)
  - [in 演算子 - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/in)
  - [Object.keys() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/keys)
  - [Array.prototype.keys() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/keys)
  - [Array.prototype.forEach() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)
  - [for...of - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for...of)
- ECMAScript® 2023 Language Specification
  - [Object.keys - ECMAScript® 2023 Language Specification](https://tc39.es/ecma262/#sec-object.keys)
  - [Array.prototype.keys - ECMAScript® 2023 Language Specification](https://tc39.es/ecma262/#sec-array.prototype.keys)
  - [Array.prototype.forEach - ECMAScript® 2023 Language Specification](https://tc39.es/ecma262/#sec-array.prototype.foreach)
  - [Array.prototype.map - ECMAScript® 2023 Language Specification](https://tc39.es/ecma262/#sec-array.prototype.map)
  - [Array.prototype.filter - ECMAScript® 2023 Language Specification](https://tc39.es/ecma262/#sec-array.prototype.filter)
  - [Array.prototype.find ECMAScript® 2023 Language Specification](https://tc39.es/ecma262/#sec-array.prototype.find)