Madogiwa Blog

主に技術系の学習メモに使っていきます。

JavaScript: クロージャーを使ってレキシカル環境ごと関数を渡して処理を共通化するメモ📝

JavaScriptを使った非同期処理を実装するときに、 try-catchで囲うとか、isLoadingsuccessといったstateを用意して、 状態を管理すようなロジックが個人的にそれぞれのメソッドに書きがちになっていて、 毎回実装するのが手間、また実行漏れがあったりと共通化したいなぁと思っていたのですが、 JavaScriptクロージャーを使うをいい感じに共通化出来た気がしたのでメモしておきます📝

クロージャーとは?

MDNのドキュメントを見てみるとクロージャーとは下記とのことです。

クロージャは、関数と、その関数が宣言されたレキシカル環境の組み合わせです。 https://developer.mozilla.org/ja/docs/Web/JavaScript/Closures

例えば以下のような関数があった場合、

function parent(parentName) {
  const childName = 'child name'
  function child() {
    console.log(parentName)
    console.log(childName)
  }
  child()
}

parent('parent name')

結果は下記のようなものになり、childからparentで定義した変数が呼び出せることがわかります。

parent name
child name

つまりクロージャーとは関数(今回で言えばchild)とレキシカル環境(今回で言えばparentで定義された変数群)がセットになったものという理解で良いのかなと思っています。

普通に実装したサンプル

クロージャーについて理解が深まったところでサンプルを見てみます。

以下のようなドキュメントを何かしらの形式に変換するメソッドとドキュメントを保存するメソッドがあるとします。

非同期リクエストを投げるときにはtry-catchで囲んだり、ロード中や成功を管理する変数を操作したりする必要があります。

現状はそれぞれのメソッドに記載しているので、手間がかかるのと実装もれの恐れがあります😢

let state = { isLoading: false, success: false, converted: "" }

const handleOnDocumentConvert = async (to, markdown) => {
  if (state.isLoading) return;
  state.isLoading = true;

  try {
    const response = await convertDocument(to, markdown);
    const responseJson = await response.json();
    state.converted = responseJson.result;
    state.success = true;
  } catch (err) {
    console.error(err);
  } finally {
    state.isLoading = false;
  }
};

const handleOnDocumentSave = async (markdown) => {
  if (state.isLoading) return;
  state.isLoading = true;

  try {
    const response = await saveDocument(markdown, state.documentId);
    const responseJson = await response.json();
    if (!state.documentId) state.documentId = responseJson.documentId;
  } catch (err) {
    console.error(err);
  } finally {
    state.isLoading = false;
  }
};

クロージャーを使って共通化してみる

非同期リクエストを送信するときに諸々必要な処理をsendRequestで行うようにしてみた例が下記です。

sendRequestは諸々必要な処理とともに引数で受け取ったrequestFunctionを実行しています。

requestFunctionは呼び出し時に渡されたクロージャーとなっているため、 呼び出し元の親メソッドの定義された変数等を含んだレキシカル環境とともに関数が渡されます✨

そのため一見定義されてない変数が参照されてエラーになるかと思いきや実行出来るわけですね。

諸々の処理も共通化出来たのでスッキリ + 実装漏れも防げそうです。

const sendRequest = async (requestFunction) => {
  if (state.isLoading) return;
  state.isLoading = true;
  try {
    await requestFunction();
    state.success = true;
  } catch (err) {
    console.error(err);
  } finally {
    state.isLoading = false;
  }
};

const handleOnDocumentConvert = async (to, markdown) => {
  const convertRequestFunction = async () => {
    const response = await convertDocument(to, markdown);
    const responseJson = await response.json();
    state.converted = responseJson.result;
  };
  sendRequest(convertRequestFunction);
};

const handleOnDocumentSave = async (markdown) => {
  const saveRequestFunction = async () => {
    const response = await saveDocument(markdown, state.documentId);
    const responseJson = await response.json();
    if (!state.documentId) state.documentId = responseJson.documentId;
  };
  sendRequest(saveRequestFunction);
};

おわりに

クロージャーを使って関数だけじゃなくてレキシカル環境も渡すことによって、 外部の変数に依存するような関数でも処理を共通化出来るのは便利ですね。

先程下記のように書いたとおり、一見エラーになりそうと思ってしまうのは慣れてないせいもありますが、使いすぎると逆に見にくいコードになってしまうかもですね💦

その関数が実行されるので一見定義されてない変数が参照されてエラーになるかと思いきや実行出来るわけですね。

参考

developer.mozilla.org