JavaScript での DOM 操作を関数型プログラミングっぽく書いてみる

以下のようなHTMLがあります。

<div id="parent">
  <div name="hoge">aaa</div>
  <div name="foo">bbb</div>
  <div name="bar">ccc</div>
</div>

#parentの子要素の中で、name属性がhogeかbarの要素を削除する処理を書いてみます。

const parent = document.getElementById('parent');
const children = Array.from(parent.children);

const eachElement = (elements, condition, applyFunc) => {
  elements.forEach(e => {
  	if(condition(e)) applyFunc(e);
  });
};

const compareName = names => element => names.includes(element.getAttribute('name'));
const removeElement = (element) => element.remove();

setTimeout(() => {
	eachElement(children, compareName(['hoge', 'bar']), removeElement)
}, 2000);

子要素すべてを取得する

まず、親要素のDOMを取得し、その children を取得します。このままだと parent.children は配列ではないので、Array.from を使って配列に変換しています。

const parent = document.getElementById('parent');
const children = Array.from(parent.children);

汎用的な処理を作る

続いて、汎用的な関数を作ります。

今回は、eachElement という関数を定義しました。

この関数は、第一引数に処理対象の要素の配列を受け取り、第二引数に処理を実行する時の条件(関数)、第三引数に書く要素に適用する処理(関数)を受け取ります。

// elements 配列
// condition 関数 applyFunc を適用するときの条件
// applyFunc 関数 各要素に適用する処理
const eachElement = (elements, condition, applyFunc) => {
  elements.forEach(e => {
  	if(condition(e)) applyFunc(e);
  });
};

今回の場合、第一引数に上記で取得した children、第二引数にname属性がhogeかbarならtrueを返す関数、第三引数に要素をremoveする処理を渡します。

eachElement に渡す関数を定義する

では、前述の「name属性がhogeかbarならtrueを返す関数」と「要素をremoveする処理」を定義します。

// names(配列)を受け取ると、その配列に含まれるnameを持っていればtrueを返す関数を返す
const compareName = names => element => names.includes(element.getAttribute('name'));

// 要素を受け取り、要素をremoveする
const removeElement = (element) => element.remove();

compareName は引数を一つにするためにカリー化してあります。

関数を組み合わせて目的の処理を作る

最後に eachElement に引数を渡して実行します。消えることが確認できるように2秒後に消えるようにsetTimeoutを使っています。

setTimeout(() => {
	eachElement(children, compareName(['hoge', 'bar']), removeElement)
}, 2000);

eachElement がどのような関数かが分かっていれば、上記が何をやる処理なのかなんとなくわかります。

children(要素の配列)に対して(第一引数)、name属性が hoge か bar のものを(第二引数)、削除する(第三引数)、というように引数をみるだけで、処理内容がわかります。

各関数の実装を読まなくても、このように宣言的にかけることで可読性があがることが関数型プログラミングのメリットでもあります。

さらに、子要素の中で text が bbb 以外の要素を削除する、という処理もこの eachElement と新しい関数を組み合わせれば実現可能であり、eachElement にはなんの変更も加える必要がありません。

const parent = document.getElementById('parent');
const children = Array.from(parent.children);

const eachElement = (elements, condition, applyFunc) => {
  elements.forEach(e => {
  	if(condition(e)) applyFunc(e);
  });
};

const compareName = names => element => names.includes(element.getAttribute('name'));
const removeElement = (element) => element.remove();

const compareText = text => element => element.innerText === text; // text を比較する関数を定義

// 関数の結果を反転させた関数を返す
const not = (func) => {
	return (arg) => !func(arg)
}

setTimeout(() => {
	eachElement(children, not(compareText('bbb')), removeElement)
}, 2000);

このように関数を組み合わせてプログラミングをすることで、非常に柔軟で可読性の高いコードを書くことができます。