オブジェクトを使用すると、キー付きの値のコレクションを格納することができます。
しかし、実際には多くの頻度で 順序付されたコレクション が必要であることがわかります。それは、1つ目、2つ目、3つ目… と言った要素であり、例えばユーザ、商品、HTML要素など何かのリストを格納します。
ここでオブジェクトを使うのは便利ではありません。なぜなら、オブジェクトには要素の順序を管理するためのメソッドは提供されていないからです。既存のリストの “間に” 新しいプロパティを挿入することはできません。オブジェクトはこのように使うものではありません。
順序付けされたコレクションを格納するために、Array と呼ばれる特別なデータ構造があります。
宣言
空の配列を作る2つの構文があります:
ほぼすべてのケースで2つ目の構文が使われます。角括弧の中に初期値となる要素を指定することができます:
配列要素はゼロから始まる番号が付けられます。
角括弧にその番号を指定することで、該当する要素を取得することができます:
要素の置き換えも可能です:
…もしくは、配列に新しいものを追加することもできます:
配列内の要素の総数は、その length で取得できます:
alert を使うことで、すべての配列を表示することも可能です。
配列はどんな型の要素も格納することができます。
例:
配列は、オブジェクトのようにカンマで終わる場合があります:
すべての行が同じようになるので、“末尾のカンマ” は項目の挿入や削除が容易になります。
pop/push, shift/unshift メソッド
キュー(queue) は配列で最も一般的に使われるものの1つです。コンピュータ・サイエンスでは、これは2つの操作をサポートする要素の順序付きコレクションを意味します。:
- push は要素を末尾に追加します。
- shift は最初から要素を取得し、2番目の要素が1番目になるようにキューを進めます。
配列は両方の操作をサポートします。
実践では、非常に頻繁にこれを見ます。例えば画面に表示が必要なメッセージのキューです。
配列の別のユースケースもあります – スタック(stack) と呼ばれるデータ構造です。
これは2つの操作をサポートします。
- push は要素を末尾に追加します.
- pop は末尾から要素を取り出します。
なので、新しい要素は常に “末尾” から追加または取得されます。
スタックは、通常カードのパックとして例えられます。新しいカードが上に追加されるか、カードが上から取り出されます:
スタックの場合、最新のプッシュされたアイテムが最初に受け取られます。これはLIFO(Last-In-First-Out)の原則とも呼ばれます。 キューの場合、FIFO(First-In-First-Out)があります。
JavaScriptの配列は、キューとスタックどちらとしても動作します。これらの要素を使用すると、要素を先頭または最後に追加/削除することができます。
コンピュータサイエンスでは、それを許可するデータ構造を両端キュー/デック(deque)と呼びます。
配列の末尾で動作するメソッド:
pop配列の最後の要素を抽出して返します。:
配列の末尾に要素を追加します。:
fruits.push(...) 呼び出しは fruits[fruits.length] = ... と同じです。
配列の先頭で動作するメソッド:
shift配列の先頭の要素を抽出して返します。:
配列の先頭に要素を追加します。:
メソッド push と unshift は一度に複数の要素を操作することができます:
内部詳細
配列は特別な種類のオブジェクトです。プロパティ arr[0] にアクセスするために使う角括弧は、実際にはオブジェクト構文から来ています。数字がキーとして使用されます。
配列はデータの順序付きコレクションと、length プロパティを処理する特別なメソッドを提供するようオブジェクトを拡張します。しかし、コアではまだオブジェクトです。
JavaScriptには7つの基本タイプしかないことに注意してください。 配列はオブジェクトであるため、オブジェクトのように動作します。
例えば、これは参照としてコピーされます:
…しかし配列を本当に特別にするのは、その内部表現です。エンジンは、このチャプターの図に示されているように連続したメモリ領域に要素を格納しようとします。そして配列を非常に高速にするために、他の最適化も行われます。
しかし、“順序付けられたコレクション” として配列を処理するのをやめ、普通のオブジェクトのように扱い始めると、それらはすべて壊れます。
例えば、技術的にはこうすることもできます:
配列のベースはオブジェクトなので、これは可能です。任意のプロパティを追加することができます。
しかし、エンジンは我々が配列を通常のオブジェクトとして処理していることを知るでしょう。配列固有の最適化は、このような場合には適しておらず無効になります。その利点は消えます。
配列の誤った使い方:
- arr.test = 5 のように非数値プロパティを追加する。
- 穴を作る: arr[0] を追加した後、arr[1000] を追加する(その間は無し)。
- 逆順で配列を埋める: arr[1000], arr[999] など。
順序付きデータ を処理するための特別な構造として配列があると考えてください。配列はそのための特別なメソッドを提供します。配列は連続した順序付きデータを処理するため、JavaScriptエンジン内部で注意深くチューニングされています。このために配列を使ってください。そして、任意のキーが必要なときは、通常のオブジェクト {} が必要な可能性が高いです。
パフォーマンス
メソッド push/pop は処理が速く、shift/unshift は遅いです。
なぜ、配列の最初よりも最後を処理する方が速いのでしょうか?実行中起こっている事を見てみましょう:
数値 0 の要素を取得して削除するだけでは不十分です。他の要素も同様に番号をつけ直す必要があります。
shift 操作は3つのことをしなければなりません:
- インデックス 0 の要素を削除します。
- 全ての要素を左に移動させます。インデックス 1 から 0、2 から 1 と言うように番号をつけ直します。
- length プロパティを更新します。
配列内の要素が増えれば増えるほど、移動に必要な時間とメモリ内の操作が増えます。
unshift でも似たようなことが起きます: 配列の先頭に要素を追加しますが、最初に存在する要素を右に移動させる必要があり、それらのインデックスを増やします。
そして、push/pop はどうでしょう?それらは何も移動させる必要がありません。末尾から要素を抽出するため、pop メソッドはインデックスを消去し、length を短くするだけです。
pop 操作のアクション:
他の要素のインデックスは変わらないので、pop メソッドは何も移動させる必要はありません。そのため非常に高速です。
push メソッドも同じです。
ループ
配列アイテムを循環させる最も古い方法の1つは、インデックス上の for ループです:
しかし、配列のための for..of という別のループの形式があります:
for..of は現在の要素の番号へアクセスすることはできず、単に値のみです。しかし、殆どのケースではそれで十分です。また、より短い構文です。
技術的には、配列はオブジェクトなので for..in を利用することもできます:
しかし、実際にこれは良くないアイデアです。そこには潜在的な問題があります:
-
ループ for..in は数値のものだけでなく、 全てのプロパティ を繰り返し処理します。
ブラウザや他の環境では 配列のように見える いわゆる “配列のような” オブジェクトがあります。つまり、それらは length とインデックスプロパティを持っています。しかし、それらは通常は必要のない他の非数値プロパティやメソッドも持っています。for..in ループはそれらもリストします。なので、もし配列のようなオブジェクトを処理する必要があるとき、それらの “余分な” プロパティが問題になる場合があります。
-
for..in ループは配列ではなく、汎用オブジェクトに対して最適化されているため、10から100倍遅くなります。もちろんそれでもとても速いです。高速化はボトルネックの場合にのみ問題なり、それ以外ではさほど重要でないこともあります。しかしそれでも私たちは違いに気をつけるべきです。
一般的に、配列に対しては for..in は使うべきではありません。
“length” について
配列を変更したとき、length プロパティは自動的に更新されます。正確には、それは配列の実際の値の数ではなく、最大の数値インデックスに1を加えたものです。
例えば、大きなインデックスの1つの要素は大きなlengthを返します:
通常、そのように配列を使わないことに注意してください。
length プロパティの別の興味深い点は、書き込み可能と言う点です。
手動で増やした場合、面白いことは起きません。しかし、それを減らしたとき、配列は切り捨てられます。この処理は不可逆です。これはその例です:
なので、配列をクリアする最もシンプルな方法は arr.length = 0; です。
new Array()
配列を作るもう1つの構文があります:
角括弧 [] がより短く書けるので、ほとんど使われません。また、トリッキーな特徴があります。
もし数値の1つの引数で new Array が呼ばれたとき、アイテムはありませんが、与えられた長さを持った 配列が作られます。
それがどのように墓穴を掘るか見てみましょう:
このような驚きを避けるため、何をしているのか本当に分かっていない限り、通常は角括弧を使います。
多次元配列
配列は配列も持つことができます。我々は行列を格納するために、それを多次元配列として使うことができます。:
toString
配列は、要素のカンマ区切りのリストを返す独自の toString メソッドの実装を持ってます。
例:
もしくは、これを試してみましょう:
配列は Symbol.toPrimitive を持っておらず、valueOf もなく、toString 変換のみを実装しているため、ここでは [] は空文字列になり、[1] は "1" に、[1,2] は "1,2" になります。
二項演算子プラス "+" が文字列に何かを加えたとき、同様に文字列に変換します。なので、その次のステップはこのように見えます:
配列を == で比較しないでください
JavaScript の配列は他のプログラミング言語とは異なり、== 演算子で比較すべきではありません。
この演算子は配列に対して特別な扱いせず、他のオブジェクトと同様に動作します。
ルールを思い出してみましょう:
- 2つのオブジェクトは、同じオブジェクトを参照しているときにだけ、等価 == です。
- == の引数の一方がオブジェクトで、もう一方がプリミティブの場合、オブジェクトはチャプター オブジェクトからプリミティブへの変換 で説明したように、プリミティブに変換されます。
- …互いに == で等価である null と undefined を除いては、他には何もありません。
厳密比較 === は、型変換をしないためよりシンプルです。
なので、== で配列を比較する場合、全く同じ配列を参照している2つの変数を比較しない限り、決して等価にはなりません。
例:
これらの配列は技術的には異なるオブジェクトです。したがって、等しくはなりません。== 演算子は要素毎の比較は行いません。
プリミティブとの比較では、以下のように、一見すると奇妙な結果がでることがあります:
ここでは、両方のケースで配列オブジェクトとプリミティブを比較しています。なので、配列 [] は比較のためにプリミティブに変換され、空文字 '' になります。
次に、チャプター 型変換 で説明されているように、比較のプロセスがプリミティブで続行されます。
では、どうやって配列を比較しましょう?
簡単です: == 演算子を使いません。代わりにループや次のチャプターで説明するイテレーションメソッドを使用して比較します。
サマリ
配列はオブジェクトの特別な種類であり、順序付けされたデータ項目を格納するのに適しています。
-
宣言:
// 角括弧 (通常) let arr = [item1, item2...]; // new Array (例外的、ほとんど使われません) let arr = new Array(item1, item2...);new Array(number) への呼び出しは与えられた長さの配列を作りますが、要素を持ちません。
-
length プロパティは配列の長さです。正確にはその最後の数値インデックスに1を加えたものです。それは配列のメソッドにより、自動的に調整されます。
-
もし手動で length を短くした場合、配列は切り捨てられます。
以下の操作で配列を両端キュー(deque)として使用できます。:
- push(...items) は items を末尾に追加します。
- pop() は末尾の要素を削除し、それを返します。
- shift() は先頭の要素を削除し、それを返します。
- unshift(...items) はアイテムを先頭に追加します。
配列の要素をループするために:
- for (let i=0; i<arr.length; i++) – 最も速く動作し、古いブラウザ互換です。
- for (let item of arr) – アイテムだけのための、現代の構文です。
- for (let i in arr) – 決して使いません。
配列を比較するには、== 演算子(>, < なども同様)は使用しません。これらは配列に対して特別な処理はしません。単にオブジェクトとして扱い、それは通常期待することではありません。
代わりに、配列を要素毎に比較するために for..of ループが使用できます。
私たちは、チャプター 配列のメソッド で配列に戻り、追加、削除、要素の抽出や配列のソートと言ったより多くのメソッドを学びます。
コメント