【JavaScript】 配列と連想配列の要素順序とMapオブジェクト
更新日:2020/06/19
JavaScriptでプログラムを作成していると、配列や連想配列を使うことって多いですね。
僕の場合、入れた順番で取り出したいことがあります。
しかし思ったようにいかず、悩むこともしばしば…
そこで、配列や連想配列内の要素の並び方を調べてみました。
また、Mapオブジェクトを使った連想配列の代替案をお伝えします。
配列の挿入順序は保証される
配列は要素を挿入した順番を記憶しているので、その順番で取り出すことができます。
挿入した順番に取り出し可能
次のコードは、いろいろな方法で配列に要素を挿入して、内容を出力しています。
JavaScript
const array=["a","c",1,"v","1"];
array.push("d");
array.push("2");
array.push(2);
array[10]="z";
array[9]="x";
console.log(array);
実行結果
Array(11) [ "a", "c", 1, "v", "1", "d", "2", 2 , undefined, "x", "z"]
要素の形式に関係なく、順番に格納されているのがわかります。
ただしインデックス付きでセットした場合は、指定した位置にセットされています。
インデックス10の後に9をセットしていますが、それぞれの位置にセットされていますね。
ちなみにインデックス8は、何もセットされていないのでundefined(※)と表示されています。
※ブラウザによって表示される内容が異なります。
最近再確認したところ、Firefox77.0.1では<1 empty slot>と表示されていました。
Google Chrome83.0.4103.116では、emptyと表示されていました。
forEachで順番に取り出し
セットした要素は、forEachで順番に取り出すことができます。
JavaScript
console.log(array);
array.forEach( ( v , i )=>{
console.log( `index:${ i } value: ${ typeof v === "string" ? `"${v}"` : v } `);
});
※forEachメソッドの2番目の引数は、配列のインデックス番号です。
ただし8番目は要素がないので、取り出せません。
実行結果
Array(11) [ "a", "c", 1, "v", "1", "d", "2", 2 , undefined, "x", "z"]
index:0 value: "a"
index:1 value: "c"
index:2 value: 1
index:3 value: "v"
index:4 value: "1"
index:5 value: "d"
index:6 value: "2"
index:7 value: 2
index:9 value: "x" ← index:8がないのでスキップされた
index:10 value: "z"
なお、明示的にundefinedをセットすると、forEachで取り出すことができます。
JavaScript
array[8]=undefined; ← index:8に値をセット
array.forEach( ( v , i )=>{
console.log( `index:${ i } value: ${ typeof v === "string" ? `"${v}"` : v } `);
});
実行結果
Array(11) [ "a", "c", 1, "v", "1", "d", "2", 2 , undefined, "x", "z"]
index:0 value: "a"
index:1 value: "c"
index:2 value: 1
index:3 value: "v"
index:4 value: "1"
index:5 value: "d"
index:6 value: "2"
index:7 value: 2
index:8 value: undefined ← index:8が表示された
index:9 value: "x"
index:10 value: "z"
forループで取り出し
インデックスを参照したいときは、前項のようにforEachのコールバック関数の二つ目を利用するか、forループを使います。
インデックスの最大値は、lengthプロパティ-1で取得できます。
const len = array.length;
console.log("インデックス最大値=" + (len-1));
for(let i = 0; i < len ; i++){
console.log(i.toString() + ':' + array[i]);
}
実行結果
インデックス最大値=10
0:a
1:c
2:1
3:v
4:1
5:d
6:2
7:2
8:undefined
9:x
10:z
配列もオブジェクト
そもそもJavaScriptの配列は、配列としての機能をもったArrayという名前のオブジェクトです。
他のオブジェクトと同じように、インデックス以外のプロパティを追加することができます。
インデックス以外のプロパティを追加
const array = [ "a" , "b" , "c" ];
array.prop1 = 12345;
console.log( array.prop1 );
実行結果
12345
ただし、配列のために用意されたオブジェクトという理由のためか、数値とみなせないプロパティ名はforEachでリストアップされません。
forEachは数値インデックスのみリストアップ
const array = [ "a" , "b" , "c" ];
array.prop1 = 12345;
array["5"] = "d";
array.forEach( ( v , i )=>{
console.log( `index:${ i } value: ${ typeof v === "string" ? `"${v}"` : v } `);
});
実行結果
index:0 value: "a"
index:1 value: "b"
index:2 value: "c"
index:5 value: "d"
他と比べて少し特殊ですが、あくまでオブジェクトだということを覚えておいてください。
連想配列の挿入順序は保証されない
連想配列は、挿入した順番を記憶していません。
そのため、順番に取り出すことができません。
そもそも、JavaScriptには連想配列という仕組みはありません。
連想配列ではなくオブジェクト
連想配列とは、数値以外のキーも使用できる配列です。
ネットで連想配列を作成する方法を調べると、次のような方法を紹介していることがあります。
JavaScript
const rensoHairetsu={"a":"1","b":"2","b":"3"};
上のコードは、実際には連想配列ではなくてオブジェクトを作成しています。
またキーではなく、プロパティと呼びます。
JavaScriptで連想配列という言葉を使うと、笑われてしまうので注意しましょう。
数値プロパティが先にくる
では次のようなオブジェクトを作成して、内容を確認してみます。
JavaScript
const obj={ "a" : "1" , "z" : "2" , "b" : "3" , 1 : "4" , "1" : "5" };
obj.c="6";
obj.e="7";
obj[2]="8";
実行結果
Object { 1: "5" , 2: "8" , a: "1", z: "2" , b: "3" , c: "6" , e: "7" }
実行結果を見ると、挿入した順番になっていません。
アルファベットのプロパティは、挿入順になっています。
しかし数値のプロパティが先頭にきています。
※これらの挙動はブラウザによって異なります。
また上の例では、次のように同じ数値のプロパティを、数字と文字列で指定しています。
1 : "4" ← プロパティを数値で指定
"1" : "5" ← プロパティを文字列で指定
しかし実行結果には、次の値しか残っていません。
1: "5"
数字・文字列にかかわらず同じ数値は、同じプロパティと判断されて、最後にセットした値が残るのです。
少しこの項目は正確性に欠けるかもしれません。
ここで例示しているのは、console.log が obj の内容を読み取ってコンソールに表示したものです。
つまり console.log() が obj のプロパティを順番に表示しているかどうかは、ブラウザの実装次第ということです。
forEachでの取り出し
次に、プログラムで順番に取り出してみます。
自分で作成したオブジェクトはforEachメソッドを持っていません。
プロパティを列挙したい場合はObject.keysでプロパティの配列を取得して、その配列に対してforEachを使用します。
JavaScript
Object.keys(obj).forEach(function(key) {
console.log(key+":"+obj[key]);
});
// わかりやすいように分割してみる
// let key = Object.keys(obj); プロパティ一覧を配列で取得する
// key.forEach(・・・ 取得したプロパティ一覧の配列をforEachで列挙
//
実行結果
1 : 5 2 : 8 a : 1 z : 2 b : 3 c : 6 e : 7
一応ですが、「数値プロパティが先にくる」の実行結果で得られた順番になっています。
ですがObject.keys()の解説に次のように書いてあります。
Object.keys() メソッドは、指定されたオブジェクトが持つプロパティの 名前の配列を、通常のループで取得するのと同じ順序で返します。
引用:https://developer.mozilla.org/
通常のループとはなんでしょうか…
なんともあやふやな表現ですね。
オブジェクトからプロパティを取り出すときの順番は、明確な法則がありません。
オブジェクトに値をセットした順番に依存したプログラムは、作成するべきではありません。
追記:2021/1
ECMAScript2020言語仕様9.1.11 [[OwnPropertyKeys]] ( )に、オブジェクトからプロパティを配列に変換するアルゴリズムが掲載されています。
これによると、配列インデックスとして有効なもの(数値)を昇順で、次に配列インデックスでない文字列を昇順で、最後にシンボルを作成順で抜き出しているのがわかります。
配列で連想配列を実現する※非推奨
2次元の配列を利用するすることで、挿入順番を維持した連想配列のようなものを実現できます。
連想配列のようなもの : キーと値からなるデータ保持機能
JavaScript
Array.prototype.aset = function(key,value){
let len = this.length;
for(let i = 0; i < len ; i++ ){
if( key === this[i][0]){
this[i][1]=value;return this;
}
}
this.push([key,value]);
return this;
};
let array = [ ["z","0"] , ["zz","00"] ].aset("a","1").aset("b","2");
array.aset(1,"3");
array.aset("1","4");
array.aset("b","5");
array.aset(1,"6");
array.aset("1","7");
console.log(array);
入れ物が配列なので、順番は保持されます。
数値型と文字列型も保持されています。
実行結果
0: Array [ "z", "0" ]
1: Array [ "zz", "00" ]
2: Array [ "a", "1" ]
3: Array [ "b", "5" ]
4: Array [ 1, "6" ]
5: Array [ "1", "7" ]
キーから値を取得するメソッドを作成します。
JavaScript
Array.prototype.avalue = function(key){
let len = this.length;
for(let i = 0; i < len ; i++ ){
if( key === this[i][0])
return this[i][1];
}
return false;
};
console.log( "1 = >" + array.avalue(1) );
console.log( "\"1\" = >" +array.avalue("1") );
実行結果
1=>6
"1"=>7
数値型と文字列型について個別に値を取得できました。
しかしこのコードは、要素の削除など必要な機能がたりません。
Arrayオブジェクトのプロトタイプにメソッドを追加する行為も、少々問題があります。
Mapオブジェクトで連想配列を実現する
最近知ったのですが、連想配列のような機能を持つオブジェクトがすでに存在していました。
Mapオブジェクトを使用すると、キーと値の保持が簡単にできます。
■Map | https://developer.mozilla.org/
JavaScript
let array2 = new Map([ ["z","0"] , ["zz","00"] ]).set("a","1").set("b","2");
array2.set(1,"3");
array2.set("1","4");
array2.set("b","5");
array2.set(1,"6");
array2.set("1","7");
for (let [key, value] of array2) { // array2.forEach((key,value)=>{})も使用可能
console.log(key + ":" + value);
}
セットした順番、および数値型と文字列型について個別に値を取得できました。
実行結果
z:0
zz:00
a:1
b:5
1:6
1:7
個別のキーの値も取得できます。
JavaScript
console.log("1=>" + array2.get(1));
console.log("\"1\"=>" +array2.get("1"));
実行結果
1=>6
"1"=>7
キーが存在するかどうかの確認は、has()を使用します。
JavaScript
console.log("has"+array2.has(1));
実行結果
has:true
要素の削除はdelete()を使用します。
成功時はtrue、削除できなかったときはfalseを返します。
JavaScript
console.log("size="+array2.size);
console.log("delete:"+array2.delete(1));
console.log("has(1):"+array2.has(1));
console.log("has(\"1\"):"+array2.has("1"));
console.log("size="+array2.size);
実行結果
size=6
delete:true
has(1):false // 数字の1は削除された
has("1"):true // 文字列の1は残っている
length=5
Mapオブジェクト内の要素数を取得するときは、sizeプロパティを使用します。
ついlengthを使いたくなりますが、ダメなんですね。
でも強引に使ってみます。
JavaScript
Object.defineProperty( // Mapオブジェクトにlengthプロパティを追加
Map.prototype, 'length',{
get:function(){return this.size;}
}
);
let array3 = new Map();
// Mapに要素を追加
console.log("length=" + array2.length);
console.log("size=" + array2.size);
lengthとsizeの値が同じになるはずです。
単なるネタですので、sizeを使いましょう。
配列を派生させて連想配列を実現する
僕がMapオブジェクトを知らないときに、配列で連想配列を実現できないかと試行したときのコードを掲載しておきます。
単なるネタコードなので、時間があったら追ってみてください
JavaScript
class AssociativeArray extends Array{
constructor(){
const propMap ={ }; // キー(プロパティ)名:配列の番号 を格納
let propNum = 0;
super();
this.aset = function(key,value){
if( !(key in propMap) ) { // キーが作成済み
propMap[ key ] = propNum ++;
this.push( [ key , value ] );
// key をプロパティとして作成する
Object.defineProperties( this , {
[ key ]:{
get: function (){
return this[ propMap[ key ] ][ 1 ];
},
set: function (value) {
this[ propMap[ key ] ][ 1 ] = value;
return value;
}
}
});
}else{ // 作成済みキーでasetが呼び出された
this [ propMap[ key ] ][ 1 ] = value;
}
};
}
forEach ( callBack ) {
super.forEach( e => callBack( e[ 0 ] , e[ 1 ]) );
}
// イテレータ定義
*[Symbol.iterator](){
for( let i = 0; i < this.length ; i ++ ) {
yield this[ i ];
}
}
}
派生させるならclass構文が楽です。
元々ある配列は、[ [キー名,値] , [キー名,値] , [キー名,値] ... ] という形式で利用しています。
asetメソッドが呼び出されると、propMapにキーと配列のインデックスの対応を格納します。
次に、キー名をプロパティ名としたゲッターとセッターをthisに定義しています。
[キー名,値] のキー名を直接変更できるので整合が取れなくなるとか、要素を削除したときどうするのかとか、arr.aset( "forEach",100 )とかするとforEachを上書きするよとか、そもそも機能が足りないなど問題点が多いので、このまま使用することはできません。
しかし 「asetはconstructor内で定義しているのに、forEachは外側なのはなぜなのか」(クロージャ)とか、「*[Symbol.iterator]()って何なのか」(イテレーターとジェネレーター)など理解できれば、JavaScriptの初級者を卒業したと言っていいのではないかと思います。
実行例
const arr = new AssociativeArray();
arr.aset("data1",123);
arr.aset("data2","abcd");
console.log( arr.data1 ); // 結果: 123
console.log( arr.data2 ); // 結果: abcd
arr.data2 = 1000;
console.log( arr.data2 ); // 結果: 1000
arr.forEach( ( key , value ) => console.log(key + ":" + value) );
// 結果: data1:123
// data2:abcd
for ( let [ key , value ] of arr ) {
console.log(key + ":" + value);
// 結果: data1:123
// data2:abcd
}
まとめ
JavaScript連想配列は、正確にはオブジェクトです。
またキーは、オブジェクトではプロパティといいます。
オブジェクトのプロパティは、挿入順を記憶していません。
そのため、列挙順は不定です。
配列やMapオブジェクトなど挿入順で列挙できるオブジェクトが存在しますが、それらは、挿入順を記憶していないオブジェクトから派生させたものです。
そして派生させたオブジェクトに、データを追加した順を記憶するようにコードを実装しているのです。
更新日:2020/06/19
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。