今回は JavaScript のループ処理についてお話していきたいと思います。
使用言語は JavaScript ですが、他の言語にも応用できるお話です。
たかがループ処理と侮ることなかれ、ちょっと意識を変えるだけでコードがグッと読みやすくなりますよ!
要約
- ループを書く場合は「1つのループで1つのことだけを行う」こと。
- 「1つのループで1つのことだけを行う」ループを書ければ、
Array.prototypeのメソッドを使ってシンプルに書ける。
シンプルな for ループ
プログラミング言語で代表的なループ処理といえば for ループでしょう。
JavaScript にも用意されていますね。
サンプルとして、以下のプログラムを for ループで実装してみます。
- 国の名前と言語をコンソールに出力する。
const countries = [
{ code: 'jp', name: '日本', language: '日本語' },
{ code: 'us', name: 'アメリカ', language: '英語' },
{ code: 'gb', name: 'イギリス', language: '英語' },
{ code: 'fr', name: 'フランス', language: 'フランス語' },
// ...
];
for (let { name, language } of countries) {
console.log(`${name}の言語は${language}です。`);
}
ごく単純な処理で、コンソールに出力される内容は以下のようになります。
日本の言語は日本語です。
アメリカの言語は英語です。
イギリスの言語は英語です。
フランスの言語はフランス語です。
では、もう少し複雑なプログラムを実装してみましょう。
少し複雑な for ループ
- 国の名前と言語をコンソールに出力する。
- 特定の国は除外する。
const countries = [
{ code: 'jp', name: '日本', language: '日本語' },
{ code: 'us', name: 'アメリカ', language: '英語' },
{ code: 'gb', name: 'イギリス', language: '英語' },
{ code: 'fr', name: 'フランス', language: 'フランス語' },
// ...
];
const excludedCountryCodes = ['jp', 'gb'];
for (let { code, name, language } of countries) {
// 特定の国は除外する。
if (!excludedCountryCodes.includes(code)) {
// 国の名前と言語をコンソールに出力する。
console.log(`${name}の言語は${language}です。`);
}
}
少し行数が増えましたが、まだシンプルに書けますね。
では、さらに複雑にしていきましょう。
さらに複雑な for ループ
- 国の名前と言語をコンソールに出力する。
- 特定の国は除外する。
- 国番号の上限 / 下限を指定する。
- 同じ言語の国はまとめて出力する。
const countries = [
{ code: 'jp', numericCode: 392, name: '日本', language: '日本語' },
{ code: 'us', numericCode: 840, name: 'アメリカ', language: '英語' },
{ code: 'gb', numericCode: 826, name: 'イギリス', language: '英語' },
{ code: 'fr', numericCode: 250, name: 'フランス', language: 'フランス語' },
// ...
];
const excludedCountryCodes = getExcludedCountryCodes();
const minNumericCode = getMinNumericCode();
const maxNumericCode = getMaxNumericCode();
const languageMap = {};
for (let { code, numericCode, name, language } of countries) {
// 特定の国は除外する。
if (!excludedCountryCodes.includes(code)) {
// 国番号の上限 / 下限を指定する。
if (minNumericCode <= numericCode && maxNumericCode >= numericCode) {
// 同じ言語の国はまとめて出力する。
if (languageMap[language] == null) {
languageMap[language] = [];
}
languageMap[language].push(name);
}
}
}
// 国の名前と言語をコンソールに出力する。
for (let language in languageMap) {
const countryNames = languageMap[language].join('と');
console.log(`${countryNames}の言語は${language}です。`);
}
この通り、実装できました……が、可読性に難ありと言わざるを得ません。
特に if 文のネストが多いのが問題です。(リーダブルコードを読まれている方々はお分かりでしょう。)
このままでは何か条件が増えるたびにネストが深くなってしまいます。
もう少しシンプルに書ける方法はないでしょうか?
for ループを改善する
この for ループを改善するのに、注意するポイントは1つだけです。
- 1つのループで1つのことだけを行う。
const countries = getCountries();
const excludedCountryCodes = getExcludedCountryCodes();
const minNumericCode = getMinNumericCode();
const maxNumericCode = getMaxNumericCode();
// 特定の国は除外する。
const allowedCountries = [];
for (let country of countries) {
if (!excludedCountryCodes.includes(country.code)) {
allowedCountries.push(country);
}
}
// 国番号の上限 / 下限を指定する。
const allowedCountries2 = []
for (let country of allowedCountries) {
if (minNumericCode <= country.numericCode && maxNumericCode >= country.numericCode) {
allowedCountries2.push(country);
}
}
// 同じ言語の国はまとめて出力する。
const languageMap = {};
for (let country of allowedCountries2) {
if (languageMap[country.language] == null) {
languageMap[country.language] = [];
}
languageMap[country.language].push(country);
}
// 国の名前と言語をコンソールに出力する。
for (let language in languageMap) {
const countryNames = languageMap[language].join('と');
console.log(`${countryNames}の言語は${language}です。`);
}
ネストが深く読みづらかったコードが整理されました。
それだけでなく、プログラムの仕様とコードの対応が分かりやすくなっています。
その代わりに allowedCountries, allowedCountries2 といった中間的な変数が増えてしまいました。
どうにかして解決できないのでしょうか?
Array.prototype のメソッドを活用する
一時変数が増えてしまったループ処理を、 Array.prototype のメソッドを使って書き換えてみます。
今回使用するのは以下のメソッドです。
- Array.prototype.forEach() : 配列の各要素に対して処理を実行する。
- Array.prototype.filter() : 条件に合う要素だけを残す。
-
Array.prototype.reduce() : 配列を1つにまとめる。
上記のメソッドを使って書き換えたコードは、以下のようになります。
const countries = getCountries();
const excludedCountryCodes = getExcludedCountryCodes();
const minNumericCode = getMinNumericCode();
const maxNumericCode = getMaxNumericCode();
const languageMap = countries
// 特定の国は除外する。
.filter(country => !excludedCountryCodes.includes(country.code))
// 国番号の上限 / 下限を指定する。
.filter(country => {
return minNumericCode <= country.numericCode
&& maxNumericCode >= country.numericCode;
})
// 同じ言語の国はまとめて出力する。
.reduce(
(map, country) => {
if (map[country.language] == null) {
map[country.language] = [];
}
map[country.language].push(country.name);
return map;
},
{}
);
// 国の名前と言語をコンソールに出力する。
Object.entries(languageMap)
.forEach([language, names] => {
const countryNames = names.join('と');
console.log(`${countryNames}の言語は${language}です。`);
})
先ほど問題になっていた、中間変数を減らすことが出来ました。
「1つのループで1つのことだけを行う」ことで、このような書き換えが容易になります。
特に filter() を使っている箇所では「要素数を減らす」意図が明確になり、読みやすくなったと思います。
一方、 reduce() の処理については賛否両論あるかもしれません。
この部分をシンプルにしたい場合、例えば Object.groupBy() を使うことができます。
なお Object.groupBy() は2024年に追加された機能になります。もしブラウザ互換性に万全を求めるのであれば、ポリフィルを活用すると良いでしょう。
まとめ
読みやすいループを書くためには「1つのループで1つのことだけを行う」ことが大切です。
Array.prototype のメソッドを活用することで、不要な一時変数を減らすこともできます。