細かすぎるけど伝わって欲しいlodash.jsの話

ギリギリの時間にこんばんは、12/2のアドベントカレンダーは、HTMLファイ部ののびーがお送りします。受託事業でWebフロントを書いたり、テクニカルディレクションをやったりしている人です。

さて今年はさっくりと、常日頃からお世話になっているlodash.jsというライブラリについて、掘り下げた紹介をさせていただきます。

lodash.jsとは

https://lodash.com/

A modern JavaScript utility library delivering modularity, performance & extras.

つまるところ、

  • ユーティリティー(なんか便利)関数を集めたやつ
  • めっちゃかるい

という特徴を持つライブラリです。近頃のフロントエンドの流行りであるThree.jsだったり、Vue.jsだったりといったものに比べると 地味 です。非常に地味ですが、個人的にはそれらの派手なライブラリよりも圧倒的に利用頻度が高く、多少なり データを取り回す案件では必須 とも考えているライブラリです。

主な利用ケース

自分の中では、おおむね以下の3種の用途があるかなと思っています。

  • (1) 配列やオブジェクトをほしい形に整形する
  • (2) イデオム系の実装
  • (3) テンプレートエンジン

(1) 配列やオブジェクトをほしい形に整形する

※痒いところに手が届く便利メソッドが超いっぱいあるぞという話と、似てるけど微妙に違うメソッドが超いっぱいあるぞという話をします。

each

_.each(bigArray, (item) => {
    console.log(item);
});
  • _.xxx() のかたちで呼び出す
  • さすがに中身は説明しません

map

// 例のためのありふれた配列
const charaArray = [{
    id: 25,
    name: 'ピカチュウ',
    type: [ 'でんき' ]
},{
    id: 35,
    name: 'ピッピ',
    type: [ 'ノーマル' ]
},{
    id: 39,
    name: 'プリン',
    type: [ 'ノーマル' ]
}];

_.map(charaArray, (chara) => {
    return chara.id;
});
// => [ 25, 35, 39 ]
  • 整形の基本。配列に入ったオブジェクトを一つずつループして、欲しい値だけ取り出してreturn

mapValues

// 例のためのありふれたオブジェクト(idをキーにした連想配列)
const charaData = {
    25: {
        name: 'ピカチュウ', type: [ 'でんき' ]
    },
    35: {
        name: 'ピッピ', type: [ 'ノーマル' ]
    },
    39: {
        name: 'プリン', type: [ 'ノーマル' ]
    }
};
_.mapValues(charaData, (chara) => {
    return chara.name;
});
// => { 25: 'ピカチュウ': 35: 'ピッピ': 39: 'プリン' }
  • オブジェクトの値をひとつずつループして、欲しい値をつくってreturn

  • mapとの違いは配列ではなく、オブジェクトを受け取ってオブジェクトを返す こと

  • 配列を渡すと暗黙的にオブジェクトに変換してくるので注意

// オブジェクトを渡してオブジェクトを返す
_.mapValues(charaData, (chara) => {
    return chara.name;
});
// => { 25: 'ピカチュウ': 35: 'ピッピ': 39: 'プリン' }

// 配列を渡されたらオブジェクトとして解釈
_.mapValues(charaArray, (chara) => {
    return chara.name;
});
// => { 0: 'ピカチュウ', 1: 'ピッピ', 2: 'プリン' }
// 配列を渡して配列を返す
_.map(charaArray, (chara) => {
    return chara.name;
});
// => [ 'ピカチュウ', 'ピッピ', 'プリン' ]

// オブジェクトを渡されたら配列として解釈
_.map(charaData, (chara) => {
    return chara.name;
});
// => [ 'ピカチュウ', 'ピッピ', 'プリン' ]

mapKeys

_.mapKeys(charaArray, (chara) => {
    return chara.id;
});
/* => { 25: { id: 25, name: 'ピカチュウ', type: [ 'でんき' ] },
35: { id: 35, name: 'ピッピ', type: [ 'ノーマル' ] },
39: { id: 39: name: 'プリン', type: [ 'ノーマル' ] } } */
  • オブジェクトのvalueをひとつずつループして、欲しい keyを つくってreturn
  • 上記はむしろ、暗黙的にオブジェクトに変換してくるのを上手く利用できるケース

