javascriptの連想配列またはマップ

連想配列とは

javascriptの連想配列は、一般の配列が格納位置を示すのに数値を使うのに対して、文字列を使うようにしたものと捉えることができます。

しかし、仕組みとしてはオブジェクトのプロパティを使って文字列と格納された値を対応付けるものなので、別のものです。Mozillaの MDN web docs では、配列は「ビルトインオブジェクト」の「Array」に書かれていますが、そこに連想配列の説明はありません。「式と演算子」の「プロパティへのアクセス」に書かれています。そこには「オブジェクトは実際には連想配列(別名 map、 dictionary、 hash、 lookup table)とみなすことができます。」と書いてあり、じゃぁ連想配列は本当はオブジェクトと呼ぶべきかというと、Arrayだってオブジェクトですから区別がつきません。

オブジェクトという言葉は対象を曖昧にしてしまいますので、ここでは連想配列で行きます。

さらにマップというデータ構造がECMAScrpt2015から導入されたとあるので、ゆくゆくはそちらに移行するのだろうと思います。

配列の格納位置を示す番号は「インデックス」「添字」などと呼ばれ、それを使って参照するものは「値」と呼ばれます。

連想配列の場合はプロパティを意識すると「(プロパティの)名前」と「(プロパティの)値」となり、他の言語からの類推でマップと意識すると「キー(key)」と「値(value)」と呼ばれます。key, valueはメソッド名や関数名で出てくるのでよく使われます。

連想配列の生成

一般の配列。

var names = ["mike","tama","kuro"];

連想配列。

var fruits = {'apple':'りんご','orange':'みかん','peach':'もも'};

名前(キー)と値をコロンで区切って列挙します。

この用途では ' 'と " " に区別はありません。

apple, orange 部分には ' ' がなくても受け入れられます。

function arrayTest01(){
   var tbl01 = document.getElementById("test01");
   var tds = tbl01.getElementsByTagName("td");
   var fruits = {'apple':'りんご','orange':'みかん','peach':'もも'};
   tds[1].firstChild.nodeValue = fruits['apple']; //needs''
   tds[2].firstChild.nodeValue = fruits.orange;
   tds[3].firstChild.nodeValue = fruits;
   tds[4].firstChild.nodeValue = "要素数 "+Object.keys(fruits).length;
}

fruits[0]に相当するfruits['apple']には ' ' が必要です([apple]だとappleという変数の値を使おうとします)。

fruits.orangeはドット演算子での参照で角括弧[]とほぼ同様に使えます。

[]で参照できるのは共通ですが、配列名だけでは内容を列挙してくれないし、lengthも使えないなどかなり差があります。

tds[0]tds[1]tds[2]tds[3]tds[4]

連想配列の作り方とタイプ

配列には3つの作り方がありました。連想配列も同様と思ったのですが、初期値を入れた状態で作れませんでした。配列はarray,連想配列はObjectと宣言します。配列の発展として連想配列を捉えていたのですが、かなり異なるものでした。他の言語の事情と混同していたようです。そこで、配列と連想配列で変数のタイプとしての違いがあるかも調べると予想に反してみんなObjectでした。

function arrayTest01b(){
   var tbl01b = document.getElementById("test01b");
   var tds = tbl01b.getElementsByTagName("td");
   var names1 = ["mike","tama","kuro"];
   var names2 = new Array("mike","tama","kuro");
   var names3 = Array("mike","tama","kuro");
   var fruits1 = {'apple':'りんご','orange':'みかん','peach':'もも'};
   var fruits2 = new Object();
   var fruits3 = Object();
   tds[0].firstChild.nodeValue = typeof names1;
   tds[1].firstChild.nodeValue = typeof names2;
   tds[2].firstChild.nodeValue = typeof names3;
   tds[3].firstChild.nodeValue = typeof fruits1;
   tds[4].firstChild.nodeValue = typeof fruits2;
   tds[5].firstChild.nodeValue = typeof fruits3;
   tds[6].firstChild.nodeValue = typeof tds;
   tds[7].firstChild.nodeValue = typeof tbl01b;
   tds[8].firstChild.nodeValue = typeof arrayTest01b;
}

