JavaScriptドリル 第1回 JSON文字列を扱う
JavaScriptの基本的な文法を押さえたら、少しずつ「解法」のストックを増やしていきましょう。第1回目はJSON文字列から特定の情報を取り出すパターンを考えます。
- カテゴリー
- JavaScript >
- JavaScriptドリル
発行
はじめに
JavaScriptを書けるようになるには、ある種の解決のパターン(解法)を覚える必要があります。解決のパターンにはさまざまな方法があり、どの方法でもたいてい目的の結果は得られます。しかし、その中でもシンプル、読みやすい、効率が良い、変更に強いといったきれいな解決方法は存在します。
このシリーズでは「ドリル」形式の問題を通して、さまざまな解決のパターンを学んでいきます。普段からJavaScriptを書いている方であれば簡単な、学習中の方でも解けるレベルの問題を想定しています。
ではさっそく今回の問題を見てみましょう。
JSON形式の文字列データの表示
問題
次のようなJSON形式の文字列データがあります。
var data = '[{"id":1,"name":"中村 享介"},{"id":2,"name":"高津戸 壮"},{"id":3,"name":"小山田 晃浩"},{"id":4,"name":"外村 和仁"},{"id":5,"name":"外村 奈津子"},{"id":6,"name":"德田 和規"},{"id":7,"name":"山田 順久"},{"id":8,"name":"小原 司"},{"id":9,"name":"山田 敬美"},{"id":10,"name":"坂巻 翔大郎"},{"id":11,"name":"中島 直博"}]';
JSONの構造は次のようにid
とname
というプロパティを持つオブジェクトの配列となっています。
[
{ "id":1, "name":"中村 享介" },
{ "id":2, "name":"高津戸 壮" },
...
]
このデータ構造からid
に対応するname
をコンソールに表示するか、name
を返す、staff
というfunctionを作成してください。もし、該当するid
を持つ人がいない場合はnull
を表示してください。
実行例
staff(1); // "中村 享介"
staff(5); // "外村 奈津子"
staff(999); // null
JavaScriptを勉強中の方も、すでに書ける方もぜひやってみてください*。
*注:問題のあいまいさ
「コンソールに表示するか、name
を返す」というあいまいな答えを求める問題になってしまっています。これはもともと「staff()で表示する」というあいまいな表現のまま問題を出してしまったことにより、社内の回答が揺れてしまったことが原因です。読者の方が混乱しないよう、回答に合わせた問題になっています。問題をやってみたい方は、あまり気にせず書いてみてください。
編集そとむらの回答
普段JavaScriptを書くことはない、しかしCodeGridを編集するため日々JavaScriptのコードを検証したりしているそとむら(@soto)に挑戦してもらいました。
var dataJson = data.replace(/'/g, "");
// console.log(dataJson);
var staffsData = JSON.parse(dataJson);
// console.log(staff[0].id);
// console.log(staff.length);
var staffNumber = staffsData.length;
function staff(id){
if (id >= 1 && id <= staffNumber){
console.log(staffsData[id-1].name);
} else {
console.log('null');
}
}
このコード*、実行してみると、一応問題の条件をほぼ満たしているように動作しますが、いくつか問題があります。
*注:コードのコメント
コードにはコメントアウトされたconsole.log()
が残っていますが、あえて回答時のまま掲載しています。
必要のない処理が入っている
var dataJson = data.replace(/'/g, "");
という行があります。元のデータから'
を全置換して削除しているようですが、そもそも元のデータである[]
内には'
はありません。確認したところJavaScript上文字列を表現するための'
がデータにも入っていると思ったそうです。
配列の順番によって名前を取り出している
この例のデータの場合は、たまたま配列の順序とidの番号が+1
するだけで一致しています。そのため問題なく動作しますが、これだとデータ内のidに欠番が発生した場合、また、順番が変わっているだけでも間違ったデータが取り出されてしまいます。配列の7番目にいる人と、IDが7番の人は違うのです。
いなかった場合のnullが文字列
問題ではユーザーが見つからなかった場合に返ってくる値はnull
を想定していましたが、文字列の'null'
が返ってきています。ここで想定しているのは文字列ではなく、値としてのnull
です。
問題もそうですし、実際の案件でも仕様書に日本語で書かれていることが多く、間違えるのも無理はないと思いますが、こういった部分がバグにつながるので、気を付けて実装するとよいでしょう。
なお、仕様によっては文字列として返すのが正しい場合もあったりするので、注意が必要です。
制御構文で解決する
そとむらがエンジニアからヒントをもらいつつ、修正したものが次のコードです。
var staffData = JSON.parse(data);
var staffNumber = staffData.length;
// console.log(staffNumber);
function staff(id){
var memo;
for (var i = 0; i < staffNumber; i++){
// console.log('id :' + staff[i].id + 'name:' + staff[i].name);
if ( staffData[i].id === id ){
console.log(staffData[i].name);
memo = staffData[i].name;
}
}
if (memo == undefined){
console.log('null');
}
}
制御構文を利用したコードとしては一般的な方法で、memo
という一時変数を使い、そこに対応するidの人がいれば一時保存します。こうすると、変数memoに何も入っていなかった場合(undefinedの場合)は、対応するidの人がいなかったという判定に利用できます。
ひとつ問題を指摘するとすれば、memo
という変数名が、何を意図しているのかわかりづらいのが難点です。staffName
など、この変数に入ってくる内容に合わせた名前にしたほうがよりわかりやすいコードになります。
JavaScriptの入門の本に載っているのは、このような方法が多いでしょう。for文、if文、変数の組み合わせでたいていの問題は解決できます。しかし、複雑な問題になればなるほど読みづらく、処理がわからなくなりがちです。
ほかのスタッフの回答も見てみましょう。
配列の項目をチェックする
山田 順久(@ykhs)の回答です。
var staff = function (id) {
var called = JSON.parse(data).some(function (person) {
if (person.id === id) {
console.log(person.name);
return true;
}
});
if (!called) {
console.log(null);
}
}
JSONをパースした配列に対してsome
を使っています。some
は引数に渡された関数によってテストした判定結果(true, false)を返す配列のメソッドです。
これを使い、staffの引数で渡されたidが、dataの中に含まれているかを判定し、含まれていれば合わせて名前を出力しています。
文字列として処理する
坂巻(@GeckoTang)の回答です。
function staff(id) {
var reg = new RegExp('"id":'+id+',"name":"([^"]*)"',"i");
var match = data.match(reg);
console.log( match ? match[1] : null );
};
データをJSON.parseせず、文字列のまま処理しています。正規表現部分で文字としてidのマッチを行い、その直後のnameの値部分にマッチした文字列を表示しています。
JSONデータの場合は、そこまでパフォーマンスが問題になることはないと思います。しかし、HTMLを処理する場合、DOM操作がパフォーマンス的に問題になると、文字列で処理することで改善できる場合があるので、このような方法も知っておくとよいでしょう。
nullの出し分けは三項演算子を使っています。このような簡易な条件分岐では三項演算子を使うとスッキリ書くことができます。
扱いやすいオブジェクトを生成する
外村(@hokaccha)の回答です。
var mapById = JSON.parse(data).reduce(function(map, item) {
map[item.id] = item;
return map;
}, {});
function staff(id) {
return mapById[id] ? mapById[id].name : null;
}
console.log(staff(1));
まずmapById
というオブジェクトをJSONをパースした配列からreduceを使って生成しています。reduce
はデータを累積して単一の値にする配列のメソッドです。
reduceの動作は解説されたWebページが多くありますので、そちらも参照してください。
reduceに第二引数を指定すると、その値を初期値として順番に配列の値を処理していくことができます。生成されたオブジェクトは、次のようなidをプロパティ名としたオブジェクトになります。
{
1: { id: 1, name: "中村 享介" },
2: { id: 2, name: "高津戸 壮" },
}
このような形式になっていると、staff関数内でやっているように対応するidのデータを特定することが簡単になります。
また、あらかじめオブジェクトを作っておくことで毎回JSONをパースする必要がなくなるので、その分の処理が軽く済みそうです。
フィルターした結果を表示する
問題を作成した筆者(@kyosuke)も回答を用意しました。
function staff(id) {
var person = JSON.parse(data).filter(function(v){
return v.id === id
})[0];
return person ? person.name : null;
}
配列のfilter
は渡された関数を使って、条件判定し、trueだったデータのみの新たな配列を作るメソッドです。これを使ってidが一致するものを抽出し、最初のものを使って判定を行っています。
実務では、このような配列に対する操作がある場合はUnderscore.js*を使うことが多いですが、この回答の考え方は同じです。
*注:Underscore.js
Underscore.jsに関しては「おすすめライブラリつまみ食い | Underscore.js:メソッド」や「おすすめライブラリつまみ食い | Underscore.js:Functions」などを参照してください。
function staff(id) {
var person = _.findWhere(JSON.parse(data), {id: id});
return person ? person.name : null;
}
_.findWhere
は条件としてオブジェクトを渡し、マッチしたものの最初の1つを返す関数です。処理の流れは先ほどのfilter
を使った場合と同じように、対応する人のみにフィルタしてその結果を元に判定しています。
配列の新しいメソッドを使う場合の注意
今回ご紹介したsome
、reduce
、filter
はIE9以降でしか対応していません。
最近は古いブラウザ対応の必要ない場合も多くありますが、注意が必要です。筆者の回答例で少し紹介した、Underscore.jsに含まれている_.some
、_.reduce
、_.filter
を使うと古いブラウザにも対応できます。
また、それ以外にも_.findWhere
*といった、配列操作に関する便利な関数を使うことができます。
*注:_.findWhrere
指定したキーと値の組み合わせにマッチしたもののうち、最初の値を返します。詳しくはドキュメントの「_.findWhere」を参照してください。
まとめ
同じ問題でも複数の解決方法があり、どの方法もメリットとデメリットがあります。きれいな解決方法もひとつではないのです。今回紹介した方法以外にもよい解決方法があると思います*。
*注:よい解決方法
ほかのよい解決案があればgistやjsfiddleなどにコードを置き、#codegridハッシュをつけてTwitterで教えてください。
多くの方法を知ることで、実際のプログラム時に適切な方法を選ぶことができ、きれいなコードを書くことができるようになります。プログラムが書ける人は、多くの解決方法の中から適切な方法を選んでいます。
問題の解決パターン(解法)を多く知り、そしてそれを場面に応じて使い分けられるために、少しでもお役に立てば幸いです。
このシリーズでは今後も現場で遭遇しそうな簡単な問題と解決方法について、解説していく予定です。