備忘と現場で使える JavaScript Tips
はじめに
現場でコードを書いていると、以前学習したことをすっかり忘れており時間をロスしてしまうことがあったりする。
案件が終わったタイミングでその都度備忘とTipsを纏められたら良いのだが、業務内にその時間の確保するのがけっこう難しくて今年もそれができなかった。
コロナの影響で例年以上に時間に余りある正月休みになっているので、お酒でも飲みながらゆるっと思い出せるかぎりの備忘とTipsを残していこうと思う。大した内容でもないがどこぞの誰かにとっての一助になれば嬉しい。
備忘とTipsの線引きはけっこう適当である。
では備忘。
element.querySelectorAll()
element.querySelectorAll は、NodeList
を取得していることを忘れがちになる。
element.querySelector だと Element
を取得するので、つい同じ感覚で扱おうとして画面に現れるこいつ。
Uncaught TypeError: hogeList.map is not a function (なんでや)
配列として扱えるようになるには Array.from
を使って新しい配列インスタンスを生成してからであることを覚えておきたい。
// 前提として dummyElement はすでに取得済みのElementノード const hogeList = Array.from(dummyElement.querySelectorAll('.targetClass'));
Function.prototype.apply()
apply()
は、thisを指定してFunctionを呼び出し、配列を引数にして関数を呼び出すことができる。
例えば Math.max()
で引数に配列渡して最大値取り出そうと思ったら配列渡せないわ、みたいなことがたまに現場で遭遇してた。
別に reduce
とか使えば最大値は取り出せるけど、楽に取り出したいときは、断然以下の方法が簡単だったりするので apply()
は覚えておきたい。
const compareArray = [1, 2, 3, 4, 5]; const maxNum = Math.max.apply(null, compareArray); // Mathは組み込みオブジェクトだから this 指定は null で問題なし( this 指定は省略できない)
更に apply()
はスプレッド構文で置き換えることができるので、その方が短く書けるし、見通しもまあ良いので以下のように書きたい。
const compareArray = [1, 2, 3, 4, 5]; const max = Math.max(...compareArray);
DOM操作(要素の取得, 追加, 書き換え)
コードを書いていて、この辺りの操作をしたいけど何を使えば良いんだっけ・・・とかよく詰まっていた気がする。
頻繁に書くようなものでもないので、たまに遭遇して困っていたので纏めておく。
innerHTML
シリアライズされるので結果としてHTML文字列が返される。
InnerHTML
に値を設定すると、要素のすべての子孫を削除して、与えられたHTML文字列をパースして構築されたノードに置き換えられる。
InnerHTML
を使って書き換える場合に起こる順番として、挿入先の要素を一旦全てシリアライズして、全ての子孫を削除して、代入するHTML文字列をパースして、構築したノードで置き換える。
もし挿入先の要素にあらかじめイベントを付与していた場合、この過程でイベントは削除されることを覚えておきたい。
HTMLパースされるのが、外部から指定されたHTML文字列の場合はサニタイズしないとXSSの原因となるので、サニタイズ用の関数などでエスケープして無害化してあげると良い。
サニタイズの参考サイト
How to sanitize third-party content with vanilla JS to prevent cross-site scripting (XSS) attacks | Go Make Things
insertAdjacentHTML
第二引数で指定されたHTML文字列がパースされて、その結果であるノードを第一引数で指定した箇所に挿入する。
挿入先の要素を再度パースしないので、既存の要素や要素に付与されているイベントは削除されない。
InnerHTML
と違って、余分なシリアル化がされないので InnerHTML
への代入による直接的な操作より高速なのでこっちの方が良い場合はこっち使う。
element.parentNode
たまに、取得した要素の親要素を起点として要素の追加をしたい場面に遭遇したりする。
そんなときは element.parentNode
で親ノードを取得すれば、簡単に実現可能なので、parentNode
は覚えておきたい。
取得しているのは Element
ではなく Node
なので、以下のメソッドなどが使える。
このあたり一覧でまとまってるサイトないか探したところ以下のサイトがすごく見やすく纏まってたので参考に載せる。
https://www.wakuwakubank.com/posts/306-javascript-dom/
ではTips。
spliceで一部の配列を取り出して別の配列で組み替えたい
Array.prototype.splice()
で一部の配列を他の配列で組み替えたいときにワンライナーで書ける方法。めちゃ楽だけど僕には思いつけない。
思いついた人はほんと天才だと思った。こういう閃きはどうやったら養えるのだろうか。
参考文献
まず Function.prototype.apply()
を使う方法。
const testArray01 = [{aa: 1}, {aa: 2}, {aa: 3}, {aa: 4}, {aa: 5}, {aa: 6}, {aa: 7}, {aa: 8}, {aa: 9}, {aa: 10}]; const testArray02 = [{bb: 1}, {bb: 2}, {bb: 3}, {bb: 4}, {bb: 5}, {bb: 6}, {bb: 7}, {bb: 8}, {bb: 9}, {bb: 10}]; testArray01.splice.apply(testArray01, [1*5, 1].concat(testArray02)); // output は以下 // Array [Object { aa: 1 }, Object { aa: 2 }, Object { aa: 3 }, Object { aa: 4 }, Object { aa: 5 }, Object { bb: 1 }, Object { bb: 2 }, Object { bb: 3 }, Object { bb: 4 }, Object { bb: 5 }, Object { bb: 6 }, Object { bb: 7 }, Object { bb: 8 }, Object { bb: 9 }, Object { bb: 10 }, Object { aa: 7 }, Object { aa: 8 }, Object { aa: 9 }, Object { aa: 10 }]
備忘で書いた通り、スプレッド構文でより短く書ける。
const testArray01 = [{aa: 1}, {aa: 2}, {aa: 3}, {aa: 4}, {aa: 5}, {aa: 6}, {aa: 7}, {aa: 8}, {aa: 9}, {aa: 10}]; const testArray02 = [{bb: 1}, {bb: 2}, {bb: 3}, {bb: 4}, {bb: 5}, {bb: 6}, {bb: 7}, {bb: 8}, {bb: 9}, {bb: 10}]; testArray01.splice(...[1*5, 1].concat(testArray02));
飲んでたお酒もなくなってしまった。
正月休み中にもう少し書こうと思う。
読んでくれた方々ありがとうございました。
Vuexのモジュール分割について
Vuexでストアを管理していると、アプリケーションが大きくなるにつれてストアが肥大化し、見通しが悪くなる問題が起こります。
それを回避するためにストアを分割できるようになっています。例えばアプリケーションの機能ごとやページごとにストアを分割していれば、コードの見通しがよくなりますし、プロジェクトメンバーで共同で作業しやすくなります。
以下にストアを分割する方法を記載します。
// ストア側 import Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); const moduleA = { namespaced: true, state: {...}, getters: {...}, mutations: {...}, actions: {...} } const moduleB = { namespaced: true, state: {...}, getters: {...}, mutations: {...}, actions: {...} } export default new Vuex.Store({ modules: { aaa: moduleA, bbb: moduleB } });
これでストア分割完了です。
ポイントは各モジュールを定義するときに、namespacedオプションをtrue
に設定することです。
モジュール分割をすると、同一のミューテーションタイプやアクションタイプが指定できることになります。そのため、どちらか一方を呼び出したときに、同一のタイプはすべて呼び出されることになります。それを避けるために、namespaced
をtrue
にすることで影響範囲を限定することができます。
namespaceを与えたときは以下のように、各モジュールのミューテーションタイプ、アクションタイプを指定して呼び出します。
// コンポーネント側 this.$store.commit('aaa/設定したミューテーションタイプ') this.$store.dispatch('bbb/設定したアクションタイプ') this.$store.getters['ccc/設定したゲッター'] // これはゲッターの例
このようにnamespaceを設定することでモジュールごとのミューテーション、アクション、ゲッターを呼び出せます。
マップヘルパーにnamespaceを登録すればモジュール名を省略して呼び出せます。その場合はヘルパーの第一引数にnamespaceを指定します。
computed: { ...mapMutations("aaa",["設定したミューテーションタイプ"]), this.[設定したミューテーションタイプ] }
ちなみにモジュール分割はネストもできるので、以下のように書くことも可能です。
const moduleA = { namespaced: true, state: {...}, getters: { parentGet: ... }, mutations: {...}, actions: {...}, modules: { page01: { // 親のnamespaceを継承するので、例えばゲッターを指定したいときは親モジュールのゲッターの呼び方と同じでOK(下記★参照) state: {...}, getters: { get: // ゲッターの処理書く }, mutations: {...}, actions: {...}, modules: { namespaced = true layer01: { // namespacedを指定しているので、例えばゲッターを指定したいときは親モジュールから相対パスで指定して呼び出す(下記♦参照) namespaced: true, state: {...}, getters: { get: // ゲッターの処理書く }, mutations: {...}, actions: {...}, } } } } }
★の呼び出し方
...mapGetters("aaa", ["parentGet", "get"]), // マップヘルパー使ったとき this.get // これで子のget呼べる this.$store.getters.parentGet // マップヘルパー使わない場合
♦の呼び出し方
...mapGetters("aaa", ["parentGet", "get", "layer01/get"]), // マップヘルパー使ったとき this['layer01/get'] // これで孫のget呼べる this.$store.getters['form/layerPage/profile'] // マップヘルパー使わない場合は親からの相対パスで呼ぶ
モジュール分割は他にも例えば別モジュールに登録された処理を呼び出したり、ルートに登録されたモジュールを呼べたりするので、公式ドキュメント載せておきます。
参考資料
モジュール | Vuex
Vuexのアクションについて
前回、Vuexのミューテーションについて説明しましたがミューテーションは以下のルールがあります。
ミューテーションハンドラ関数は同期的でなければならない
つまりミューテーションでできるのは同期処理のみで、非同期処理はできません。そのため実際に使うとなるとミューテーションだけでは辛くなります。
そのためにアクションがあり、アクションは非同期処理を行うことができます。
アクションはアクションハンドラをアクションに登録してそれを呼び出すことで使います。
実際にアクションハンドラを登録して、それを呼び出す方法を記載します。
やってることはただのカウントアップです。
// ストア側 state: { count: 0 } mutations: { [INCREMENT](state) { state.count += 1; } }, actions: { increment({ commit, state }) { // 例としてプロミスで非同期処理しています return new Promise((resolve, reject) => { setTimeout(() => { resolve("success"); }, 1000); if (state.count === null) { reject("err"); } }) .then(value => { console.log(value); commit("INCREMENT"); }) .catch(err => { console.log(err); return err; }); } }
// コンポーネント側 methods: { aaa() { this.$store.dispatch('increment') } }
やってることは以下です。(ミューテーションには定数を使っている前提です)
- ストア側に状態変更に使うミューテーション登録
- ストア側にそのミューテーションを使うためのアクション登録(登録したアクションハンドラの引数に
commit
渡してるのは後ほど説明します) - コンポーネント側のメソッドオプションにそのアクションを使うためのメソッドaaaを登録
aaaを使うとアクションハンドラ -> ミューテンションハンドラ -> ステート変更 の流れで処理されます。
ポイントになるところは、登録したアクションハンドラの引数にcommit
を渡しているところです。
アクションハンドラは引数にcontext
を取ります。context
とはストアインスタンスで持っているステート、ゲッター、ミューテーション、アクションなどと同じものを呼び出せるオブジェクトです。context
ごと渡してもOKです。
よりシンプルに書くために、分割代入を使って使いたいコンストラクタオプションを呼び出して使います。(上の例ではcommit
とstate
を分割代入して使っています)
また、アクションハンドラはdispatch
されることでトリガーされます。
ミューテーションハンドラはcommit
アクションハンドラはdispatch
アクションの使い方は以上になりますが、最後にコンポーネント側でアクションを呼び出すときの短縮記法を以下に記載しておきます。
// コンポーネント側 methods: { ...mapActions(["increment"]), // まずマップヘルパーに登録 aaa() { // this.$store.dispatch('increment') // これ冗長です this.increment() // これで上と同じ意味です } }
補足として説明してきたVueとVuex周りの用語で混乱しがちなものを以下にまとめました。
Vueのオプション
・data
・computed
・methods
Vuexのオプション
・state
・getters
・mutations
・actions
mutationsオプションに登録するもの
・mutationハンドラ
mutationsハンドラを呼び出すとき
・mutationの名前を指定してコミットする
actionsオプションに登録するもの
・actionハンドラ
actionsハンドラを呼び出すとき
・actionsの名前を指定してディスパッチする
次はモジュール分割について書きたいと思います。
ここまで読んでいただいてありがとうございました!!!
参考資料
アクション | Vuex
Vuexのミューテーションについて
ミューテーションはVuexのストアの状態を変更したいときに使うもので、分かりやすくいうとハンドラに近いものです。
使い方と特徴がいくつかあるので以下にまとめます。
- ミューテーションはタイプとハンドラを持っています
- ハンドラみたいなものなので直接使うことができません(公式ではハンドラに近い概念といっています)
- タイプを指定してコミットすることでハンドラを使えます
- ミューテーションのハンドラは第一引数にstateを渡して実際にstateの変更を行います
- ミューテーションのハンドラは第二引数に追加の引数(payloadといいます)を渡すこともできます※payloadはオブジェクトで渡します
- コンポーネント側でタイプを指定してコミットします
- 上に書いた第一引数と第二引数を無視して、第一引数にtypeプロパティを持つオブジェクトを渡してコミットすることもできます
- ミューテーションハンドラでは非同期処理ができない
具体的に設定すると以下のような感じになります。
// ストア側 state: { hoge: 10 } mutations: { aaa(state) { state.hoge + 100 }, bbb(state, payload) { state.hoge + payload.num // ここではnumはpayloadオブジェクトにセットした値のキーと仮定します } }
コンポーネント側でコミットするには以下のように書きます。
// コンポーネント側 this.$store.commit('aaa') // これがミューテーションタイプを指定した普通のコミット this.$store.commit('bbb', { // これがpayloadも渡す場合のコミット num: 100 }) this.$store.commit({ // これがtypeプロパティを持つオブジェクトを渡した場合のコミット type: 'bbb', num: 100 })
また、ステートとゲッターと同じく、コンポーネント側で上のように書いていても冗長なのでもっと短く書ける方法があります。
mapMutations
を使います。
設定方法は以下のようにします。
// コンポーネント側 import { mapMutations } from "vuex"; export default { methods: { ...mapMutations(["aaa"]) } }
スプレッド演算子を使うことでオブジェクトをマージできるので、他に設定したいmethodsがあったとしても問題ありません。
呼び出すときは以下の記述で書けます。実際仕事ではこっちを使うほうが多いと思います
this.$store.commit('aaa') // マップミューテーション使う前 this.aaa() // 使った後
また、仕事ではミューテーションに定数を使用していることも多いと思うので、以下の方法に慣れておくといいかもしれません。
- 定数を宣言するファイルを作成します
- ファイルをstore側でインポートしてミューテーションのタイプに定数を使います
- コンポーネント側ではタイプとして定数を指定してコミットします
例を出すとこんな感じです。
// mutation-type.js export const AAA = 'AAA'
// ストア側 import { AAA } from './mutation-types' // 分割代入で必要な定数をインポートします state: { hoge: 10 } mutations: { [AAA](state) { // 定数はブラケット記法で書きます。この書き方はES2015のcomputed property nameです state.hoge + 100 } }
// コンポーネント側 import { mapMutations } from "vuex"; export default { methods: { ...mapMutations([AAA]) } }
this.AAA() // 使うとき
基本の使い方はこんな感じになります。
次回はアクションについて書いていきます。
ここまで読んでいただいてありがとうございました!!!
参考資料
ミューテーション | Vuex
Vuexのゲッターについて
Vue.jsには算出プロパティがありますが、Vuexではそれと同じことをgettersを用いて行います。
設定方法は以下のようにgettersの中に好きなgetterを追加していきます。
state: { hoge: 10 } getters: { aaa: aaa(state) { return state.hoge } }
getterは必ず第一引数がstateになります。
コンポーネント側でhogeの値10を使いたい場合、コンポーネント側でgettersを呼び出せば使えます。
呼び出し方は冗長ですが、以下のように書きます。
this.$store.getters.aaa // 短く書く方法もあるのでそれは後ほど説明します
ちなみにgetterはアロー関数を使えば以下のように短く書けるのでこっちのほうが良いです。
state: { hoge: 10 } getters: { aaa: state => state.hoge }
getterの中で他のgetterを呼び出したいときもあると思いますので、その場合は第二引数にgettersを渡して、以下のように使います。
state: { hoge: 10, huga: 100, } getters: { aaa: state => state.hoge, bbb: (state, getters) => getters.aaa + state.huga // getters.aaaでhogeの値を取得 -> hugaの値と足す -> getters.bbbは110になる }
繰り返しになりますが呼び出し方は以下です。
this.$store.getters.bbb
ここまで、getterの第一引数にstate、第二引数にgettersを渡すことでストアオブジェクトの状態(stateオブジェクトの中身のこと)を参照できました。
この方法をプロパティアクセススタイルといいます。
さらに、ストアオブジェクトの状態を参照するもうひとつの方法があります。
その方法をメソッドアクセススタイルといいます。
メソッドアクセススタイルでは引数を渡すことができます。これはプロパティアクセススタイルではできません。
以下に両者の違いを書きます。
aaa: state => state.hoge, // プロパティアクセススタイル: getters.aaaでhogeの値を取得 zzz: (state, getters) => num => getters.aaa + num // メソッドアクセススタイル: getters.aaaでhogeの値を取得 + 引数numを足す
呼び出し方は以下です。
this.$store.getters.zzz(500) // 引数が渡せる
ここまでgetterを呼び出す2つの方法について説明しました。
最後にgetterを短い記述で呼び出す仕組みを説明します。
コンポーネント側で短い記述で呼び出す仕組みをmapGettersヘルパーといいます。
これを使うと以下のように省略して書くことができます。
コンポーネント側で、算出プロパティにmapStateヘルパーを設定します。
// コンポーネント側 computed: { ...mapGetters(["aaa", "bbb"]), computed01() {...}, computed02() {...} }
そうするとコンポーネント側でgetterを呼び出したいときはこれくらい短く書けます。
this.aaa // プロパティアクセススタイル this.zzz(500) // メソッドアクセススタイル
次はミューテーションについてまとめたいと思います。
ここまで読んでいただいてありがとうございました!!
参考資料
ゲッター | Vuex
Vuexのステートについて
Vue.jsを使用するときに、アプリケーションの状態を管理するためにVuexを使うことがほぼだと思いますので、これから何回かに渡ってVuexについての基本的な使い方をまとめようと思います。
今回はVuexについて少し触れて、あとはステートについてまとめようと思います。
なぜVuexを使う必要があるのかというと、アプリケーション全体の状態を1つの場所で管理しておけば、現在のアプリケーションの状態のスナップショットが容易になるのでデバッグしやすくなります。
- Vuexはsingle state tree(アプリケーション全体の状態が1つのオブジェクトツリーに格納される)
- スナップショットが容易なのでデバッグしやすい
Vuexを使うときはVuex専用のファイルを作ってそれをルートコンポーネントでインポートする使い方が一般的です。
// store.js import Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); // SFCを使う場合はこの記述を書く -> この記述で各コンポーネントでstoreを参照できるようにする機能が有効になります。 export default new Vuex.Store({ state: { aaa // ステートプロパティを設定していく bbb hoge: 10 } })
store.jsをルートコンポーネントに設定する
import store from "./store"; new Vue({ store, // SFCを使う場合はこの記述を書く -> この記述で各コンポーネントでstoreを参照できるようになりました。 aaa: 1, bbb: 5, hoge: 10 })
これでVuexを使う準備と、SFCでstoreを参照できる設定が完了です。
各コンポーネントでstoreを参照する方法は以下です。
this.$store.state.hoge // これで設定したステートプロパティにアクセスできます。(例でhogeプロパティを参照しています。)
ただこの書き方は冗長過ぎて面倒なので、短縮できる仕組みが用意されています。
その仕組みをmapStateヘルパーといいます。これを使うと以下のように省略して書くことができます。
コンポーネント側で、算出プロパティにmapStateヘルパーを設定します。
computed: { ...mapState(["aaa", "bbb", "hoge"]), computed01() {...}, computed02() {...} }
そうするとコンポーネント側でステートプロパティを参照したいときはこれくらい短く書けます。
this.hoge // mapStateヘルパーを使う前はthis.$store.state.hoge
mapStateヘルパーはcomputedにスプレッド演算子で設定します。
mapStateヘルパーは以下の特徴があるのでこのように設定できます。
- mapState内ではthis.$store.stateと同じ意味としてstateが使える
なのでmapStateの正体はただ算出プロパティにステートプロパティのゲッターを設定しているだけです。
mapState({ aaa: state => state.aaa, bbb: state => state.bbb, hoge: state => state.hoge })
しかもmapStateには以下の特徴もあります。
なのでここまでシンプルに書けます。
mapState(["aaa", "bbb", "hoge"]),
最後に、なぜスプレッド演算子を使わないといけないか説明します。
mapState()はオブジェクトをリターンするので、mapState(["aaa", "bbb", "hoge"])を実行したときに返り値は以下になります。
{ aaa() { return this.$store.state.aaa }, bbb() { return this.$store.state.bbb }, hoge() { return this.$store.state.hoge }, }
そうすると、算出プロパティに他の関数をセットしたいときに以下のようになり問題が起こります。
computed: { { aaa() { return this.$store.state.aaa }, bbb() { return this.$store.state.bbb }, hoge() { return this.$store.state.hoge }, }, computed01() {...}, computed02() {...} }
そこでスプレッド演算子を使ってオブジェクト同士をマージします。(mapStateの返り値と、computed01() {...}とかcomputed02() {...}をマージする)
...mapState(["aaa", "bbb", "hoge"]),
そうすると算出プロパティの中が以下のようになります。
computed: { aaa() { return this.$store.state.aaa }, bbb() { return this.$store.state.bbb }, hoge() { return this.$store.state.hoge }, computed01() {...}, computed02() {...} }
長くなりましたが、Vuexのステートについて基本的な使い方でした。
ここまで読んでいただいてありがとうございました!!!
参考資料
ステート | Vuex
配列操作でよく使うメソッドまとめ05
前回の配列操作でよく使うメソッドまとめ04つづき。
今回も前回までと同じJSONデータを使ってfindの使い方についてまとめます。
JSONデータの中身は配列操作でよく使うメソッドまとめ01のページ下部に載っています。
findを使えば引数で与えた関数に一致する要素を返してくれます。
今回は選手名で検索して、その選手がいるチームを返そうと思います。
特定の選手がいるチームを返す
import data from '../EasternConference.json'
return data.find(data => { for(let i = 0; i < data.members.length; i++) { if(data.members[i].name === "Quincy Acy") { return data } } })
これで該当選手がいるチームを返すことができます。