配列
連想配列
その他

連想配列も配列も typeof では Object です。

値の型は混在可

普通の配列と同様に値の方には様々な型の変数やオブジェクトを格納できます。

キーの方には文字列を使います。文字列以外(たとえば数値)を使っても文字列として扱われます。

function arrayTest02(){
   var tbl02 = document.getElementById("test02");
   var tds = tbl02.getElementsByTagName("td");
   var iroiros = {string:"mike", int:365, boo:true, object:tbl02, array:tds};
   tds[0].firstChild.nodeValue = iroiros['string'];
   tds[1].firstChild.nodeValue = iroiros['int'];
   tds[2].firstChild.nodeValue = iroiros['boo'];
   tds[3].firstChild.nodeValue = iroiros['object'];
   tds[4].firstChild.nodeValue = iroiros['array'];
   var ofs = 5;
   tds[0+ofs].firstChild.nodeValue = typeof iroiros['string'];
   tds[1+ofs].firstChild.nodeValue = typeof iroiros['int'];
   tds[2+ofs].firstChild.nodeValue = typeof iroiros['boo'];
   tds[3+ofs].firstChild.nodeValue = typeof iroiros['object'];
   tds[4+ofs].firstChild.nodeValue = typeof iroiros['array'];
}

位置tds[0]tds[1]tds[2]tds[3]tds[4]
値の型

"mike"は文字列、365は数値などですが、typeof で確認することができます。

要素ごとに異なるタイプの値を入れることができるのは、ちょっと驚きです。

キーの型

キーは文字列として扱われます。{ }での定義では ' 'で囲んだのと同様の扱いを受けるということを示します。その文字列が変数名であったときにも関係がないことを示します。var shiro = 'white' としていて、'white'をキーにするつもりでshiroを指定してもキーは'shiro'になるということです。

参照で[変数名]とすると変数に格納されて値を文字列に変換してそれをキーにして値を探します。'white'はないので出てはきません(ブラウザによってはundefinedになります)。

いたずらで、var san = 3;については 3もキーにして'three'を格納しているので、sanでも'3'でも'three'が参照できます。

function arrayTest02b(){
   var tbl02b = document.getElementById("test02b");
   var tds = tbl02b.getElementsByTagName("td");
   var shiro = 'white';
   var san = 3;
   var honto = true;
   var iroiros = {shiro:"しろ", san:"さん", honto:"真", 3:"three", tbl02b:"表2b"};
   tds[0].firstChild.nodeValue = iroiros['shiro'];
   tds[1].firstChild.nodeValue = iroiros['san'];
   tds[2].firstChild.nodeValue = iroiros['honto'];
   tds[3].firstChild.nodeValue = iroiros['3'];
   tds[4].firstChild.nodeValue = iroiros['tbl02b'];
   var ofs = 5;
   tds[0+ofs].firstChild.nodeValue = iroiros[shiro];
   tds[1+ofs].firstChild.nodeValue = iroiros[san];
   tds[2+ofs].firstChild.nodeValue = iroiros[honto];
   tds[3+ofs].firstChild.nodeValue = iroiros[3];
   var tbl01 = document.getElementById("test01");
   tds[4+ofs].firstChild.nodeValue = iroiros[tbl02b];
   var ofs = 10;
   var keys = Object.keys(iroiros);
   tds[0+ofs].firstChild.nodeValue = keys[0];
   tds[1+ofs].firstChild.nodeValue = keys[1];
   tds[2+ofs].firstChild.nodeValue = keys[2];
   tds[3+ofs].firstChild.nodeValue = keys[3];
   tds[4+ofs].firstChild.nodeValue = keys[4];
   var ofs = 15;
   tds[0+ofs].firstChild.nodeValue = typeof keys[0];
   tds[1+ofs].firstChild.nodeValue = typeof keys[1];
   tds[2+ofs].firstChild.nodeValue = typeof keys[2];
   tds[3+ofs].firstChild.nodeValue = typeof keys[3];
   tds[4+ofs].firstChild.nodeValue = typeof keys[4];
}

