Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

意外すぎる"未定義"の配列とその挙動

(この記事はQiitaで僕が書いたものを移行した記事です。記事中のコメントはQiitaの該当記事を参照ください)

JavaScript で同じ要素が繰り返す配列を作る

この記事を受けてコメントして自分の浅学を晒してしまいました。 さすがにまずいと思い、自分で検証していたら想像以上に根が深い問題でこれは記事にしなくてはと責任感に駆られました。

本題

以下の説明では配列arr1,arr2,arr3,arr4を使います。定義は以下のようにします。また、lodashのmapの挙動が思ってたよりも恐ろしかったのでその説明もします。

const _ = require('lodash');
const arr1 = new Array(10);
const arr2 = Array.apply(null, Array(10));
const arr3 = Array.apply(null, Array(10));
const arr4 = Array.apply(null, Array(10));
arr3.length = 20;
arr4[19] = 20;
const f = () => 'AB';

1. arr1とarr2の違い

自分も全く知らなかったですが、arr1とarr2、実は本質的に違う配列です。

arr1は"未定義"の要素が10個入った配列、arr2はundefinedが10個入った配列です。 つまり、 arr1はlengthプロパティは持ちますが、インデックス0から9は空です 。それに対し、 arr2ではインデックス0から9はundefinedが代入されています (追記:コメントで補足していただきました。そちらのほうがわかりやすいです)。この違いが以下の説明でかなり重要です。

これを踏まえてarr3とarr4を説明します。arr3,arr4は、インデックス0から9はarr2と同様undefinedが代入されています。しかし、arr3はインデックス10以降はすべて"未定義"です。arr4はインデックス10から18まで"未定義"で19は20です。この事実をよく覚えてください。

2. ネイティブのArray.prototype.map()の仕様

ネイティブのArray.prototype.map()の仕様が直感に反しまくりで正直驚きました。その原因が、その1.で説明した"未定義"とundefinedという2つの状態があることです。Array.prototype.map()では、undefinedの要素、数字の代入されている要素、とにかくなにかが代入されている要素に関数を適用させていきます。つまり、"未定義"以外に関数を適用します。 コードを見ると一発です。

const f = () => 'AB';
arr1.map(f);// [ "未定義" x10 ]
arr2.map(f);// [ 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB' ]
arr3.map(f);// [ 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', "未定義" x10  ]
arr4.map(f);// [ 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', 'AB', "未定義" x9 , 'AB' ]

"未定義"のところは'AB'になっていません。勝手にlengthに沿ってループで処理するのかと思ってたので驚きました。

3. lodash.map()の仕様

上で Array.prototype.map() を見ました。これで安心してはいけません。 _.map()Array.prototype.map() とは完全に異なる動きをとります。実際のコードを見てみましょう。

_.map(arr1,f);// ["AB","AB","AB","AB","AB","AB","AB","AB","AB","AB"]
_.map(arr2,f);// ["AB","AB","AB","AB","AB","AB","AB","AB","AB","AB"]
_.map(arr3,f);// ["AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB"]
_.map(arr4,f);// ["AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB","AB"]

_.map() では"未定義"だろうとなんであろうと、lengthに沿ってループするだけです。だから、arr1とarr2、arr3とarr4はそれぞれ同じ配列になります。 (どちらかというと_.map()の動作の方が自然に感じるのは自分だけでしょうか?)

まとめ

"未定義"とundefinedに気をつけないとそのうち痛い目見ますよ。という話でした。