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と呼ばれるこの記事は、優れた出発点です。