LINDORのAdvent Calendar(本物)の22日目を開けたところ。
配列とかおれおれAdvent Calendar2018 – 22日目

コンストラクターに要素数を与えると、その「要素数」を持った配列が作られます。しかし、「その数の要素」は持っておらず、配列の中身が undefined すら入っていない「空き枠」の状態になります。

const arr = new Array(3);
console.log(arr.length); // => 3
console.log(arr); // => [ <3 empty items> ]

作り方

コンストラクター

さっきの。

const arr = new Array(3);

大きなインデックスで代入

arr.length を超えるインデックスで値を設定すると配列の大きさが拡張される、と一般には表現されるんだけど、実際は length が更新されるだけで該当インデックスに至る途中の枠は埋められず、空き枠になります。

const arr = [11, 22, 33];
delete arr[2];
console.log(arr.length); // => 3
console.log(arr); // => [ 11, 22, <1 empty item> ]

配列初期化子 []

値為しでカンマ , を置くことで同様に空き枠を伴う配列を作成することができます。

const arr = [,, 1, , 2, ,,];
console.log(arr.length); // => 7
console.log(arr);
// => [ <2 empty items>, 1, <1 empty item>, 2, <2 empty items> ]

delete

普通のオブジェクトプロパティと同様、 delete で配列の要素を削除できます。削除後は空き枠になります。

const arr = [11, 22, 33];
delete arr[1];
console.log(arr.length); // => 3
console.log(arr); // => [ 11, <1 empty item>, 33 ]

※ちなみに普通のオブジェクトの場合。

delete obj.foo;

空き枠にせず、削除して詰めたい場合は splice() を使ってください。

仕組み

インデックスはただのプロパティ名

まずJavaScript (ECMAScript) の配列こと Array インスタンスは、例えばC言語のように実際に連続したメモリー領域として存在するわけではなく、JSの他のオブジェクトと同様の仕組みです。俗にいうハッシュマップ。俗にね。

例えばオブジェクトのプロパティは obj.foo としてアクセスできますが、これは obj['foo'] とも書けます。

逆に言えば arr[0] というのも、 arr.0 と同じようなものなのです。(実際は .0 は使えないけど。)

あ、後述するけどインデックスは数値ではなく文字列です。 arr[0] は arr['0'] へ変換されると。

「在る undefined 」と「無い undefined 」

配列じゃない普通のオブジェクトで、こういうのがあったとします。

const obj = {
  foo: undefined,
};
console.log(obj.foo); // => undefined
console.log(obj.bar); // => undefined

どちらも undefined だけど、前者 obj.foo はその中身が undefined として存在する一方で、後者 obj.bar は存在しないため undefined を得ます。

得られる値は同じだけど動きが違いますね。配列でもこれと同様のことが起きているわけです。

配列風オブジェクトでそれっぽく書きます。

const obj = {
  0: 11,
  2: undefined,
  length: 3,
};

console.log(obj.length);// => 3
console.log(obj[1]);// => undefined
console.log(obj[2]);// => undefined

あるかないか確かめる

オブジェクトがあるプロパティを持つかどうかは、 in 演算子を使って調べることができます。

const obj = {
  foo: undefined,
};
console.log('foo' in obj); // => true
console.log('bar' in obj); // => false

配列でも使える。

const arr = [11, , undefined];
console.log('2' in arr); // => true
console.log('1' in arr); // => false

空き枠で気を付けること

ただ undefined が挿入されている場合と何が違うかというと、配列要素を繰り返す系の多くのメソッドでコールバックが呼ばれません。

undefined が入ってる普通の配列

まず普通のやつ。これは期待通りに動作する。

const arr = [undefined, undefined, undefined];
console.log('length :', arr.length);
// length : 3

arr.forEach((item, i) => {
  console.log(`[${i}] : ${item}`);
});
// [0] : undefined
// [1] : undefined
// [2] : undefined

const arr2 = arr.map((_, i) => i);
console.log(arr2);
// [ 0, 1, 2 ]

数だけあって空の配列

undefined も入っていない空き枠の場合。

const arr = new Array(3);
console.log('length :', arr.length);
// length : 3

arr.forEach((item, i) => {
  console.log(`[${i}] : ${item}`); // <- これが実行されない
});

const arr2 = arr.map((_, i) => i);
console.log(arr2);
// [ <3 empty items> ]

呼ばれません。

呼ばれない機能的理由

例えば forEach() の場合、仕様はこのようになっています。(抜粋)

  1. Let k be 0.
  2. Repeat, while k < len
    1. Let Pk be ! ToString(k).
    2. Let kPresent be ? HasProperty(O, Pk).
    3. If kPresent is true, then
      1. Let kValue be ? Get(O, Pk).
      2. Perform ? Call(callbackfn, T, « kValue, k, O »).
    4. Increase k by 1.

(※強調は引用者)

(引用註: 記述の都合上、リスト項目先頭の数字が変わっています。)

よくある for 文みたいな繰り返し方ですね。

