前書き
「コメントではなくコード本体を見て何やってるか理解できるコードを書きたい」
最近いろんな本や記事を読んだりして、当たり前のことですが改めて考える機会が増えてきました。
これを実現するためには、
適切なクラス、関数の切り分け、名前の付け方など手段は沢山あります。
その中で簡単にできる方法の1つとして、
配列をいじる際に適切なメソッドを使うだけでコードリーディングが早くなるような気がしたのでご紹介できればと思っています。
(ここで紹介するコードの動作検証はやっていません。ニュアンスが伝わればいいなと書いてみました。)
配列操作の基本形
まずよく初心者記事で見かける配列の処理です。for文を使った単純なループ。
const fruits = ['apple', 'orange', 'grape'];
for(let i = 0; i < fruits.length; i ++) {
const fruit = fruits[i];
console.log(fruit);
}
apple
orange
grape
基本形のメリット・デメリット
この形にはメリットとデメリットが存在します。
メリット
一般的に、単純なforを使用したループが最速だと言われています。
またほぼ全ての配列操作を自前で実装可能であり、
例えば大規模配列のソートなどのように、アルゴリズムを意識してプログラムすればパフォーマンスを向上させることが可能です。
玄人向けな使い方ができるというお話です。
デメリット
こちらがメインのお話。私が考えているデメリットは2つあります。
1.自前実装が必要
メリットの裏返しになりますが、単純なループしか提供されないため中身で何をするかは全て自分でプログラミングする必要があります。そのため実装の時間がかかってしまいます。
また大抵の場合は自分で実装したプログラムはパフォーマンスが良くなくなる(自戒)ことがあります。
2.ループの中を読まないと処理がわからない
以下のようなコードがあった場合、forでは何の処理をしているでしょうか。
// 100まで数字が入った配列
const foo = [0, 1, 2, 3, 4, ... 100];
const result = [];
for(let i = 0; i < foo.length; i ++) {
const value = foo[i];
if(value % 2 === 0) {
result.push(value * 2);
}
}
console.log(result);
正解は0から100の中から偶数だけを取り出して2倍にした新しい配列を作っています。
このぐらいのfor文だったら簡単に読めます。
このfor文がもっと複雑になっていくとどうでしょう?
コメントを残せば何をやっているかすぐ理解できるかもしれません。この部分だけ関数として切り出して命名しても読み手に優しくなるでしょう。
しかしfor文がでてくる度に「このforは何をやっているんだ...??」という精神にさせられます。
Arrayメソッドとそのメリット
ここから少しずつ本題に。
JavaScriptに限らずですが、最近の言語では配列などの操作に便利な関数が初期装備されています。
例えば冒頭に示した単純なループは
const fruits = ['apple', 'orange', 'grape'];
fruits.forEach(fruit => {
console.log(fruit);
});
apple
orange
grape
と書き換えることができます。
このメソッドを使用した場合、読み手は
「forEachを使っているということは配列全体を1つ1つ順番に読み込んで何か処理してるんだな」
とコードから大まかな処理を推測できます。
つまりコードを簡潔に書けるだけではなく、何をしたかったのかコード自身で読み手に伝えることができ、
「なぜこの考えに至ったのか」の思想をコメントとして残す余地が生まれます。
もちろんパフォーマンスを考慮する場合や、メソッドを組み合わせても行いたい処理ができない・冗長になる場合はfor文を使います。
しかし現在のArrayメソッドはかなり充実しています。基本的にはこのメソッドを活用していくことで、よりリーダブルなコードになっていくだろうと考えています。
良く使うメソッドと使用例
私がよく使うメソッドの一部と、そのメソッドに遭遇したときの気持ちを簡単にご紹介します。
ちなみにメソッドに関してはMDNに全て記載されています。
どのようなことができるのか、一通りみて確認しておくとよいでしょう。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array#instance_methods
forEach
まずは先ほどもご紹介したforEachです。
読んで字のごとく、for(〜のために、〜に対して) Each(それぞれ、各個)ということで、
記述した処理を配列の要素それぞれに対して適用していきます。
従ってforEachに遭遇した際は、
「配列の要素1つずつに対して処理してるんだなあ」 という気持ちになります。
上でも使用例を載せましたが、もう一度簡単な使用例を載せます。
1つずつ処理していく気持ちになってもう一度確認してみましょう。
ちなみに、第二引数にはindexが入ります。
const fruits = ['apple', 'orange', 'grape'];
fruits.forEach((fruit, index) => {
console.log(`${index}: ${fruit}`);
});
0: apple
1: orange
2: grape
全てに対して処理が実行されるため「途中でbreakで処理を中断して抜ける」のようなことはできません。For Eachです。
map
続いてマップです。初めて見たときは素で「...地図?」となりました。
地図ではありません。
「プロジェクションマッピング」などの言葉にも使用されている「マップ」の意味で、
投影・対応づけ・写像などの意味を持ちます。
つまり、こちらでゴニョゴニョしたやつを、あちら側へ写す、ということで
配列を1つずつ読み込んで何か処理をして、その結果で新しい配列を作成します。
イメージは、ループを回すというよりも
A, B, C を
↓ mapでゴニョると
A', B', C' になった!
という感じです。
さて、このような気持ちで例を確認してみましょう。
処理の説明をコメントに書いてみました。
// valueには金額が入っている
const fruits = [
{ name: 'apple', value: 100 },
{ name: 'orange', value: 200 },
{ name: 'grape', value: 300 }
];
// taxIncludedPriceArray: 消費税込みで計算された金額の配列. 中身は[110, 220, 330]になっている。
const taxIncludedPriceArray = fruits.map(fruit => {
const value = fruit.value;
const price = value * 1.1; // 消費税10%
return price
});
// mapによって作られるのは配列のため、結果に対してまたArrayメソッドが使える
taxIncludedPriceArray.forEach(price => {
console.log(price);
});
110
220
330
使用例の処理を少し増やしてみました。
- fruits配列を元にして、
- 値段を抜き出して税込み価格にする処理を行い、
- taxIncludedPriceArrayという新しい配列に反映させています。
これがmapです。
投影・写像ための処理なので、**中の処理では必ずreturnして、新しい配列に反映(代入)**させましょう。
これでmapの文字を見ると
「何か処理した結果で新しい配列つくってるんだな」となるはずです。
filter, find
同時に2つご紹介。
配列を取り扱っていると、ほぼ確で遭遇する
「この条件にマッチする値欲しいんだけど」
の状況に使用できます。
これも読んで字の如くですが、
filterはフィルタリング、値をふるいにかける処理,
findは値を検索する処理です。
これらの文字列を見ると
**「条件にマッチするかどうか調べているんだなあ」**という気持ちになります。
マッチした結果をどうするのか(マッチした値を1つ返すのか、全部返すのか等)の違いでしかありません。
このような気持ちを持ちつつ、次の例を確認しましょう。
const fruits = [
{ name: 'apple', value: 100 },
{ name: 'orange', value: 200 },
{ name: 'grape', value: 300 }
];
const fruitsWithin200yen = fruits.filter(fruit => {
const value = fruit.value;
const isLessThanOrEqualTo200yen = value <= 200 // value <= 200 によって200以下ならtrue, そうでなければfalseとなる
return isLessThanOrEqualTo200yen; // 条件にマッチするかどうかを調べるためtrueかfalseをreturnする
});
const apple = fruitsWithin200yen.find(fruit => {
const isApple = fruit.name === 'apple'; // nameにappleがついていればtrueになる
return isApple; // これも条件にマッチするかどうかが必要なのでtrueかfalseをreturnする
});
console.log(fruitsWithin200yen);
console.log('---') // ただの仕切り
console.log(apple)
[
{ name: 'apple', value: 100 },
{ name: 'orange', value: 200 }
]
---
{ name: 'apple', value: 100 }
まず、fruitsWithin200yen は変数名から察しがつくように、
「200円以内」という条件でfruitsをフィルターにかけて残ったアイテムが格納されています。
また次の変数: apple では、
「200円以内」のフルーツの中で、名前に’apple’がついている物を探して取得してきたアイテムが入っています。
filter, find は私は頻繁に使います。覚えておいて損はないでしょう。
reduce
リデュース...? リサイクル? リユース?
エコの3Rでも出てくるreduceですが、単語には減らすという意味があります。
配列を減らすとはどういうことか。
つまり、「配列の値を全部1つにガッチャンコして要素の数を減らしちゃおう」 ということです。
何かしらの処理を行い、配列の値を1つにまとめるので
例えば配列の値を足し合わせて合計を出すなどの場面で使用できます。
reduceは次のような流れで1つにマージしていきます。
- 配列の値を1つ取得する
- 今まで計算してきた結果(初期値は引数で設定)と、1で取得した値をつなぎ合わせる
- 配列のindexを1つ進める。計算結果を次に持ち越してループ
reduce = 1つずつガッチャンコしていく気持ちで例を確認しましょう。
const fruits = [
{ name: 'apple', value: 100 },
{ name: 'orange', value: 200 },
{ name: 'grape', value: 300 }
];
const sum = fruits.reduce((sumValue, fruit) => { //第一引数(sumValue) は前回の計算結果が入る。
const result = sumValue + fruit.value;
return result; // ここでreturnした値が次ループに持ち越されて、次の第一引数(sumValue)に入る
}, 0) // この数字は初期値。ループ1発目の第一引数(sumValue)にこの値が入る
console.log(sum);
600
使用頻度はそこまで多くないですが、適切な箇所で使用できればかなり便利です。
アンチパターン
Qiitaでは強い言葉を使うと良いと学びました
前項で説明した内容も少しありますが、やらない方がいいと私が考えているパターンをいくつかご紹介。
forEachで全部やっちゃう
序盤でご紹介した偶数を取り出す例をforEachに書き換えてみました。
// 100まで数字が入った配列
const foo = [0, 1, 2, 3, 4, ... 100];
const result = [];
foo.forEach(number => {
if(number % 2 === 0) {
result.push(value * 2);
}
})
console.log(result);
問題点
上記の例の場合ではforEachを使用することで
- 偶数だけを抜き出して、
- 新しい配列を作成する
処理を行っています。
これではただfor文を使うより少しシンプルになっただけです。
今回の目的は 「Arrayメソッドを使うことでリーダブルなコードを目指す」です。
forEachは結局のところ中身の処理を読む必要があるため、
他のメソッドの組み合わせで対応できない場合や、単純な処理をする場合に限定して使うべきだと考えます。
解決案
例えば今回は、他のメソッドを使用して次のように置き換えることができます。
// 100まで数字が入った配列
const foo = [0, 1, 2, 3, 4, ... 100];
const evenNumbers = foo.filter(number => {
return number % 2 === 0;
});
const result = evenNumbers.map(number => {
return number * 2;
});
console.log(result);
さて記述量が若干増えてしまったように感じます。
解決案のメリット
大きく2つあると考えています。
1つ目はそれぞれの計算過程が残っていること。例えば「evenNumbersからさらに5の倍数のみ取り出したい」などの追加要望にも比較的柔軟に対応できます。
デバッグも非常にやりやすいです。
そしてもう1つは中の処理を読む必要がないことです。
処理を読まなくていい
Arrayメソッドに遭遇したときの気持ちも一緒に紹介してきました。
その気持ちを思い出しましょう。
例えば、arrayメソッドの内部での処理を全部見えなくしたとすると、次のようになります。
// 100まで数字が入った配列
const foo = [0, 1, 2, 3, 4, ... 100];
const evenNumbers = foo.filter
const result = evenNumbers.map
console.log(result);
これだけでも何をやっているのかおおよそ理解できます。
foo.filterで何かのフィルターにかけて、evenNumbers(偶数) を取り出し、
偶数に対して何か処理をしてresultにマッピングしているんだな
と理解できるわけです。
このように適切なメソッドと適切な変数名を用いることで、コードを読む心理的障壁を大きく減らすことができます。
メソッド名と違う処理をやっちゃう
時々map, reduceなどを使用して、**「ただループさせるだけ」**の処理が行われているのを目にします。
const fruits = ['apple', 'orange', 'grape'];
fruits.map(fruit => {
console.log(fruit)
});
fruits.reduce((acc, fruit) => {
console.log(fruit);
} ,0)
問題点
確かにmap,reduceなどは配列のループが発生するため、このような処理は可能です。
しかしこれも気持ち、認識と関わってくる話で、
mapメソッドならば対応づけや写像、reduceなら減少と、メソッド名を見てその結果を想像します。
つまりそれ以外の処理がこれらのメソッドで行われていた場合、メソッド名への信頼が一瞬で消えます。
メソッド名から処理の概要を把握しようにも、
毎回「いやでもこれは本当にちゃんと処理されているのか??」と、結局中の処理も注意深く観察しなければならなくなります。
解決案
単純なループを行う場合はforEachを使用する方が適切です。
mapやreduceなどを使用する場合は必ずreturnしてメソッド名通りの処理を行いましょう。
コメントを書こう
さて、いろいろと紹介してきましたが
作業工数の問題であったり、パフォーマンスの問題であったりと様々な要因から
for文を使用したり、アンチパターンでご紹介した例を使う必要が出てきたとします。
その場合どうするのか。コメントを書きましょう。
コメントには「何をやっているのか」ではなく**「なぜこれをやったのか」**を書きます。
// 例1
// 〇〇によってパフォーマンスに影響したためfor文を使用
for(let i = 0; i < foo.length; i ++) {
bar(foo[i]);
}
// 例2
// forEachで処理して変数への代入を減らす
foo.forEach(bar => {
if(baz(bar)) {
fooBar();
}
});
// 例3
// 〇〇のため一時的にループのみ使用。今後マッピングを行う。
foo.map(bar => {
baz();
})
コメントを見れば「意図的にやっているんだな」と理解できるため、メソッド名への信頼はある程度保たれるはずです。
最後に
最近いろいろ考えていることを言語化して整理してみました。
綺麗なコードを目指してチャレンジしてみると、結局のところ変数や関数にどのような名前をつけるのかで悩み時間がかかってしまいます。
「プログラマーは英語を勉強しなければならない」という言葉は、もちろん英語のドキュメントを読むためでもありますが、
プログラミングには英語を使うため英語ができなければ綺麗なコードも書けないという意味でもあると痛感しました。
英語の勉強も並行して行い、綺麗なコードを目指していきたいですね。
アドバイスやご意見などお待ちしています。
最後までご覧いただきありがとうございました。