groupBy / invertBy

  • xxxをキーにしたオブジェクトをつくりたいシリーズ
  • このへんまで来るとかなりマニアック
_.mapKeys(charaArray, 'id');
/* => { '25': { id: 25, name: 'ピカチュウ', type: [ 'でんき' ] },
'35': { id: 35, name: 'ピッピ', type: [ 'ノーマル' ] },
'39': { id: 39, name: 'プリン', type: [ 'ノーマル' ] } } */

// valueに入るのは元と同じオブジェクト(もしキーが被っていたら片方しか残らない)
_.groupBy(charaArray, 'id');
/* => { '25': [ { id: 25, name: 'ピカチュウ', type: [Array] } ],
'35': [ { id: 35, name: 'ピッピ', type: [Array] } ],
'39': [ { id: 39, name: 'プリン', type: [Array] } ] } */

// valueに入るのは元と同じオブジェクトの配列(キーが被っているものをグルーピング
_.invertBy(charaArray, 'id');
// => { '25': [ '0' ], '35': [ '1' ], '39': [ '2' ] }

// valueに入るのは元のキーにあたるもの

絞込系

filter

  • 配列を、条件に合う(第2引数で渡した関数がtrueを返す)ものだけに絞り込んで返す

  • ついでで例示しちゃったけど _.includes は第1引数の配列に第2引数の要素が含まれてるかを返します 絞込系と相性よし

_.filter(charaArray, (item) => {
    return _.includes(item.type, 'でんき');
});
// => [ { id: 25, name: 'ピカチュウ', type: [ 'でんき' ] } ]

find

  • 条件に合った 最初の要素 を返す
_.find(charaArray, (item) => {
    return _.includes(item.type, 'ノーマル');
});
// => { id: 35, name: 'ピッピ', type: [ 'ノーマル' ] }

pickBy

  • _.filter のオブジェクト用
  • map / mapValues のときと似たような型変換も起きる
_.pickBy(charaData, (item) => {
    return _.includes(item.type, 'でんき');
});
// => { 25: { name: 'ピカチュウ', type: [ 'でんき' ] } }

compact

  • 配列からfalsyな要素を削って返す
_.compact([ 0, 1, 2, '', 'a', 'ab', null, undefined ]);
// => [ 1, 2, 'a', 'ab' ]
// 複数行テキストから空行を除く例
_.compact(longText.split(/\n/g));

集合系

union / intersection / difference

  • それぞれ和集合・積集合・差集合
  • 集合として扱う = 重複をはじく だということも把握しておくと便利
_.union([2], [1, 2]);
// => [2, 1]

// 集合として扱うので重複は弾かれている
_.intersection([2, 1], [2, 3]);
// => [2]

// 両方に含まれる要素の配列を返す
_.difference([2, 1], [2, 3]);
// => [1]

// 前者にだけ含まれる要素の配列を返す

並び替え系

sortBy / orderBy

  • 関数あるいはプロパティ名で並び替え
  • orderByの方が、昇順降順が選べてすごい
  • 破壊的(元の配列の順番を変えてしまう)なので注意
// 名前昇順
_.sortBy(members, 'name'); // => membersが書き換わる

// 名前の長さ昇順
_.sortBy(members, (item) => {
    return item.name.length;
});

// 名前・作成日昇順(名前のほうが優先)
_.sortBy(members, [ 'name', 'created_at' ]);
// 名前降順
_.orderBy(members, 'name', 'desc'); // => membersが書き換わる

// 名前の長さ昇順
_.orderBy(members, (item) => {
    return item.name.length;
}, 'desc');

// 名前昇順・作成日降順(名前のほうが優先)
_.orderBy(members, [ 'name', 'created_at' ], [ 'asc', 'desc' ]);

shuffle / sample

  • shuffleは見ての通り
    • 破壊的(元の配列の順番を変えてしまう)なので注意
  • sampleは適当に1個取ってきてくれる