位置tds[0]tds[1]tds[2]tds[3]tds[4]
文字列で参照
変数名で参照
キーの一覧
キーの型

キーの一覧を取得すると(Object.keys(iroiros))、3だけ前に出てきます。一般的に「キー:値」の順序は格納順になるとは限らないので、それほど驚くことではありませんが、文字列の方は格納順に見えます。キーの型は3を含めてすべて文字列です。

これについて、要素の追加の場合の振る舞いについての発展、ECMAScrpt2015から導入されたマップとの違いの発展もあります。

空の連想配列から角括弧記法で要素追加と参照

生成の時に要素が空であれば、要素数0の連想配列ができます。

var karas = {};

キーと値の組であれば、後から追加したり削除したりは普通のことと考えられます。

角括弧[ ]を使って配列と同様に要素を追加できます。連想配列と呼ばれる理由です。

karas['apple'] = 'りんご';
var kajitsu = karas['orange']; 
function arrayTest03(){
   var tbl03 = document.getElementById("test03");
   var tds = tbl03.getElementsByTagName("td");
   var karas = {};
   karas['apple'] = 'りんご';
   karas['orange'] = 'みかん';
   karas['peach'] = 'もも';
   karas['orange'] = 'オレンジ';
   var pine = 'pineapple';
   karas[pine] = 'パイナップル';
   tds[0].firstChild.nodeValue = "['apple']";
   tds[1].firstChild.nodeValue = "['orange']";
   tds[2].firstChild.nodeValue = "['peach']";
   tds[3].firstChild.nodeValue = "['pineapple']";
   var apple = 'peach';
   tds[4].firstChild.nodeValue = "[apple]";
   var ofs = 5;
   tds[0+ofs].firstChild.nodeValue = 'apple';
   tds[1+ofs].firstChild.nodeValue = 'orange';
   tds[2+ofs].firstChild.nodeValue = 'peach';
   tds[3+ofs].firstChild.nodeValue = 'pineapple';
   tds[4+ofs].firstChild.nodeValue = apple;
   var ofs = 10;
   tds[0+ofs].firstChild.nodeValue = karas['apple'];
   tds[1+ofs].firstChild.nodeValue = karas['orange'];
   tds[2+ofs].firstChild.nodeValue = karas['peach'];
   tds[3+ofs].firstChild.nodeValue = karas['pineapple'];
   tds[4+ofs].firstChild.nodeValue = karas[apple];
}

[ ]
key
value

結果から、次の事柄が確認できます。(1)同じキーを使うと値は上書きされること、(2)[ ]に文字列でなく変数を書くと変数の値がキーとして使われること。

この他の書き方もあります。次の2つは同じ連想配列を作ります。

var karas = {};
var karas = new Object();

空の連想配列からドット記法で要素追加と参照

角括弧の代わりにドット記法を使います。オブジェクトと見るとこちらのほうが自然です。

karas.apple = 'りんご';
var kajitsu = karas.orange; 

ただ、ドットの後ろには変数を書くことができません。

また、プロパティ名の扱いになりますから、数字で始まったり、途中にスペースがあったりする場合はドット記法は使えません。角括弧を使うことになります。

function arrayTest04(){
   var tbl04 = document.getElementById("test04");
   var tds = tbl04.getElementsByTagName("td");
   var karas = {};
   karas.apple = 'りんご';
   karas.orange = 'みかん';
   karas.peach = 'もも';
   karas.orange = 'オレンジ';
   var pine = 'pineapple';
   karas[pine] = 'パイナップル'; // karas.pineでは karas.pineapple にならない
   tds[0].firstChild.nodeValue = '.apple';
   tds[1].firstChild.nodeValue = '.orange';
   tds[2].firstChild.nodeValue = '.peach';
   tds[3].firstChild.nodeValue = '.pineapple';
   var apple = 'peach';
   tds[4].firstChild.nodeValue = '[apple]';
   var ofs = 5;
   tds[0+ofs].firstChild.nodeValue = karas.apple;
   tds[1+ofs].firstChild.nodeValue = karas.orange;
   tds[2+ofs].firstChild.nodeValue = karas.peach;
   tds[3+ofs].firstChild.nodeValue = karas.pineapple;
   tds[4+ofs].firstChild.nodeValue = karas[apple]; // karas.appleでは karas.peach にならない
}

