このページの本文へ

意外と知らないES5の新機能でJavaScriptのコードをメンテナブルに書き直す方法

2017年02月27日 04時00分更新

文●M. David Green

  • この記事をはてなブックマークに追加
本文印刷
時代はECMAScript 2015(ES6)とはいえ、実はまだES5も追いかけられていない、使いこなせていない…という開発者も意外と多いのでは?  ES5のArrayメソッドを使ってメンテナンスしやすいコードを書く実例を紹介。

JavaScriptのメリットは多様なプログラミングスタイルを選べることです。オブジェクト指向、命令型、関数型のプログラミングもできます。さらに、必要性、好み、チームの意向に応じて、スタイルの切り替えもできます。

JavaScriptは関数型の書き方をサポートしてはいるものの、HaskellやScalaといった言語のように純粋な関数型プログラミングには最適化されていません。JavaScriptのプログラムを100%関数型にはできませんが、関数型プログラミングのコンセプトを使うと、コードがきれいになるだけでなく、簡単に再利用でき、テストしやすくバグを減らしやすいコードデザインに集中できます。

データセットにフィルターをかける

ES5の登場で、JavaScriptの配列は関数型プログラミングに適したメソッドを継承しました。JavaScriptの配列はmapreducefilterをネイティブにサポートします。これらのメソッドは、配列内のすべてのアイテムにアクセスし、ループを使ったりローカルの状態を変更したりせず処理します。その結果をそのまま使用したり、追加の操作の対象にできます。

この記事ではフィルターの使い方を紹介します。フィルターは配列内のすべてのアイテムを評価し、指定した条件に合致するアイテムを含む新たな配列を返します。Arrayクラスのfilterメソッドを使うと、元の配列のアイテムのうち設定した条件に合致するアイテムで構成されるので、戻り値の配列の要素数は元の配列と同じか少なくなります。

ループでフィルターを理解する

フィルターが力を発揮する簡単な事例として、文字列の配列の中から3文字の文字列だけを取り出してみます。これは複雑な問題ではなく、filterメソッドを使わず素のJavaScriptのforループを使うと次のように簡単に実装できます。

var animals = ["cat","dog","fish"];
var threeLetterAnimals = [];
for (let count = 0; count < animals.length; count++){
  if (animals[count].length === 3) {
    threeLetterAnimals.push(animals[count]);
  }
}
console.log(threeLetterAnimals); // ["cat", "dog"]

ここでは、3文字の文字列を含む配列を定義し、3文字の文字列だけを格納するために空の配列を用意しました。配列を繰り返し処理するためにforループで変数countを定義し、文字数が3つの文字列を見つけるたびに、新たに用意した空の配列にpushしました。完了後に結果を出力しています。

ループでは元の配列の変更は制限されていません。配列を変更すると、元の値を完全に失ってしまう危険性があります。新たな配列を作って元の配列に手を付けないほうが、きれいなコードになります。

filterメソッドを使う

技術的には先の方法に何の問題もありませんが、Arrayクラスのfilterメソッドを使うときれいで分かりやすいコードになります。先ほどと同じことをfilterメソッドを使って書くと次のようになります。

var animals = ["cat","dog","fish"];
var threeLetterAnimals = animals.filter(function(animal) {
  return animal.length === 3;
});
console.log(threeLetterAnimals); // ["cat", "dog"]

先ほどと同じく、元の配列を格納する変数を宣言し、次に文字数が3の文字列だけを含む配列用に新たに変数を定義しています。しかし今回は2つ目の配列を定義すると同時に、元の動物の配列にfilterメソッドを適用した結果を直接代入しました。操作対象の要素の数が3であればtrueを返す無名のインライン関数をfilterに渡しています。

filterメソッドは次のように動作します。まず配列内のすべての要素に対して条件を設定した関数を実行し、次にtrueを返した要素を含む配列をfilterメソッドが返します。ほかの要素はスキップされます。

filterメソッドを使うことできれいなコードになりました。事前にfilterについて知らなかったとしても、このコードを読めば意図を読み取れるでしょう。

関数型プログラミングのメリットは、ローカルの状態を保持する量を減らせること、また関数内で外部変数の変更を制限することで、コードがきれいになることです。filterを使う前は、元の配列をforループで処理する間に変数countと配列threeLetterAnimalsの状態が何度も変わっており、その分追跡する情報が多くなりました。filterを使うと、変数countだけでなくforループもなくせます。またforループの場合とは異なり、新たな配列の値を何度も変更することはありません。定義して、元の配列にfilter条件を適用して得られる値を代入するだけです。

filterのいろいろな記述方法

const宣言と無名インラインアロー関数を活用すれば、さらに簡素なコードになります。これはECMAScript 6(ES6)の機能で、多くのブラウザーとJavaScriptエンジンでネイティブにサポートされています。

const animals = ["cat","dog","fish"];
const threeLetterAnimals = animals.filter(item => item.length === 3);
console.log(threeLetterAnimals); // ["cat", "dog"]

既存のコード群に合わせる必要がある場合を除くと、多くの場合、古い書き方の変更は好ましいことです。しかし、対象を選択すべきです。短くなるほど、コードの各行が複雑になるためです。

JavaScriptの楽しみの1つは、同じコードでも多くの手法を駆使し、チームの意向に合わせてサイズ、効率、分かりやすさ、メンテナンスのしやすさを最適化できることです。その一方、チームで共通のスタイルガイドを作成しその賛否を議論する負担を生みます。

さらに読みやすく柔軟に変更できるコードにするには、無名インラインアロー関数をやめて通常の名前付き関数にし、filterメソッドにその関数を渡すようにすると良いでしょう。そのコードを次に示します。

