JavaScriptでmap()、filter()、reduce()を使用する方法
序章
JavaScriptの関数型プログラミングは、コードの可読性、保守性、およびテスト性に役立ちます。 関数型プログラミングの考え方のツールの1つは、配列処理スタイルでのプログラミングです。 これには、基本的なデータ構造として配列を使用する必要があります。 次に、プログラムは配列内の要素に対する一連の操作になります。
AJAX結果をマップでReactコンポーネントにマッピングする、filterで無関係なデータを削除する、reduceを使用するなど、これが役立つコンテキストはたくさんあります。 「ArrayExtras」と呼ばれるこれらの関数は、forループを抽象化したものです。 これらの機能では、forでは実現できないことは何もできません。その逆も同様です。
このチュートリアルでは、filter、map、およびreduceを見て、JavaScriptの関数型プログラミングをより深く理解します。
前提条件
このチュートリアルを完了するには、次のものが必要です。
- JavaScriptの実用的な理解。 詳細については、JavaScriptでコーディングする方法シリーズを確認してください。
- JavaScriptで
forループを構築および実装する方法の理解。 JavaScriptのforループに関するこのの記事は、始めるのに最適な場所です。 - Node.jsはローカルにインストールされます。これは、Node.jsのインストール方法とローカル開発環境の作成に従って実行できます。
ステップ1—forEachで反復
forループは、配列内のすべてのアイテムを反復処理するために使用されます。 通常、途中で各アイテムに何かが行われます。
1つの例は、配列内のすべての文字列を大文字にすることです。
const strings = ['arielle', 'are', 'you', 'there'];
const capitalizedStrings = [];
for (let i = 0; i < strings.length; i += 1) {
const string = strings[i];
capitalizedStrings.push(string.toUpperCase());
}
console.log(capitalizedStrings);
このスニペットでは、stringsと呼ばれる小文字のフレーズの配列から始めます。 次に、capitalizedStringsという空の配列が初期化されます。 capitalizedStrings配列は、大文字の文字列を格納します。
forループ内では、すべての反復の次の文字列が大文字になり、capitalizedStringsにプッシュされます。 ループの最後で、capitalizedStringsには、strings内のすべての単語の大文字バージョンが含まれています。
forEach関数を使用すると、このコードをより簡潔にすることができます。 これは、リストを「自動的に」ループする配列メソッドです。 つまり、カウンターの初期化とインクリメントの詳細を処理します。
stringsに手動でインデックスを付ける上記の代わりに、forEachを呼び出して、反復ごとに次の文字列を受け取ることができます。 更新されたバージョンは次のようになります。
const strings = ['arielle', 'are', 'you', 'there'];
const capitalizedStrings = [];
strings.forEach(function (string) {
capitalizedStrings.push(string.toUpperCase());
})
console.log(capitalizedStrings);
これは初期関数に非常に近いです。 ただし、iカウンターが不要になり、コードが読みやすくなります。
これはまたあなたが何度も何度も見るであろう主要なパターンを紹介します。 つまり、Array.prototypeで、カウンターの初期化やインクリメントなどの詳細を抽象化するメソッドを使用するのが最適です。 そうすれば、重要なロジックに集中できます。 この記事で説明する配列メソッドは他にもいくつかあります。 今後は、暗号化と復号化を使用して、これらの方法で何ができるかを完全に示します。
ステップ2—シーザー暗号と暗号化および復号化を理解する
以下のスニペットでは、配列メソッドmap、reduce、およびfilterを使用して文字列を暗号化および復号化します。
最初に暗号化とは何かを理解することが重要です。 'this is my super-secret message'のような通常のメッセージを友人に送信し、他の誰かがそれを手にした場合、意図された受信者でなくても、その人はすぐにメッセージを読むことができます。 パスワードなど、誰かが聞いている可能性のある機密情報を送信する場合、これは悪いことです。
文字列を暗号化するということは、「文字列をスクランブリングして、スクランブリングを解除せずに読みにくくする」ことを意味します。 このように、誰かが聞いていて、彼らがあなたのメッセージを傍受したとしても、彼らがそれを解読するまで、それは読めないままになります。
暗号化にはさまざまな方法があり、シーザー暗号はこのような文字列をスクランブルする1つの方法です。 この暗号は、コードで使用できます。 caesarという定数変数を作成します。 コード内の文字列を暗号化するには、次の関数を使用します。
var caesarShift = function (str, amount) {
if (amount < 0) {
return caesarShift(str, amount + 26);
}
var output = "";
for (var i = 0; i < str.length; i++) {
var c = str[i];
if (c.match(/[a-z]/i)) {
var code = str.charCodeAt(i);
if (code >= 65 && code <= 90) {
c = String.fromCharCode(((code - 65 + amount) % 26) + 65);
}
else if (code >= 97 && code <= 122) {
c = String.fromCharCode(((code - 97 + amount) % 26) + 97);
}
}
output += c;
}
return output;
};
このGitHubの要点には、EvanHahnによって作成されたこのシーザー暗号関数の元のコードが含まれています。
シーザー暗号で暗号化するには、1〜25のキーnを選択し、元の文字列のすべての文字をアルファベットのさらに下の1つのn文字に置き換える必要があります。 したがって、キー2を選択すると、aはcになります。 bはdになります。 cはeになります。 等
このような文字に置き換えると、元の文字列が判読できなくなります。 文字列は文字をずらしてスクランブルをかけているので、元に戻すとスクランブルを解除できます。 キー2で暗号化されていることがわかっているメッセージを受け取った場合、復号化するために必要なのは、文字を2スペース戻すことだけです。 したがって、cはaになります。 dはbになります。 等 シーザー暗号の動作を確認するには、caesarShift関数を呼び出し、最初の引数として文字列'this is my super-secret message.'を渡し、2番目の引数として数値2を渡します。
const encryptedString = caesarShift('this is my super-secret message.', 2);
このコードでは、メッセージは各文字を2文字前方にシフトすることによってスクランブルされます。aはcになります。 sはuになります。 等 結果を確認するには、console.logを使用してencryptedStringをコンソールに出力します。
const encryptedString = caesarShift('this is my super-secret message.', 2);
console.log(encryptedString);
上記の例を引用すると、メッセージ'this is my super-secret message'はスクランブルされたメッセージ'vjku ku oa uwrgt-ugetgv oguucig.'になります。
残念ながら、この形式の暗号化は簡単に破られます。 シーザー暗号で暗号化された文字列を復号化する1つの方法は、可能なすべてのキーを使用して文字列を復号化することです。 結果の1つは正しいでしょう。
今後のコード例のいくつかでは、暗号化されたメッセージを復号化する必要があります。 このtryAll関数は、次のことを行うために使用できます。
const tryAll = function (encryptedString) {
const decryptionAttempts = []
while (decryptionAttempts.length < 26) {
const decryptionKey = -decryptionAttempts.length;
const decryptionAttempt = caesarShift(encryptedString, decryptionKey);
decryptionAttempts.push(decryptionAttempt)
}
return decryptionAttempts;
};
上記の関数は暗号化された文字列を受け取り、可能なすべての復号化の配列を返します。 それらの結果の1つは、必要な文字列になります。 したがって、これは常に暗号を破ります。
26の可能な復号化の配列をスキャンすることは困難です。 間違いなく間違っているものを排除することは可能です。 この関数isEnglishを使用して、これを行うことができます。
'use strict'
const fs = require('fs')
const _getFrequencyList = () => {
const frequencyList = fs.readFileSync(`${__dirname}/eng_10k.txt`).toString().split('\n').slice(1000)
const dict = {};
frequencyList.forEach(word => {
if (!word.match(/[aeuoi]/gi)) {
return;
}
dict[word] = word;
})
return dict;
}
const isEnglish = string => {
const threshold = 3;
if (string.split(/\s/).length < 6) {
return true;
} else {
let count = 0;
const frequencyList = _getFrequencyList();
string.split(/\s/).forEach(function (string) {
const adjusted = string.toLowerCase().replace(/\./g, '')
if (frequencyList[adjusted]) {
count += 1;
}
})
return count > threshold;
}
}
このGitHubの要点には、PelekeSengstackeによって作成されたtryAllとisEnglishの両方の元のコードが含まれています。
必ずこの英語で最も一般的な1,000語のリストをeng_10k.txtとして保存してください。
これらの関数をすべて同じJavaScriptファイルに含めることも、各関数をモジュールとしてインポートすることもできます。
isEnglish関数は文字列を読み取り、その文字列に英語で最も一般的な1,000語がいくつ出現するかをカウントし、文中に3つ以上の単語が見つかった場合、その文字列を英語として分類します。 文字列にその配列からの単語が3つ未満含まれている場合、文字列は破棄されます。
filterのセクションでは、isEnglish関数を使用します。
これらの関数を使用して、配列メソッドmap、filter、およびreduceがどのように機能するかを示します。 mapメソッドについては、次のステップで説明します。
ステップ3—mapを使用して配列を変換する
forループをリファクタリングしてforEachを使用することは、このスタイルの利点を示唆しています。 しかし、まだ改善の余地があります。 前の例では、capitalizedStrings配列がforEachへのコールバック内で更新されています。 これには本質的に何も問題はありません。 ただし、可能な限り、このような副作用を回避することをお勧めします。 別のスコープにあるデータ構造を更新する必要がない場合は、更新を避けるのが最善です。
この特定のケースでは、stringsのすべての文字列を大文字のバージョンに変換したいとしました。 これは、forループの非常に一般的な使用例です。配列内のすべてを取得し、それを別のものに変換して、結果を新しい配列に収集します。
配列内のすべての要素を新しい要素に変換し、結果を収集することをマッピングと呼びます。 JavaScriptには、mapと呼ばれるこのユースケース用の組み込み関数があります。 forEachメソッドが使用されるのは、反復変数iを管理する必要性を抽象化するためです。 これは、本当に重要なロジックに集中できることを意味します。 同様に、mapは、空の配列の初期化とプッシュを抽象化するために使用されます。 forEachが各文字列値で何かを行うコールバックを受け入れるのと同じように、mapは各文字列値で何かを行うコールバックを受け入れます。
最後の説明の前に、簡単なデモを見てみましょう。 次の例では、暗号化関数が使用されます。 forループまたはforEachを使用できます。 ただし、この場合はmapを使用するのが最適です。
map関数の使用方法を示すために、2つの定数変数を作成します。1つは値が12のkeyと呼ばれ、配列はmessagesと呼ばれます。
const key = 12;
const messages = [
'arielle, are you there?',
'the ghost has killed the shell',
'the liziorati attack at dawn'
]
次に、encryptedMessagesという定数を作成します。 messagesでmap機能を使用します。
const encryptedMessages = messages.map()
map内で、パラメーターstringを持つ関数を作成します。
const encryptedMessages = messages.map(function (string) {
})
この関数内に、暗号関数caesarShiftを返すreturnステートメントを作成します。 caesarShift関数には、引数としてstringとkeyが必要です。
const encryptedMessages = messages.map(function (string) {
return caesarShift(string, key);
})
encryptedMessagesをコンソールに出力して、結果を確認します。
const encryptedMessages = messages.map(function (string) {
return caesarShift(string, key);
})
console.log(encryptedMessages);
ここで何が起こったかに注意してください。 mapメソッドは、messagesで使用され、caesar関数で各文字列を暗号化し、結果を新しい配列に自動的に格納します。
上記のコードを実行すると、encryptedMessagesは['mduqxxq, mdq kag ftqdq?', 'ftq staef tme wuxxqp ftq etqxx', 'ftq xuluadmfu mffmow mf pmiz']のようになります。 これは、手動で配列にプッシュするよりもはるかに高いレベルの抽象化です。
矢印関数を使用してencryptedMessagesをリファクタリングし、コードをより簡潔にすることができます。
const encryptedMessages = messages.map(string => caesarShift(string, key));
mapがどのように機能するかを完全に理解したので、filter配列メソッドを使用できます。
ステップ4—filterを使用して配列から値を選択する
もう1つの一般的なパターンは、forループを使用して配列内のアイテムを処理しますが、配列アイテムの一部のみをプッシュ/保持します。 通常、ifステートメントは、保持するアイテムと破棄するアイテムを決定するために使用されます。
生のJavaScriptでは、これは次のようになります。
const encryptedMessage = 'mduqxxq, mdq kag ftqdq?';
const possibilities = tryAll(encryptedMessage);
const likelyPossibilities = [];
possibilities.forEach(function (decryptionAttempt) {
if (isEnglish(decryptionAttempt)) {
likelyPossibilities.push(decryptionAttempt);
}
})
tryAll関数は、encryptedMessageを復号化するために使用されます。 つまり、26の可能性の配列になってしまうことを意味します。
ほとんどの復号化の試行は読み取り可能ではないため、forEachループを使用して、isEnglish関数を使用して復号化された各文字列が英語であるかどうかを確認します。 英語の文字列は、likelyPossibilitiesという配列にプッシュされます。
これは一般的な使用例です。 そのため、filterという組み込みの機能があります。 mapと同様に、filterにはコールバックが与えられ、コールバックも各文字列を取得します。 違いは、filterは、コールバックがtrueを返した場合にのみ、アイテムを配列に保存することです。
上記のコードスニペットをリファクタリングして、forEachの代わりにfilterを使用できます。 likelyPossibilities変数は空の配列ではなくなります。 代わりに、possibilities配列と同じに設定してください。 possibilitiesでfilterメソッドを呼び出します。
const likelyPossibilities = possibilities.filter()
filter内で、stringというパラメーターを受け取る関数を作成します。
const likelyPossibilities = possibilities.filter(function (string) {
})
この関数内で、returnステートメントを使用して、stringを引数として渡したisEnglishの結果を返します。
const likelyPossibilities = possibilities.filter(function (string) {
return isEnglish(string);
})
isEnglish(string)がtrueを返す場合、filterはstringを新しいlikelyPossibilities配列に保存します。
このコールバックはisEnglishを呼び出すため、このコードをさらにリファクタリングして、より簡潔にすることができます。
const likelyPossibilities = possibilities.filter(isEnglish);
reduceメソッドは、知っておくことが非常に重要なもう1つの抽象化です。
ステップ5—reduceを使用して配列を単一の値に変換する
配列を反復処理してその要素を単一の結果に収集することは、非常に一般的な使用例です。
この良い例は、forループを使用して数値の配列を反復処理し、すべての数値を合計することです。
const prices = [12, 19, 7, 209];
let totalPrice = 0;
for (let i = 0; i < prices.length; i += 1) {
totalPrice += prices[i];
}
console.log(`Your total is ${totalPrice}.`);
pricesの番号がループされ、各番号がtotalPriceに追加されます。 reduceメソッドは、このユースケースの抽象化です。
上記のループはreduceでリファクタリングできます。 totalPrice変数はもう必要ありません。 pricesでreduceメソッドを呼び出します。
const prices = [12, 19, 7, 209]; prices.reduce()
reduceメソッドはコールバック関数を保持します。 mapやfilterとは異なり、reduceに渡されるコールバックは、合計累積価格と、合計に追加される配列内の次の価格の2つの引数を受け入れます。 これは、それぞれtotalPriceとnextPriceになります。
prices.reduce(function (totalPrice, nextPrice) {
})
これをさらに細かく分類すると、totalPriceは最初の例のtotalのようになります。 これまでに受け取ったすべての価格を合計した後の合計価格です。
前の例と比較すると、nextPriceはprices[i]に対応します。 mapとreduceは自動的に配列にインデックスを付け、この値をコールバックに自動的に渡すことを思い出してください。 reduceメソッドは同じことを行いますが、その値を2番目の引数としてコールバックに渡します。
totalPriceとnextPriceをコンソールに出力する2つのconsole.logステートメントを関数内に含めます。
prices.reduce(function (totalPrice, nextPrice) {
console.log(`Total price so far: ${totalPrice}`)
console.log(`Next price to add: ${nextPrice}`)
})
totalPriceを更新して、新しいnextPriceを追加する必要があります。
prices.reduce(function (totalPrice, nextPrice) {
console.log(`Total price so far: ${totalPrice}`)
console.log(`Next price to add: ${nextPrice}`)
totalPrice += nextPrice
})
mapおよびreduceの場合と同様に、各反復で値を返す必要があります。 この場合、その値はtotalPriceです。 したがって、totalPriceのreturnステートメントを作成します。
prices.reduce(function (totalPrice, nextPrice) {
console.log(`Total price so far: ${totalPrice}`)
console.log(`Next price to add: ${nextPrice}`)
totalPrice += nextPrice
return totalPrice
})
reduceメソッドは2つの引数を取ります。 1つ目は、すでに作成されているコールバック関数です。 2番目の引数は、totalPriceの開始値として機能する数値です。 これは、前の例のconst total = 0に対応します。
prices.reduce(function (totalPrice, nextPrice) {
console.log(`Total price so far: ${totalPrice}`)
console.log(`Next price to add: ${nextPrice}`)
totalPrice += nextPrice
return totalPrice
}, 0)
これまで見てきたように、reduceを使用して、数値の配列を合計に集めることができます。 しかし、reduceは用途が広いです。 数値だけでなく、配列を単一の結果に変換するために使用できます。
たとえば、reduceを使用して文字列を作成できます。 これが実際に動作することを確認するには、最初に文字列の配列を作成します。 以下の例では、coursesと呼ばれる一連のコンピュータサイエンスコースを使用しています。
const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];
curriculumという定数変数を作成します。 coursesでreduceメソッドを呼び出します。 コールバック関数には、courseListとcourseの2つの引数が必要です。
const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];
const curriculum = courses.reduce(function (courseList, course) {
});
courseListを更新して、新しいcourseを追加する必要があります。
const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];
const curriculum = courses.reduce(function (courseList, course) {
return courseList += `\n\t${course}`;
});
\n\tは、各courseの前にインデント用の改行とタブを作成します。
reduce(コールバック関数)の最初の引数が完了しました。 数値ではなく文字列が作成されているため、2番目の引数も文字列になります。
以下の例では、reduceの2番目の引数として'The Computer Science curriculum consists of:'を使用しています。 console.logステートメントを追加して、curriculumをコンソールに出力します。
const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];
const curriculum = courses.reduce(function (courseList, course) {
return courseList += `\n\t${course}`;
}, 'The Computer Science curriculum consists of:');
console.log(curriculum);
これにより、出力が生成されます。
OutputThe Computer Science curriculum consists of:
Introduction to Programming
Algorithms & Data Structures
Discrete Math
前述のように、reduceは用途が広いです。 配列を任意の種類の単一の結果に変換するために使用できます。 その単一の結果は配列でさえありえます。
文字列の配列を作成します。
const names = ['arielle', 'jung', 'scheherazade'];
titleCase関数は、文字列の最初の文字を大文字にします。
const names = ['arielle', 'jung', 'scheherazade'];
const titleCase = function (name) {
const first = name[0];
const capitalizedFirst = first.toUpperCase();
const rest = name.slice(1);
const letters = [capitalizedFirst].concat(rest);
return letters.join('');
}
titleCaseは、0インデックスで文字列の最初の文字を取得し、その文字にtoUpperCaseを使用して文字列の残りの部分を取得することにより、文字列の最初の文字を大文字にします。すべてを一緒に結合します。
titleCaseを配置した状態で、titleCasedという定数変数を作成します。 namesと等しく設定し、namesでreduceメソッドを呼び出します。
const names = ['arielle', 'jung', 'scheherazade'];
const titleCase = function (name) {
const first = name[0];
const capitalizedFirst = first.toUpperCase();
const rest = name.slice(1);
const letters = [capitalizedFirst].concat(rest);
return letters.join('');
}
const titleCased = names.reduce()
reduceメソッドには、titleCasedNamesとnameを引数として取るコールバック関数があります。
const names = ['arielle', 'jung', 'scheherazade'];
const titleCase = function (name) {
const first = name[0];
const capitalizedFirst = first.toUpperCase();
const rest = name.slice(1);
const letters = [capitalizedFirst].concat(rest);
return letters.join('');
}
const titleCased = names.reduce(function (titleCasedNames, name) {
})
コールバック関数内で、titleCasedNameという定数変数を作成します。 titleCase関数を呼び出し、引数としてnameを渡します。
const names = ['arielle', 'jung', 'scheherazade'];
const titleCase = function (name) {
const first = name[0];
const capitalizedFirst = first.toUpperCase();
const rest = name.slice(1);
const letters = [capitalizedFirst].concat(rest);
return letters.join('');
}
const titleCased = names.reduce(function (titleCasedNames, name) {
const titleCasedName = titleCase(name);
})
これにより、namesの各名前が大文字になります。 コールバック関数titleCasedNamesの最初のコールバック引数は配列になります。 titleCasedName(nameの大文字バージョン)をこの配列にプッシュし、titleCaseNamesを返します。
const names = ['arielle', 'jung', 'scheherazade'];
const titleCase = function (name) {
const first = name[0];
const capitalizedFirst = first.toUpperCase();
const rest = name.slice(1);
const letters = [capitalizedFirst].concat(rest);
return letters.join('');
}
const titleCased = names.reduce(function (titleCasedNames, name) {
const titleCasedName = titleCase(name);
titleCasedNames.push(titleCasedName);
return titleCasedNames;
})
reduceメソッドには2つの引数が必要です。 1つ目は、コールバック関数が完了したことです。 このメソッドは新しい配列を作成しているため、初期値は空の配列になります。 また、console.logを含めて、最終結果を画面に出力します。
const names = ['arielle', 'jung', 'scheherazade'];
const titleCase = function (name) {
const first = name[0];
const capitalizedFirst = first.toUpperCase();
const rest = name.slice(1);
const letters = [capitalizedFirst].concat(rest);
return letters.join('');
}
const titleCased = names.reduce(function (titleCasedNames, name) {
const titleCasedName = titleCase(name);
titleCasedNames.push(titleCasedName);
return titleCasedNames;
}, [])
console.log(titleCased);
コードを実行すると、次の大文字の名前の配列が生成されます。
Output["Arielle", "Jung", "Scheherazade"]
reduceを使用して、小文字の名前の配列をタイトル大文字の名前の配列に変換しました。
前の例は、reduceを使用して数値のリストを単一の合計に変換でき、文字列のリストを単一の文字列に変換できることを証明しています。 ここでは、reduceを使用して、小文字の名前の配列を大文字の名前の単一の配列に変換しました。 大文字の名前の単一のリストは依然として単一の結果であるため、これは依然として有効なユースケースです。 プリミティブ型ではなく、たまたまコレクションです。
結論
このチュートリアルでは、map、filter、およびreduceを使用してより読みやすいコードを作成する方法を学習しました。 forループの使用に問題はありません。 しかし、これらの機能を使用して抽象化のレベルを上げると、当然のことながら、読みやすさと保守性にすぐにメリットがあります。
ここから、flattenやflatMapなどの他の配列メソッドの調査を開始できます。 flat()およびflatMap()を使用したVanillaJavaScriptのFlatten Arraysと呼ばれるこの記事は、優れた出発点です。