書き方
value

繰り返し機構(1)

for文でindexを使ってすべての要素にアクセスするのは配列の基本ですが、連想配列ではindexがないので使えません。

連想配列に使えるのは次の2つ

for ... in は for (変数 in オブジェクト) で得られる変数(実はプロパティ名)をkeyにして値を参照します
  for(var k in fruits)
Object.keys は Object.keys(オブジェクト) で得られるkeyの配列を使って値を参照します
  Object.keys(fruits)

keyが全て英字ならば平和なのですが、あえて数字を入れてkeyの順序が登録順とは限らないことを示します。下の数字の15は登録は文字列として登録されますから不正ではありません。英字文字列部分を見ると文字コード順になっているわけではないようです。

function arrayTest05(){
   var tbl05 = document.getElementById("test05");
   var tds = tbl05.getElementsByTagName("td");
   var fruits = {orange:'みかん', peach:'もも',apple:'りんご', 15:'いちご'};
   var i = 0;
   for(var k in fruits){
      tds[i].firstChild.nodeValue = k + ":"+ fruits[k];
      i++;
   }
   var keys = Object.keys(fruits);
   for(var k=0;keys.length>k;k++){
      tds[i].firstChild.nodeValue = keys[k] + ":"+ fruits[keys[k]];
      i++;
   }
}

0123
in fruits
Object.keys

繰り返し機構(2 蛇足)

この項は蛇足です。(1)は、どちらもまずkeyを取得しそれを使ってvalueを参照します。これで十分ですが、valueを直接得る方法もあるとバランスがとれます。

for ... of は 連想配列には使えません
  for(var v of fruits) だから使えないってば。
Object.values は Object.values(オブジェクト) で得られるvalueの配列を使って値を参照します
  Object.values(fruits)

for ... in に対して for ... Of がありますが、これは連想配列には使えません。not iterable というエラーになります。iterable なオブジェクトとは、ArrayやMap,Setなどをいいます。使いたければ連想配列でなくMapを使えということです。Mapは ECMAScript 2015の規格からですからまだ使えない環境が残っているかもしれません。とにかく連想配列ではエラーになります。

Object.keysに対してはObject.valuesがあります。また、keyとvalueがセットで得られる、Object.entriesがあります。この2つは ECMAScript 2017の規格からですから使えない環境があります。

使えるかどうかは、それぞれ確認してください。

function arrayTest05b(method){
   var tbl05 = document.getElementById("test05b");
   var tds = tbl05.getElementsByTagName("td");
   var fruits = {orange:'みかん', peach:'もも',apple:'りんご', 15:'いちご'};
   var i = 0;
   try{
      if(method=='of'){
         i = 0;
         for(var v of fruits){
            tds[i].firstChild.nodeValue = v;
            i++;
            //TypeError: fruits is not iterable
          }
      }else if(method=='values'){
         i = 4;
         var vals = Object.values(fruits);
         for(var v=0;vals.length>v;v++){
            tds[i].firstChild.nodeValue = vals[v];
            i++;
          }
      }else if(method=='entries'){
         i = 8;
         var keyvals = Object.entries(fruits);
         for(var k=0;keyvals.length>k;k++){
            tds[i].firstChild.nodeValue = keyvals[k][0] +"/"+ keyvals[k][1];
            i++;
         }
      }
   }catch(e){
      tds[i].firstChild.nodeValue = e;
   }
}

ここでは、try-catchを使ってエラーを表示しています。

0123
of fruits
Object.values
Object.entries

繰り返し機構(3 さらに蛇足)

連想配列でやったことを iterable なオブジェクトであるArrayに対して使ってみようということで追加です。

不連続な番号をあえて使うようなことをしなければ、keyは連番が返るだけですし、Object.valuesは元の配列と同じものが得られるだけですから、ほとんど意味のないことをしています。