// メンバーをランダムに並び替える
_.shuffle(members); // => membersが書き換わる

// メンバーからランダムに1人選ぶ
_.sample(members);

wrapper記法

個人的に、lodash初心者と上級者分ける境目だと思っているのがこのwrapper記法を使いこなせているかどうかです。 wrapper記法を使うことで、複数のlodash関数を メソッドチェーンで繋ぐことができます (第1引数が配列/オブジェクトなもの)。これを使うことで、lodash関数をいくつも重ねて使わなくてはいけないような複雑なデータ変換も、かなりスマートに書けるようになります。

// キャラの名前一覧をシャッフルして取得
_(charaArray).map((chara) => {
    return chara.name;
}).shuffle().value();

// 最終的な配列をとるために .value() というメソッドを叩く必要がある
// メンバーのスコアを合計して、スコア上位5名を割り出した上で、
// idをキーにしたオブジェクトにして返す
_(members)
    .map((member) => {
        member.scoreSum = _.sum(member.scoreList);
        return member;
    })
    .orderBy('scoreSum', 'desc')
    .slice(0, 5)
    .mapKeys((member) => {
        return member.id;
    })
    .value();

(2) イデオム系の実装

javascriptでよく登場する処理、イデオムと呼ばれている一連の処理のいくつかを、lodashはメソッドとして提供しています。このあたりは自分で書いたとしてもそれほどの長さにはならないのでそうしてもよいのですが、lodashから借りてくることで、実装時間の短縮やバグ減少、一緒に開発しているメンバーへの説明のしやすさなどで利点があります。

padStart

  • 数字のゼロ詰めなどをする
  • ゼロ以外で詰めたり、後ろに詰めたり( _.padEnd
_.padStart(5, 2, '0'); // => '05'
_.padStart(12, 4, '0'); // => '0012'
_.padStart(100, 2, '0'); // => '100'

throttle / debounce

  • おなじみ、処理を間引くやつ
  • 1度実行したらしばらくやらない => throttle
  • しばらく再実行されないのを待ってから実行 => debounce
  • lodashで数少ない非同期関数
window.addEventListener('resize', _.throttle(() => {
    // throttle: 一度この処理を実行したら、その後1000msは再実行しない
    console.log('resize!');
}, 1000));

window.addEventListener('resize', _.debounce(() => {
    // debounce: 1000ms以上間隔があくまでこの処理を実行しない
    console.log('resize!');
}, 1000));

(3) テンプレートエンジン

javascriptで動的にDOMを生成する、といえば今ならReact.jsやVue.jsのようなSPA系フレームワークを思い浮かべるかもしれませんが、フレームワークを使うほとでもないが、一部分だけそうした処理を行いたい、というようなケースもあると思います。

そんな時に便利なのが、javascript上で動作するテンプレートエンジンです。lodashにはなんとそんなテンプレートエンジンも組み込まれており、必要になったタイミングでスマートに呼び出すことができます。

// pug
// htmlの中にlodashテンプレートを仕込んでおく
script#template-message-modal(type="template/lodash")
  .modal
    .modal__message!= '<%= message %>'
// 仕込んでおいたテンプレートを読み出して、lodashで関数化
const tmplSrc = document.getElementById('template-message-modal').innerHTML;
const tmpl = _.template(tmplSrc);

// 引数を渡してHTMLを生成
document.body.innerHTML += tmpl({ message: 'hello!!' });

まとめ

  • fluxなりreduxなりを扱うと、絶対にpure objectをごりごりぶん回す機会は増えるので、覚えておくと損はない
  • ぜんぶ覚えるのはつらい
    • 頑張ってlodashを使ったやり方を思い出すより、手書きしちゃったほうが早いケースはもちろんある
    • とはいえ、だいたいどういうことができるのかを掴んでおくと、「もしや」という時に見つけられるので、ドキュメントをブックマークしておくと吉

カヤックのHTMLファイ部では、派手なものを作りたい人も、こうした地味な技術に精通している人も広く募集しています!! あなたの得意が活きる仕事もきっとあります。

明日は id:kazasiki さんの記事です。おたのしみに!