問題はコールバック実行前の条件 “If kPresent is true” です。 HasProperty() を使ってプロパティキーがあるか確認しています。空き枠だとこの条件を満たせません。

呼ばれない思想的理由

わからないです。なんでだろね。

オブジェクトで Object.entries() とかしたら存在しないものは当然追加されないので、それに合わせたのかなあ。

空き枠への対処

自前で for を書く

件の除外条件をわざわざ書かなければ(結果として) undefined を得るだけなので、当然 length 分動きます。

コードはいらないよね。

空き枠を埋める

空き枠だけうまく埋める機能ってないので(ないよね?)、 in を使って自力で埋める必要がありそう。

const fillEmptySlots = (arr, value = undefined) => {
  for (let i = 0; i < arr.length; i++) {
    if (!(i in arr)) {
      arr[i] = value;
    }
  }
};

const arr = [11,, undefined];
fillEmptySlots(arr);
console.log(arr); // => [ 11, undefined, undefined ]

全ての枠を埋める

全てが空き枠の場合、あるいは既存のものを無視しても良い場合は、 fill() というメソッドがあります。

const arr = [11,, undefined];
arr.fill();
console.log(arr); // => [ undefined, undefined, undefined ]

対象の配列を破壊的に更新されてますね。

「全てが空き枠の場合」としたが、引数に開始位置、終了位置を受け付けるので、空き枠の範囲が明確な場合でも利用可能。

そもそも空き枠を作らない

はいそうですね。

配列インデックス

インデックスは数値ではなく文字列

ちらと触れたけど、インデックスは文字列です。もっというと「232 – 1未満の正の整数を文字列にしたもの」です。

Properties are identified using key values. A property key value is either an ECMAScript String value or a Symbol value. All String and Symbol values, including the empty string, are valid as property keys. A property name is a property key that is a String value.

An integer index is a String-valued property key that is a canonical numeric String (see 7.1.16) and whose numeric value is either +0 or a positive integer ≤ 253-1. An array index is an integer index whose numeric value i is in the range +0 ≤ i < 232-1.

プロパティはキー値を用いて特定される。プロパティのキー値はECMAScriptのString値かSymbol値のいずれかである。あらゆるString及びSymbol値(空文字列を含む)はプロパティキーとして妥当 (valid) である。 property name と言う場合はString値のキーを指す。

integer index は正規の数値文字列 (a canonical numeric String) であるString値プロパティキーであり(7.1.16を見よ)、その数的な値 (numeric value) は +0 ないし正の整数 ≤ 253-1 である。 array index は、その数的な値 i が +0 ≤ i < 232-1 の範囲内となる integer index である。

i < 232-1

へーちょっと試してみよう。

const arr = [];
arr[2 ** 32 - 2] = 0;
console.log(arr); // => [ <4294967294 empty items>, 0 ]
console.log(arr.length); // => 4294967295

arr[2 ** 32 - 1] = 0;
console.log(arr); // => [ <4294967294 empty items>, 0, '4294967295': 0 ]
console.log(arr.length); // => 4294967295

なるほど。

プロパティアクセス

いわゆる「配列の添え字アクセス」も、実際は全て文字列へ変換され、普通のプロパティアクセスになります。

ちなみに foo.bar も foo['bar'] へ変換されます。

(抜粋)

The dot notation is explained by the following syntactic conversion:

MemberExpression . IdentifierName

is identical in its behaviour to

MemberExpression [ <identifier-name-string> ]

その他

名称

FirefoxのコンソールやMDNの一部で “empty slots” という表現を見かけてそれ採用してたんだけど、一般的なんだろうか。

(Note: this implies an array of 7 empty slots, not slots with actual undefined values)

仕様中にそういう表現は見つからない。

日本語の「ある」と「ない」

「有無」なんて熟語にもなるくらい対として扱われるこいつらだけど、「ある」は動詞なのに「ない」は形容詞なんだよね。なんで動詞にならなかったんだろう? ないものはないので動けないから?

英語も「ある」が “exist” だけど「ない」に該当する語はなさそう。文脈によっては “absent” も使えるが形容詞だ。 “lack” は動詞だけど、「欠ける」だとなんか違うよね。

おまけ: 各ブラウザーでの空き枠の表現

次のコードをコンソールで実行した結果。

Array(3)

Chrome 71

(3) [empty × 3]

Firefox 64

Array(3) [ <3 empty slots> ]

Safari 12

[] (3) = $

最初の [] 以外は文字色が薄くなってました。

Node.js v10

[ <3 empty items> ]

Edge (EdgeHTML 17)

[object Array]: [, , ]

IE 11

!!

[object Array][undefined, undefined, undefined]

forEach() のコールバックはちゃんと呼ばれませんでした。良かった。

おしまい

ちなみにこれで困ったことないです。でもいつか引っかかりそう。

関連

参考

更新履歴

  • 2018-12-22 参考リンクをいくつか追加
  • 2018-12-22 「プロパティアクセス」の説明があいまいだったのを修正