でも [key,value] of だけは不思議な結果です。文字列は iterable なオブジェクトですが、一文字目がkey,二番目がvalueになっています。そもそもマップの解説にあったものを使ってみたものです。

[key,value] of 以外は当たり前の結果なので、もうひとついたずらをしてみました。配列もオブジェクトなのでプロパティを追加できます。 melom:'メロン' を追加してみました。結果、in,of では異なる結果になります。Object.*では数値も文字列のkeyと全く区別なく取り扱われています。

function arrayTest05c(method){
   var tbl05 = document.getElementById("test05c");
   var tds = tbl05.getElementsByTagName("td");
   var vegs = ['onion','eggplant','carrot'];
   vegs['melon'] = "メロン";
   var i = 0;
   try{
      if(method=='in'){
         i = 0;
         for(var k in vegs){
            tds[i].firstChild.nodeValue = k;
            i++;
         }
      }else if(method=='of'){
         i = 4;
         for(var v of vegs){
            tds[i].firstChild.nodeValue = v;
            i++;
         }
      }else if(method=='kv'){
         i = 8;
         for(var [k,v] of vegs){
            tds[i].firstChild.nodeValue = k+"/"+v;
            i++;
         }
      }else if(method=='keys'){
         i = 12;
         var keys = Object.keys(vegs);
         for(var k=0;keys.length>k;k++){
            tds[i].firstChild.nodeValue = keys[k];
            i++;
         }
      }else if(method=='values'){
         i = 16;
         var vals = Object.values(vegs);
         for(var v=0;vals.length>v;v++){
            tds[i].firstChild.nodeValue = vals[v];
            i++;
         }
      }else if(method=='entries'){
         i = 20;
         var keyvals = Object.entries(vegs);
         for(var k=0;keyvals.length>k;k++){
            tds[i].firstChild.nodeValue = keyvals[k][0] +"/"+ keyvals[k][1];
            i++;
         }
      }
   }catch(e){
      tds[i].firstChild.nodeValue = e;
   }
}

ここでは、try-catchを使ってエラーを表示しています。 新機能に対応していないブラウザでエラーになる場合があります。

0123
in fruits
of fruits
[k,v] of fruits
Object.keys
Object.values
Object.entries

連想配列に感動した昔

連想配列に出会ったのは1993年のことでした。UNIX上のawkでした。

次のような名前と数値が組になったデータから、名前ごとの合計を出すというものです。

onion 50
carrot 226
carrot 315
eggplant 30
radish 75
eggplant 30
onion 120
carrot 417
eggplant 23
onion 80
radish 89
carrot 715
onion 95
radish 135
radish 98

こんな感じでできてしまいます。

$ awk '{sum[$1]+=$2} END {for(name in sum)print name,sum[name]}' onion.txt 
radish 397
eggplant 83
carrot 1673
onion 345

END以下は結果の表示のためのforで、計算の本体は連想配列 sum[$1] += $2 です。 $1がキー、$2が値でキーが同じ場所に値を加算するものです。

これを javascript でやってみます。

awkではファイルから読み込みますが、html経由で使うjavascriptではそうは行きません。DOMを使ってページから読むのでは文字列から数値への変換が必要になりちょっと面倒です。ここでは[key,value]の配列の配列を作っておき、そこからスタートします。

function arrayTest06(){
   var tbl06ans = document.getElementById("test06ans");
   var tds = tbl06ans.getElementsByTagName("td");
   var veg = [
      ['onion', 50] , ['carrot', 226] , ['carrot', 315] ,  ['eggplant', 30] ,
      ['radish', 75] , ['eggplant', 30] , ['onion', 120] , ['carrot', 417] ,
      ['eggplant', 23] , ['onion', 80] , ['radish', 89] , ['carrot', 715] ,
      ['onion', 95] , ['radish', 135] , ['radish', 98] ];
   var sum = {};
   for(var tdi=0;veg.length>tdi;tdi++){
      var key = veg[tdi][0];
      if(key in sum){
         sum[key] += veg[tdi][1];
      }else{   			
         sum[key] = veg[tdi][1];
      }
   }
   var i=0;
   for(var key in sum){
      tds[i].firstChild.nodeValue = key;
      tds[i+1].firstChild.nodeValue = sum[key];
      i+=2;
   }
}

