こんにちは、ほそ道です。
ここまでシリーズでconst
で宣言した変数(変数より値の名前といったほうが個人的にはしっくり来ますが)の状態管理方法についてやってきました。
今回は反復操作を行うにあたってやむを得ずlet
を使うことになってしまうようなケースに対して、あくまでconst
宣言した値に一発代入するようなレシピを作っていきたいと思います。
※オブジェクトや配列、Map
やSet
に関してはconst宣言したものであっても子要素の追加/変更が行えますが今回の趣旨として一発変換・一発代入でありますので不変性の担保については過去編参照でおねがいします。
基本編・Array
Array.filter
まずはこんなケースからやってみます。
配列の中から偶数だけを取り出して新しい配列を生成する処理です.
下記のbeforeケースでは反復と条件分岐の結果を配列に詰めて返すために
反復インデックスiと返却配列evensをlet
で宣言しております。
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let i = 0, evens = [];
for (; i < nums.length; i++) {
if (nums[i] % 2 === 0) {
evens.push(nums[i]);
}
}
console.log(evens); // [ 2, 4, 6, 8, 10 ]
これをArrayのfilter
を使って下記のようにconst
を使って一発で偶数配列を返します。
filter
は引数に、「配列要素を一つ引数に取る関数」を受け取りtrueを返した場合の配列要素のみで新しい配列を作って返します。
const evens = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter(num => num % 2 === 0);
console.log(evens); // [ 2, 4, 6, 8, 10 ]
こんな感じで幾つかのケースの一発代入レシピを作っていきたいと思います。
Array.map
よく使うものとしてmapメソッドも挙げておきます。
今回はfilter
と、このmap
を中心に据えていこうかなと思います。
const array = [1, 2, 3, 4, 5].map(elm => elm * 100);
console.log(array); // [ 100, 200, 300, 400, 500 ]
filter
は配列要素の絞込でしたが、こちらは各要素に対する変換となります。
応用的な考え方
組み合わせ
map
もfilter
も新しい配列を作って返すので、メソッドチェーンでつないで関数を生成することも可能です。
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter(num => num % 2 === 0).map(num => num * 100);
console.log(array); // [ 200, 400, 600, 800, 1000 ]
下記のようにして、値の検索を行うこともできます。
const has5 = [1, 3, 5, 6, 9].filter(num => num === 5).length > 0
console.log(has5); // true
関数化
ちょっとだけ目線を変えて、filter
やmap
を使って配列を返す関数を作ります。
よりシンプルな関数にすることができると思います。
const getEvens = array => array.filter(num => num % 2 === 0);
console.log(getEvens([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); // [ 2, 4, 6, 8, 10 ]
値の検索処理も関数化することで汎化できます。
const has = (array, value) => array.filter(num => num === value).length > 0
console.log(has([1, 3, 5, 6, 9], 5)); // true
console.log(has([1, 3, 5, 6, 9], 2)); // false
Map
ES6で追加されたMap
もコレクションとして見ておきます。
Map
はfilter
やmap
メソッドを持っていないということが要注意です。
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
console.log(map.filter); // undefined
console.log(map.map); // undefined
ただし、Map
はforEach
メソッドなら持っていますので下記のように書くことはできます。
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
const map2 = new Map();
map.forEach((v, k) => map2.set(k, v * 100));
console.log(map2); // Map { 'a' => 100, 'b' => 200, 'c' => 300 }
上記のmap2に関してはconst
で宣言はしているものの、あとから要素を追加しているので不変な扱い方はできていないところが微妙に感じるところです。
なので、その微妙さは下記のように解決してみます。
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
const map2 = new Map(Array.from(map).map(kv => [kv[0], kv[1] * 100]));
console.log(map2); // Map { 'a' => 100, 'b' => 200, 'c' => 300 }
Array.from
をMapにカマすと[[key, value], [key, value]]
な形式の配列にしてくれるのでmap
やらfilter
やらを好きに使える、と言った感じです。
この形式の配列はそのままMap
のコンストラクタ引数になるので、変換してnew Map
にくわせてやれば良いという感じです。
オブジェクトリテラル生成オブジェクト
オブジェクトリテラルで作ったオブジェクトはMapと同じように変換していきたいと思います。
こちらはlodashが提供してくれるmapValues
関数一発で対応します。
const _ = require('lodash');
const obj = {a: 1, b: 2, c: 3};
const obj2 = _.mapValues(obj, v => v * 100);
console.log(obj2); // { a: 100, b: 200, c: 300 }
プロパティ名の方を変換したい場合はmapKeys
を使って変換します。
const _ = require('lodash');
const obj = {a: 1, b: 2, c: 3};
const obj2 = _.mapKeys(obj, (v, k) => k + k + k);
console.log(obj2); // { aaa: 1, bbb: 2, ccc: 3 }
immutable.js
前回も触れましたが、immutable.jsを使うことでconst定義したオブジェクトを不変なものとして扱えます。
基本的に配列はimmutable.List
、Map
はimmutable.Map
、オブジェクトはimmutable.Record
という読み替えで扱っていけば良いと思います。
で、今回の一発変換・一発代入レシピとしてはimmutable.jsが「ならではのもの」として持っている便利なメソッドにフォーカスを当てたいと思います。
immutable.Map.map
まず、先ほどMap
のところでおこなった変換を、そのまんまimmutable.Map
に当てはめてみます。
const I = require('immutable');
const map = I.Map([['a', 1], ['b', 2], ['c', 3]]);
const map2 = I.Map(Array.from(map).map(kv => [kv[0], kv[1] * 100]));
console.log(map2); // Map { "a": 100, "b": 200, "c": 300 }
これがものの見事に動きます。すごいなー。
で、もうちょっとシンプルにならないか、ということでimmutable.Map
はmap
メソッドなどの処理を持っていてくれてますので、そちらを使ってみます。
const I = require('immutable');
const map = I.Map([['a', 1], ['b', 2], ['c', 3]]);
const map2 = map.map(v => v * 100);
console.log(map2); // Map { "a": 100, "b": 200, "c": 300 }
シンプルな感じにすることができました!
引数の関数は第二引数でkeyを受け取ることもできますが最終的にreturnする値はimmutable.Map
のvalueになるという感じです。
immutable.List.flatMap
例えば学校の時間割を曜日とn時間目で表現します。
[["月", 1], ["月", 2]...["金", 5]]
のような感じにしたいなーと思った時単純にmap
メソッドを重ねると下記のようになってしまいます。
const I = require('immutable');
const daysOfWeek = I.List(['月', '火', '水', '木', '金']);
const lessons = I.List([1, 2, 3, 4, 5]);
const lessonsOfWeek = daysOfWeek.map(dayOfWeek => lessons.map(lesson => I.List([dayOfWeek, lesson])));
console.log(lessonsOfWeek); // List [ List [ List [ "月", 1 ], List [ "月", 2 ] ... , List [ List [ "火", 1 ] ... , List [ "金", 5 ] ] ]
Listが三段重ねになって、曜日と曜日の間に間に余計なListができてしまいます。
こんな時にArray
は持っていないメソッドであるimmutable.List.flatMap
を使ってみます。
const I = require('immutable');
const daysOfWeek = I.List(['月', '火', '水', '木', '金']);
const lessons = I.List([1, 2, 3, 4, 5]);
const lessonsOfWeek = daysOfWeek.flatMap(dayOfWeek => lessons.map(lesson => I.List([dayOfWeek, lesson])));
console.log(lessonsOfWeek); // List [ List [ "月", 1 ], List [ "月", 2 ] ... , List [ "火", 1 ] ... , List [ "金", 5 ] ]
一発で望む形のListを得ることができました!
こんな感じでimmutable.jsにはビルトインメソッドにはない強力な道具が色々搭載されています。
大概の望む形は一発で取得できると思いますので大変気に入っています。
今回は以上です。