const animals = ["cat","dog","fish"];
function exactlyThree(word) {
  return word.length === 3;
}
const threeLetterAnimals = animals.filter(exactlyThree);
console.log(threeLetterAnimals); // ["cat", "dog"]

ここでは、無名インラインアロー関数を別の名前付きの関数に替えました。見て分かるように、配列の要素に適した型を渡すと同じ型を返す純粋関数を定義しています。その関数名を直接filterメソッドに条件として渡します。

MapとReduceの紹介

filtermapreduceというES5の2つの関数型Arrayメソッドとセットで使えます。さらにJavaScriptのメソッドチェーンを使うと、この組み合わせでかなり複雑な操作をとてもきれいなコードで書けます。

簡単に振り返っておくと、mapメソッドは配列のすべての要素にアクセスして、関数に従って変更し、要素の数が同じで変更された値を持つ新たな配列を返します。

const animals = ["cat","dog","fish"];
const lengths = animals.map(getLength);
function getLength(word) {
  return word.length;
}
console.log(lengths); //[3, 3, 4]

reduceメソッドは配列の要素に順番にアクセスし、指定の操作をして、その結果をアキュムレータに蓄積します。すべての要素にアクセスし終えると、最終結果を返します。下の場合、第2引数を使ってアキュムレータの初期値を0に設定しています。

const animals = ["cat","dog","fish"];
const total = animals.reduce(addLength, 0);
function addLength(sum, word) {
  return sum + word.length;
}
console.log(total); //10

関数型プログラミングの慣習どおり、この3つのメソッドはどれも元の配列を変更しません。mapreduceの使い方の復習は、過去の記事『JavaScriptプログラマーならMap()とReduce()で関数型プログラミングを始めてみない?』を読んでください。

Map、Reduce、Filterのチェーン

簡単な使用法の事例として、文字列の配列の中から3文字からなる文字列だけを取り出し、StudlyCaps形式にフォーマットした単一文字列を返すことを考えます。mapreducefilterを使わないのなら、次のように書けるでしょう。

const animals = ["cat","dog","fish"];
let threeLetterAnimalsArray = [];
let threeLetterAnimals;
let item;
for (let count = 0; count < animals.length; count++){
  item = animals[count];
  if (item.length === 3) {
    item = item.charAt(0).toUpperCase() + item.slice(1);
    threeLetterAnimalsArray.push(item);
  }
}
threeLetterAnimals = threeLetterAnimalsArray.join("");
console.log(threeLetterAnimals); // "CatDog"

もちろんこれでも動きますが、必要以上に変数を使っていて、異なるループ中で値が変化する配列の状態を維持しているので、改善の余地があります。

また変数宣言の背後にあるロジックについて付け加えておくと、空の配列を宣言するときに技術的にはconstとして宣言できますがletを使うようにしています。letを使うことで、配列の中身が変更されることが分かりやすくなるためです。チームによってはこのような場合にconstを選択するでしょう。これは良いディスカッションの題材です。

文字列を渡して文字列を返す純粋関数を作り、mapreducefilterをメソッドチェーンで使い、次のように結果を次から次へ伝えてきます。

const animals = ["cat","dog","fish"];
function studlyCaps(words, word) {
  return words + word;
}
function exactlyThree(word) {
  return (word.length === 3);
}
function capitalize(word) {
  return word.charAt(0).toUpperCase() + word.slice(1);
}
const threeLetterAnimals = animals
  .filter(exactlyThree)
  .map(capitalize)
  .reduce(studlyCaps);
console.log(threeLetterAnimals); // "CatDog"

この例では3つの純粋関数studlyCapsexactlyThreecapitalizeを定義し、直接mapreducefilterに渡して、一連のメソッドチェーンにしています。まず元の配列をexactlyThreefilterし、次にその結果をcapitalizemapし、最後にその結果をstudlyCapsreduceしました。そしてメソッドチェーンの最終結果を新たな変数threeLetterAnimalsに直接代入しています。この中にループや途中の状態はなく、元の配列に変更は加えていません。

最後にはきれいでテストしやすいコードができました。純粋関数を使っているので、転用や要件変更に応じた修正が簡単です。

フィルターとパフォーマンス

ブラウザーやJavaScriptエンジンがまだ新しいArrayメソッドに最適化されていないため、filterメソッドはforループよりわずかに遅いことは知っておいてください(jsPerf)。

たとえ実行速度が多少遅くとも、説明してきたとおり、ループではなく、きれいなコードを書ける関数型Arrayメソッドを使うことをおすすめします。きれいでメンテナンスしやすいことを最重視してコードを書き、実際の使用環境でパフォーマンス改善が必要になったときにだけ最適化するのが良いでしょう。典型的なWebアプリケーションにおいて、filterのパフォーマンスが大きなボトルネックになるとは考えにくいためですが、確認する方法は動かしてみるしかありません。

filterforループよりわずかに遅いとはいえ、実稼働時にその差に気づくことはほとんどありません。もし実際に応答が遅くユーザーに負の影響が出れば、どこをどう最適化すれば良いか明白でしょう。そしてJavaScriptエンジンがこれらの新しいメソッドに最適化されればパフォーマンスは改善します。

恐れずにさっそくfilterを使い始めてください。これはES5でネイティブに実装されている機能で、ほぼすべての環境でサポートされています。きれいでメンテナンスしやすいコードになります。filterメソッドを使えば、評価対象の配列の状態を変更する心配がなくなります。毎回新しい配列を返すので、元の配列は変更されません。

※本記事はDan PrinceVildan SofticJoan Yinnが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Filtering and Chaining in Functional JavaScript

[翻訳:内藤夏樹/編集:Livit

Web Professionalトップへ

WebProfessional 新着記事