未登録のキーを参照したときに、undefinedになるので、その対策が必要な分感激は薄れます。

当時は普段使用できる言語はBASICぐらいしかなく、名前の配列と合計の配列をそれぞれ作って新出の名前を登録しながら合計に数値を足していく必要がありました。その配列も値を探す関数(indexOf()のような)とか要素数の追加のできないものでしたから、awkは革命的に見えました。

要素の追加と削除

配列の場合は要素の順番が固定だったので、末尾に追加したり先頭に追加したりしましたが、 連想配列では順序の固定が保証されないので単なる追加と削除になります。

delete fruits['apple'];
delete fruits.orange;

もともと存在しないキーを指定しても無視されるだけです。

次の例では表のセルから文字列を読んで連想配列に格納し、それからdeleteして次のセルに書き出すという操作をしています。読み書きをする時の例外処理で複雑になっているのですが、delete部分が極端に単純で例外処理がいらない分、他の部分に手を掛けてみました。

key in 連想配列 でkeyの存在を確認できます。ここでは不要なのですが使ってみました。

historyは操作回数(1が初期状態)。ptrは次に書き出すセル番号。ptr-2はデータを読み出すセル番号

splitは区切り文字を指定して文字列を区切り配列を作るメソッドですが、元の文字列が空のときは、空の配列ではなく 1つの空文字列をからなる配列を返します。この仕様に合わせた設計をしています。

書き出しが複雑なのは最後に","を付けないためのものです。

表の見た目のわかりやすさと、プログラムのわかりやすさの折り合いは非妙な所があります。

function arrayTest07(dkey){
   var tbl07 = document.getElementById("test07");
   var tds = tbl07.getElementsByTagName("td");
   var history = tds[0].firstChild.nodeValue;
   var ptr = history*2+1;
   if (ptr>tds.length) return;
   var propsstr = tds[ptr-2].firstChild.nodeValue;
   var propslist = propsstr.split(",");
   //文字列が空であるとき、split は、空の配列ではなく、1 つの空文字列を含む配列を返す
   var fruits = {};
   for(var i=0;propslist.length>i;i++){
      var propkv = propslist[i].split(":");
      if(propkv.length>1) fruits[propkv[0]] = propkv[1];
   }
   var message = (dkey in fruits) ? dkey+" deleted" : dkey+" not exist";
   delete fruits[dkey];
   var keys = Object.keys(fruits);
   var kvstr = '';
   if(keys.length>0){
      var kvstr = keys[0]+":"+fruits[keys[0]];
      for(var k=1;keys.length>k;k++){
         kvstr +=  ","+keys[k] + ":" + fruits[keys[k]];
      }
   }
  tds[ptr-1].firstChild.nodeValue = message;
  tds[ptr].firstChild.nodeValue = kvstr;
  tds[0].firstChild.nodeValue = ++history;
}

テスト07

No./削除要素連想配列の内容
1 apple:りんご,orange:みかん,peach:もも

要素の削除だけならば、dkey in fruits で存在を確認する必要はありません。

5回まで削除操作ができます。ないものを指定しても無視されます。

マップは今は略します

最近のバージョンではマップという構造が使えるようになっています。

javascriptは閲覧者の使っているブラウザにより新しい仕様に対応していないことがあります。作成者側でバージョンを管理できないので、共通な仕様で書かなければなりません。保留しておきます。

まとめ

連想配列はオブジェクトのプロパティの流用

Arrayは iterable という仕組みを持ったオブジェクト

一般のオブジェクトは iterable ではない

将来的には iterable な Mapが使われる様になるかもしれない。

作成

var fruits = {};

追加

fruits.key1=value1;
fruits.[key2]=value2;

繰り返し

var ks = Object.keys(fruits);
for(var k=0;ks.length>k;k++){
   ks[k],   .... fruits[ks[k]]
}
for(var k in fruits){
   k, ....fruits[k]
}

削除

delete fruits.key1;
delete fruits.[key2];