配列オブジェクトのコールバックタイプメソッド(その2)

#JavaScript の配列には #コールバック を抱えて走ってくれるメソッドがあります。以前の投稿では #forEach を取り上げました。今回は私の理解まで、 #map と #filter の特徴について取り上げます。
forEachと同じように、mapとfilterも配列要素を順に処理してくれますが、個性的であるがためその特徴をよく理解して使い所を見極める必要があります。少し頭を捻らなければならないのですが、その分、端的な表記で複雑な処理が達成できます。

まずは以前の投稿と同じく検証用の #多次元配列 ( #MIME と #拡張子 セットのテーブル)を生成します。つづくターミナル表示は、macOS上でSafariでのコンソール出力の表記です。また、「>」がコンソールへ私が打ち込んだ入力テキストで、「<」がリターンキーを押すと現れる出力情報です。

> tbl = new Array(
	['text/plain','txt'],
	['text/csv','csv'],
	['text/tsv','tsv'],
	['text/css','css'],
	['text/javascript','js'],
	['text/xml','xml'],
	['text/html','html']);
< [["text/plain", "txt"], ["text/csv", "csv"], ["text/tsv", "tsv"], ["text/css", "css"], ["text/javascript", "js"], ["text/xml", "xml"], ["text/html", "html"]] (7)

まずはmapからまいります。よくある解説では、map(callback [, that])とあって、「thatパラメータは関数callback内でthisを示すオブジェクト」だそうです。ここではアロー関数ばっか使うのでthatの出番はありません。callbackの中身はmap(function(要素, 位置, 配列) {})と表記されます。「要素」は繰り返し処理される配列の一要素、「位置」はその要素の配列内のインデックス、「配列」は処理される配列そのものです。forEachと全く同じです。それではアロー関数の先を単純に子配列の各要素を当ててみます。

> tbl.map((vc,id,ar)=>vc[0]);
< ["text/plain", "text/csv", "text/tsv", "text/css", "text/javascript", "text/xml", "text/html"] (7)

> tbl.map((vc,id,ar)=>vc[1]);
< ["txt", "csv", "tsv", "css", "js", "xml", "html"] (7)

ここで注目です。forEachでは入力しても #undefined が返ってきましたが、map(filterも同じく)では配列が返ってきます。
返ってきた配列は、多次元配列がバラけてそれぞれの配列になりました。これ、すっごい便利です。

つぎは同じことをfilterでやってみます。おっと、記述の仕方はmapとまったく同じです。

> tbl.filter((vc,id,ar)=>vc[0]);
< [["text/plain", "txt"], ["text/csv", "csv"], ["text/tsv", "tsv"], ["text/css", "css"], ["text/javascript", "js"], ["text/xml", "xml"], ["text/html", "html"]] (7)

元の多次元配列がまんま返ってきました。
アロー関数の先をvc[1]に替えても結果は同じなのですが、それじゃあ芸がないので条件にしてみます。

> tbl.filter((vc,id,ar)=>vc[1].length===3);
< [["text/plain", "txt"], ["text/csv", "csv"], ["text/tsv", "tsv"], ["text/css", "css"], ["text/xml", "xml"]] (5)

「vc[1].length===3」としました。「拡張子文字列長が3であれば真」ということになります。
すると文字列長が3にならない’js’と’html’のセットを除いた多次元配列が返ってきました。配列長が5と短くなってます。

それでは、同じ記述をmapで入力してみます。

> tbl.map((vc,id,ar)=>vc[1].length===3);
< [true, true, true, true, false, true, false] (7)

今度は真偽値だけの一次元配列が返ってきました。もとの多次元配列とはまったく異なるものになってしまいました。でも、配列長はもとの多次元配列と同じ7です。
まったく処理が異なりますね。それで最後に、最初の多次元配列を参照してみます。

> tbl
< [["text/plain", "txt"], ["text/csv", "csv"], ["text/tsv", "tsv"], ["text/css", "css"], ["text/javascript", "js"], ["text/xml", "xml"], ["text/html", "html"]] (7)

はい、もとのままで破壊されてません。mapとfilterも非破壊のメソッドです。これ重要です。

mapとfilterも配列要素を順に処理しますが、

  • mapはアロー関数の先の処理を返してくれます。要素の内容にコミットできるので、加工したり異なるタイプの配列にすることも可能です。
  • filterはアロー関数の先の条件に適合した要素を返してくれます。要素の内容を検証だけして抽出するので配列長が変化します。

これらは、アロー関数でなくとも動作は同じはずです。
試しに、違いがわかりやすいようすの少し複雑な処理をしてみます。解説の必要には及ばないと思います。(手抜きに感じたら失敬。)

> tbl.map((vc,id,ar)=>vc[1]+'あ');
< ["txtあ", "csvあ", "tsvあ", "cssあ", "jsあ", "xmlあ", "htmlあ"] (7)

> tbl.map((vc,id,ar)=>[vc[0],vc[1].length===3]);
< [["text/plain", true], ["text/csv", true], ["text/tsv", true], ["text/css", true], ["text/javascript", false], ["text/xml", true], ["text/html", false]] (7)

> tbl.map((vc,id,ar)=>[vc[0].indexOf('v'),vc[1].length===3]);
< [[-1, true], [7, true], [7, true], [-1, true], [7, false], [-1, true], [-1, false]] (7)

> tbl.filter((vc,id,ar)=>(vc[0].indexOf('v')!==-1 && vc[1].length===3));
< [["text/csv", "csv"], ["text/tsv", "tsv"]] (2)

いかがですか?少しは理解の役に立ちそうでしょうか?(これ、自分自身にも問いかけてしまいます。)
実際、何度も確認はしてきたのですが、コーディングをしていて「あれ?なんかmapとかfilterを使うとさらっと書けそうな気がするなあ?」と思いついても、うろ覚えで適当にどちらかを記述していくと思ってもいない結果に遭遇して「あれ?」となり、結局は本やウェブでリファレンスを当たることになってしまいます。(ええっ、そんなことない?私だけ?ぐ…、歳のせいにしておきます。)

つぎは #some と #every あたりを取り上げてみようかと思うのですが、実は個人的に使った記憶がありません。まあ、これから使うこともあるやもしれないのでちゃんと検証してみようと思います。また悩みの種が増えるだけかもしれませんがね。そかさ。