iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

歴史を感じつつ、クロージャで遊んでみた(JavaScript編)

こんにちは!株式会社iimonでフロントエンジニアをしている「ひが」です!
本記事はアドベントカレンダー19日目の記事になります!

先日夢で「メリークロージャマス!!!」と叫んでスベる夢を見ました。 冬だからか、みなさん冷たかったです(現実では暖かいです)
そのようなこともあり、思い切って記事にしてみようと思いました!
どうか、暖かい目で見守っていただけると嬉しく思います!!

本題

本題ですが、みなさんはクロージャをご存知でしょうか。
MDNよりお言葉を借りると

クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせ

です! 初見だと何言ってるかよくわからないですよね。
本記事でざっくり掘っていきますので、是非一緒に見ていただければ幸いです!
※ 本記事は基本的にMDNの内容に沿う形で進めつつ、たまに脱線したり小ネタを挟んだりします
※ コードの内容も面白くしようと努力はしましたが、暖かい目で見守っていただけると...

まず初めに、コードをいくつか書きますので動確用のプロジェクトを用意します。 今回は手軽にvite+vanilla-tsでセットアップしました。

プロジェクト構築後、サーバを立ち上げると良い感じの初期画面が立ち上がります。
(何気にカウントアップ部品も用意されていて、気が利いてますね!)

取り急ぎプロジェクト構築ということで、挨拶してもらいます。

main.ts

import { setupClosure } from "./closure.ts";

document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
  <div>
    <div id="closure"></div>
  </div>
`;

setupClosure(document.querySelector<HTMLButtonElement>("#closure")!);

closure.ts

export function setupClosure(element: HTMLButtonElement) {
  let greet = "私はクロージャ、名前はまだない";
  const setClosure = () => {
    element.innerHTML = `挨拶:${greet}`;
  };

  setClosure();
}

良い感じですね!
良い挨拶もいただけましたので、さっそくレキシカルスコープについて見ていきましょう。

closure.ts

export function setupClosure(element: HTMLElement) {
  let greet = "私はクロージャ、名前はまだない";
  const setClosure = () => {
    element.innerHTML = `挨拶:${greet}`;
    temporaryMyName();
    // console.log(temporaryMyName); ← Cannot find name 'temporaryMyName'.
    // console.log(setTemporaryMyName); ← Cannot find name 'setTemporaryMyName'.
  };

  function temporaryMyName() {
    const temporaryMyName = "強いていうならクロージャマス";
    function setTemporaryMyName() {
      // setTemporaryMyName() は内部に閉じた関数
      const sorryDiv = document.createElement("div");
      sorryDiv.textContent = `${temporaryMyName}`;
      element.appendChild(sorryDiv);
    }

    setTemporaryMyName();
  }

  setClosure();
}

上記では関数内にローカル変数とネストされた内部関数を作成し、最後に内部関数を実行するという簡単な作りになっています。ローカル変数は外部から直接参照することはできませんが、内部関数から参照できていることが分かるかと思います。

JavaScriptではレキシカルスコープが採用されており、関数が入れ子になっている場合は内側から外側のスコープで宣言された変数にアクセスできるようパーサーが解釈しているようです。
レキシカルスコープは「字句/静的スコープ」とも訳されますが、入れ子状の関数などに対してまさにコードの字句の並びに基づいて決定されている方式で、プログラムの解析時に静的にスコープが決まるということらしいです。 そのため、ソースコード内で宣言された場所によって利用できる範囲も決まります。
(あまり細かいこと書くと長引きそう&詳しい人に怒られそうなので、ひとまずここまでで)
ちなみにレキシカルスコープに対称にダイナミック(動的)スコープというものもあるみたいです。 言葉の通り、呼び出し元によって処理結果が変わるみたいですが、レキシカルスコープに慣れている方はかなり混乱しそうです。
(使いこなせれば気持ち良さそうですが)
言語の採用例では、RubyJavaJavascriptPythonなど有名どころの言語は基本的にレキシカルスコープですが、ダイナミックスコープは初期のLISPEmacs Lisp、LOGO、Perl(「local」宣言した変数)、Bashなどが採用されているみたいです。

さてさて、ざっくりレキシカルスコープについて見ましたが、スコープ関連でfunctionとarrow関数の違いについても簡単に見て見ましょう。 下記の例では従来のfunctionとarrow環境で同じ内容の処理を書いてますが、結果が異なることが分かります。

closure.ts

export function setupClosure(element: HTMLElement) {
  let greet = "私はクロージャ、名前はまだない";
  const setClosure = () => {
    element.innerHTML = `挨拶:${greet}`;
    myNameTimer.start();
  };
  const myNameTimer = {
    temporaryMyName: "強いていうならクロージャマス",
    voiceOfTheHeart: "(心の声)言わされてるのです",
    start: function () {
      // 従来の関数
      setTimeout(function () {
        const myNameDiv = document.createElement("div");
        myNameDiv.textContent = `function関数: ${
          this.voiceOfTheHeart
            ? this.voiceOfTheHeart
            : myNameTimer.temporaryMyName
        }`;
        element.appendChild(myNameDiv);
      }, 5000);

      // アロー関数
      setTimeout(() => {
        const myNameDiv = document.createElement("div");
        myNameDiv.textContent = `アロー関数: ${
          this.voiceOfTheHeart
            ? this.voiceOfTheHeart
            : myNameTimer.temporaryMyName
        }`;
        element.appendChild(myNameDiv);
      }, 10000);
    },
  };
  setClosure();
}

「this」が鍵になるのですが、従来のfunctionにはthisのバインドがあり実行タイミングによってthisが参照するものも変わってきますが、arrow関数はthisへのバインドがないので宣言されたタイミングで親から引き継いでthisを確定させます。
上記の例ではarrow関数はthis経由で親の変数を参照できていますが、従来のfunctionだとundefinedになってしまうため、結果も変わってきちゃってますね。 (他にも従来functionとarrow関数の挙動差があるのですが、今回の記事ではスコープ外とさせていただきます!)
letとconstについても見て見ましょう。
ES6以前ではJavaScriptの変数は関数スコープとグローバルスコープに二種類しかなかったとのことで、例えばvarで宣言された変数はブロック内で宣言されたとしてもグローバルとして扱われてしまいます。 思わぬグローバル化でハマりそうですね、怖いです。
そのため、ES6のletとconstでスコープがだいぶ整理されました。 現在はみなさんも恩恵にあやかっているのではないでしょうか。
下記のコード例から見ても、違いがわかりますね!

check.ts

// constとletの登場で救われるvar
if (Math.random() > 0.5) {
  var myName = "私はクロージャマス";
} else {
  var myName = "ほんとの名前はメリークロージャマス";
}
console.log(myName); // 気分次第

if (Math.random() > 0.5) {
  const anyway = "どうせエラー!";
} else {
  const anyway = "どうせエラー!";
}
// console.log(anyway); ←Cannot find name 'anyway'.(ちゃんとエラー)

ここまでスコープまわりを見てきましたが、いよいよ(やっと)クロージャを見ていきます!

closure.ts

export function setupClosure(element: HTMLElement) {
  let greet = "私はクロージャ、名前はまだない";
  const setClosure = () => {
    element.innerHTML = `挨拶:${greet}`;
    const temporaryMyNameFunc = temporaryMyName();
    temporaryMyNameFunc(); // ←setTemporaryMyName()が実行
  };

  function temporaryMyName() {
    const temporaryMyName = "強いていうならクロージャマス";
    function setTemporaryMyName() {
      // setTemporaryMyName() は内部に閉じた関数
      const sorryDiv = document.createElement("div");
      sorryDiv.textContent = `${temporaryMyName}`;
      element.appendChild(sorryDiv);
    }

    return setTemporaryMyName;
  }

  setClosure();
}

上記のコードを実行すると最初に確認したコードと同じように動いていますが、着目することは関数の呼び出し方です。
関数を実行した後にまた関数を呼び出して動いてる?!!
と驚かれた方もいるかもしれません、これがクロージャの特徴です。
通常だと関数が実行されたらその内部の変数は破棄されて利用できないように思いますが、上記では二回目の関数呼び出しでもしっかり変数を利用できています。 ここで、もう一度クロージャの定義について見て見ましょう。

クロージャは、組み合わされた(囲まれた)関数と、その周囲の状態(レキシカル環境)への参照の組み合わせ

クロージャは囲まれた関数とその周囲の状態への「参照」の組み合わせです。
上記で外側の関数を実行しその結果を変数で受け取っていますが、その結果が内部関数(setTemporaryMyName)のインスタンスへの参照であり、内部関数は内部変数(temporaryMyName)への参照を持っているため(ややこしいですね)、内部変数が存在し続けられます。 JavaScriptガベージコレクションは変数や関数などで割り当てられたメモリに対して、「不要」となったタイミングで自動破棄してくれます。その「不要」の判断基準が「参照の有無」に依存してるみたいです。 (ここもあまり細かいことを書くと長くなる&えらい人に怒られそうなので、一旦ここまで)
クロージャでレキシカル環境への参照を持ち続ける限り、その環境(内部変数もろもろ)はお掃除されず生き続けられるのですね〜、なんかすごいです!(語彙力が、、気の利いたコメントできずすみません。。)
そのため、下記のように別々に宣言してそれぞれの状態を持つこともできます。
(引数に変数も入れて見ましたが、それも持ち回せてます)

closure.ts

export function setupClosure(element: HTMLButtonElement) {
  let greet = "私はクロージャ、名前はまだない";
  const setClosure = () => {
    element.innerHTML = `挨拶:${greet}`;
    const doNotThink = makeFamous("don't think");
    const believe = makeFamous("believe");

    setTimeout(() => {
      display(doNotThink("feel"));
    }, 1000);
    setTimeout(() => {
      display(believe("in your self"));
    }, 2000);
  };

  function makeFamous(x: string) {
    return function (y: string) {
      return x + y;
    };
  }

  function display(famous: string) {
    const famousDiv = document.createElement("div");
    famousDiv.textContent = `${famous}`;
    setTimeout(() => {
      element.appendChild(famousDiv);
    }, 1000);
  }

  setClosure();
}

なんか、クラスのコンストラクタとプライベートメソッドみたいですね!(propsやstateとも似てます)
以前のJavaScriptではES6までクラスがなかったみたいですが、その頃からもクロージャを使えばうまくカプセル化など模倣することができたみたいです。 また、関数のネストを繰り返してクロージャのスコープチェーンを組むこともできます。
(ちょっと長くなってきたので、詳細はMDNでご確認ください)

さてさてさて、そんなクロージャですが、スコープの理解が甘いと痛い目を見ることがあります。 下記のコードを見て見ましょう。

closure.ts

export function setupClosure(element: HTMLElement) {
  let greet = "私はクロージャ、名前はまだない";
  const setClosure = () => {
    element.innerHTML = `挨拶:${greet}`;
    setupComment();
  };

  function setupComment() {
    var commentText = [
      { id: "member", comment: "今年もタスク消化頑張った" },
      { id: "leader", comment: "チーム進捗も問題なさそう" },
      {
        id: "manager",
        comment: "クリスマスプレゼント(タスク)だよ!!!",
      },
    ];

    const commentDiv = document.createElement("div");
    commentDiv.id = `comment`;
    commentDiv.textContent = ``;
    element.appendChild(commentDiv);
    for (var i = 0; i < commentText.length; i++) {
      var item = commentText[i];
      const itemDiv = document.createElement("div");
      itemDiv.textContent = `${item.id}`;
      element.appendChild(itemDiv);
      itemDiv.onclick = function () {
        showComment(item.comment);
      };
    }
  }

  function showComment(comment: string) {
    document.getElementById("comment")!.textContent = comment;
  }
  setClosure();
}

上記を実行すると、なんと誰をクリックしても「クリスマスプレゼント(タスク)」しか答えてくれません!
本当にみなさんの意思なのでしょうか?実はここにクロージャの罠が隠れています。
onclickに代入された関数はクロージャとなっているため3つのクロージャが構築されますが、その前のvar宣言での変数がグローバルになっているので、一番最後に代入された値の変数を3つのクロージャが参照していることになります。 結果として、「クリスマスプレゼント(タスク)」しか答えてくれなくなりました。

しっかり修正してみなさんの意思を聞いてみましょう! なおし方はいくつかあるのですが、例えばコールバックを用意する方法があります。

closure.ts

export function setupClosure(element: HTMLElement) {
  let greet = "私はクロージャ、名前はまだない";
  const setClosure = () => {
    element.innerHTML = `挨拶:${greet}`;
    setupComment();
  };

  function setupComment() {
    var commentText = [
      { id: "member", comment: "今年もタスク消化頑張った" },
      { id: "leader", comment: "チーム進捗も問題なさそう" },
      {
        id: "manager",
        comment: "クリスマスプレゼント(タスク)だよ!!!",
      },
    ];

    const commentDiv = document.createElement("div");
    commentDiv.id = `comment`;
    commentDiv.textContent = ``;
    element.appendChild(commentDiv);
    for (var i = 0; i < commentText.length; i++) {
      var item = commentText[i]; //←諸悪の根源
      const itemDiv = document.createElement("div");
      itemDiv.textContent = `${item.id}`;
      element.appendChild(itemDiv);
      itemDiv.onclick = makeCommentCallback(item.comment); //←callbackの追加によりそれぞれの要素に対してレキシカル環境を構築;
    }
  }

  function makeCommentCallback(comment: string) {
    return function () {
      showComment(comment);
    };
  }

  function showComment(comment: string) {
    document.getElementById("comment")!.textContent = comment;
  }
  setClosure();
}

これで無事にみなさんの意思が聞けるようになりました! 他にもconstやforEachによるスコープの割り当てで回避する方法もあります。

closure.ts

export function setupClosure(element: HTMLElement) {
  let greet = "私はクロージャ、名前はまだない";
  const setClosure = () => {
    element.innerHTML = `挨拶:${greet}`;
    setupComment();
  };

  function setupComment() {
    const commentText = [
      { id: "member", comment: "今年もタスク消化頑張った" },
      { id: "leader", comment: "チーム進捗も問題なさそう" },
      {
        id: "manager",
        comment: "クリスマスプレゼント(タスク)だよ!!!",
      },
    ];

    const commentDiv = document.createElement("div");
    commentDiv.id = `comment`;
    commentDiv.textContent = ``;
    element.appendChild(commentDiv);
    for (let i = 0; i < commentText.length; i++) {
      const item = commentText[i];
      const itemDiv = document.createElement("div");
      itemDiv.textContent = `${item.id}`;
      element.appendChild(itemDiv);
      itemDiv.onclick = function () {
        showComment(item.comment);
      };
    }
  }

  function showComment(comment: string) {
    document.getElementById("comment")!.textContent = comment;
  }
  setClosure();
}

closure.ts

export function setupClosure(element: HTMLElement) {
  let greet = "私はクロージャ、名前はまだない";
  const setClosure = () => {
    element.innerHTML = `挨拶:${greet}`;
    setupComment();
  };

  function setupComment() {
    var commentText = [
      { id: "member", comment: "今年もタスク消化頑張った" },
      { id: "leader", comment: "チーム進捗も問題なさそう" },
      {
        id: "manager",
        comment: "クリスマスプレゼント(タスク)だよ!!!",
      },
    ];

    const commentDiv = document.createElement("div");
    commentDiv.id = `comment`;
    commentDiv.textContent = ``;
    element.appendChild(commentDiv);
    commentText.forEach(function (item) {
      const itemDiv = document.createElement("div");
      itemDiv.textContent = `${item.id}`;
      element.appendChild(itemDiv);
      itemDiv.onclick = function () {
        showComment(item.comment);
      };
    });
  }

  function showComment(comment: string) {
    document.getElementById("comment")!.textContent = comment;
  }
  setClosure();
}

終わりも近づいてきましたので、この辺でパフォーマンスについて見て見ましょう。
関数インスタンスは各々でスコープとクロージャを管理するため、不要意にクロージャを多用すると処置速度とメモリ消費に影響が出てしまいます。 JavaScriptでは一般的なメソッドに対しては、オブジェクトのプロトタイプに結びつける方がコンストラクタの呼び出し(オブジェクトの作成ごと)にメソッドの再代入を防いでパフォーマンスが上がるみたいです。 下記の例ではクロージャとプロトタイプで同じような処理を大量に繰り返したときの速度を測って見ましたが、確かにプロトタイプの方が早い印象を持ちました。(結果がブレたりしますが)

closure.ts

export function setupClosure(element: HTMLElement) {
  let greet = "私はクロージャ、名前はまだない";
  const setClosure = () => {
    element.innerHTML = `挨拶:${greet}`;
    const closureTimer = document.createElement("div");
    const closureFunc = closureObject();
    const closureStart = performance.now();
    [...Array(10000).keys()].forEach(() => {
      closureFunc();
    });
    const closureEnd = performance.now();
    closureTimer.textContent = `closure: ${closureEnd - closureStart} ms`;

    const prototypeTimer = document.createElement("div");
    const prototype = new (prototypeObject as any)();
    const prototypeStart = performance.now();
    [...Array(10000).keys()].forEach(() => {
      prototype.getMessage();
    });
    const prototypeEnd = performance.now();
    prototypeTimer.textContent = `prototype: ${
      prototypeEnd - prototypeStart
    } ms`;

    element.append(closureTimer, prototypeTimer);
  };

  function closureObject() {
    const getMassage = function () {
      console.log("hello closure");
    };

    return getMassage;
  }

  function prototypeObject() {}
  prototypeObject.prototype.getMessage = function () {
    console.log("hello prototype");
  };

  setClosure();
}

では最後に、クロージャを使って遊んでみようと思います!
今回はクリスマス間近ということで、サンタさんとトナカイさんに褒めあってもらって照れたら負けゲームを開催してみました。 クロージャを使って色々状態を持たせられるのですが、今回は簡単に「照れ度」の管理だけお願いしました。

ゲームを行うにあたり、色味もあった方が盛り上がるかと思いましたので、取りあえずtailwindだけプロジェクトの追加しました。

コードは下記になります(ちょっと長いため、閲覧注意)

closure.ts

export function setupClosure(element: HTMLElement) {
  const topArea = document.createElement("div");
  const title = document.createElement("div");
  const battleButton = document.createElement("button");
  const resetButton = document.createElement("button");
  const battleArea = document.createElement("div");

  const santaArea = document.createElement("div");
  const santaAreaTop = document.createElement("div");
  const santaAreaTitle = document.createElement("div");
  const santaAreaDamage = document.createElement("div");
  const santaProgressBar = document.createElement("div");
  const santaProgressBarDetail = document.createElement("div");
  const santaCompliment = document.createElement("div");
  const santaWinLoseWrapper = document.createElement("div");
  const santaWinLose = document.createElement("div");

  const reindeerArea = document.createElement("div");
  const reindeerAreaTop = document.createElement("div");
  const reindeerAreaTitle = document.createElement("div");
  const reindeerAreaDamage = document.createElement("div");
  const reindeerProgressBar = document.createElement("div");
  const reindeerProgressBarDetail = document.createElement("div");
  const reindeerCompliment = document.createElement("div");
  const reindeerWinLoseWrapper = document.createElement("div");
  const reindeerWinLose = document.createElement("div");

  const buttonClasses = [
    "text-white",
    "text-sm",
    "font-bold",
    "py-1",
    "px-2",
    "rounded",
    "ml-6",
    "disabled:bg-slate-100",
    "disabled:text-slate-500",
  ];
  const battleButtonClasses = buttonClasses.concat([
    "bg-green-500",
    "hover:bg-green-700",
  ]);
  const resetButtonClasses = buttonClasses.concat([
    "bg-slate-500",
    "hover:bg-slate-700",
  ]);

  const progressBarClasses = [
    "flex",
    "w-5/6",
    "bg-gray-200",
    "rounded-full",
    "h-2.5",
    "mb-4",
  ];
  const progressBarDetailClasses = [
    "bg-gradient-to-r",
    "from-pink-100",
    "to-pink-500",
    "h-2.5",
    "rounded-full",
  ];
  const winLoseWrapperClasses = [
    "place-content-center",
    "grid",
    "text-2xl",
    "mt-4",
    "italic",
  ];
  const santaDamageWords = [
    "よっ、イケおじ!",
    "ヒゲ伸びた?",
    "今年のひっげは一段と白いね!",
    "ちょっと痩せた?",
    "まさにお金と夢を持ち合わせた存在!",
    "まさにサンタ・クロース!!!",
    "あなたと組めて幸せです",
    "今年も子供たちに笑顔を届けるつもりかい?",
    "どれだけ世界中に笑顔を振り撒くつもりだ",
    "お前がNo1だ",
  ];
  const santaDamageList: { id: number; damageWord: string }[] = [];
  santaDamageWords.forEach((val, index) => {
    santaDamageList.push({ id: index + 1, damageWord: val });
  });

  const reindeerDamageWords = [
    "相変わらず良い毛皮!",
    "今年も元気いっぱいですな!",
    "たくましき肉体",
    "ちょっと痩せた?",
    "足早いね",
    "今年の毛皮もあったかそう",
    "あなたと組めて幸せです",
    "世界中のトナカイに幸せを!",
    "なぜ君は空を飛べるのか",
    "暗い夜道はピッカピカの〜、お前の鼻が〜、役にたっつのさ!",
  ];
  const reindeerDamageList: { id: number; damageWord: string }[] = [];
  reindeerDamageWords.forEach((val, index) => {
    reindeerDamageList.push({ id: index + 1, damageWord: val });
  });

  let battleIntervalId = 0;
  let isEndBattle = false;

  // closure
  function battleState() {
    let embarrassed: number = 0;
    const addEmbarrassed = (damage: number) => {
      embarrassed = embarrassed + damage;
    };
    const getEmbarrassed = () => {
      return embarrassed;
    };
    const resetEmbarrassed = () => {
      embarrassed = 0;
    };

    return { addEmbarrassed, getEmbarrassed, resetEmbarrassed };
  }
  const santaBattleState = battleState();
  const reindeerBattleState = battleState();

  const setClosure = () => {
    // 全体的なレイアウト セット
    setOverall();
    // サンタさんエリア セット
    setSantaArea();
    // トナカイさんエリア セット
    setReindeerArea();
    // バトルエリア セット
    setBattleArea();
  };

  const setOverall = () => {
    // wrapper
    element.classList.add("p-4");
    topArea.classList.add("flex");
    // title
    title.textContent = ":star::star:聖夜の、照れたら負けゲームっ:star::star:";
    title.classList.add("font-bold");
    // battleButton
    battleButtonClasses.forEach((val) => {
      battleButton.classList.add(val);
    });
    battleButton.textContent = "バトル開始";
    // battleButton
    resetButtonClasses.forEach((val) => {
      resetButton.classList.add(val);
    });
    resetButton.textContent = "リセット";

    topArea.append(title, battleButton, resetButton);
    element.append(topArea);
  };

  const setSantaArea = () => {
    santaAreaTop.style.display = "flex";
    santaAreaTitle.textContent = "サンタさん";
    santaAreaDamage.textContent = "照れ度: 0%";
    santaAreaDamage.classList.add("ml-4");
    santaArea.style.width = "50%";
    santaAreaTop.append(santaAreaTitle, santaAreaDamage);
    santaArea.append(santaAreaTop);
    progressBarClasses.forEach((val) => {
      santaProgressBar.classList.add(val);
    });
    progressBarDetailClasses.forEach((val) => {
      santaProgressBarDetail.classList.add(val);
    });
    santaProgressBarDetail.style.width = "0%";
    santaProgressBar.append(santaProgressBarDetail);
    santaArea.append(santaProgressBar);
    santaCompliment.textContent = ":santa::";
    santaArea.append(santaCompliment);
    winLoseWrapperClasses.forEach((val) => {
      santaWinLoseWrapper.classList.add(val);
    });
    santaWinLoseWrapper.append(santaWinLose);
    santaArea.append(santaWinLoseWrapper);
  };

  const setReindeerArea = () => {
    reindeerAreaTop.style.display = "flex";
    reindeerAreaTitle.textContent = "トナカイさん";
    reindeerAreaDamage.textContent = "照れ度: 0%";
    reindeerAreaDamage.classList.add("ml-4");
    reindeerArea.style.width = "50%";
    reindeerAreaTop.append(reindeerAreaTitle, reindeerAreaDamage);
    reindeerArea.append(reindeerAreaTop);
    progressBarClasses.forEach((val) => {
      reindeerProgressBar.classList.add(val);
    });
    progressBarDetailClasses.forEach((val) => {
      reindeerProgressBarDetail.classList.add(val);
    });
    reindeerProgressBarDetail.style.width = "0%";
    reindeerProgressBar.append(reindeerProgressBarDetail);
    reindeerArea.append(reindeerProgressBar);
    reindeerCompliment.textContent = ":deer::";
    reindeerArea.append(reindeerCompliment);
    winLoseWrapperClasses.forEach((val) => {
      reindeerWinLoseWrapper.classList.add(val);
    });
    reindeerWinLoseWrapper.append(reindeerWinLose);
    reindeerArea.append(reindeerWinLoseWrapper);
  };

  const setBattleArea = () => {
    // バトルエリアに追加
    battleArea.classList.add("flex");
    battleArea.classList.add("p-4");
    battleArea.append(santaArea, reindeerArea);

    // バトルエリアに追加
    element.append(battleArea);
  };

  const attackSanta = () => {
    const damageVal = Math.floor(Math.random() * 10) + 1;
    const target = reindeerDamageList.find((e) => e.id === damageVal);
    santaCompliment.textContent = `:santa:: ${target?.damageWord}`;
    reindeerBattleState.addEmbarrassed(damageVal);
    reflectDamage();
  };
  const attackReindeer = () => {
    const damageVal = Math.floor(Math.random() * 10) + 1;
    const target = santaDamageList.find((e) => e.id === damageVal);
    reindeerCompliment.textContent = `:deer:: ${target?.damageWord}`;
    santaBattleState.addEmbarrassed(damageVal);
    reflectDamage();
  };

  const reflectDamage = () => {
    const santaDamage = santaBattleState.getEmbarrassed();
    santaAreaDamage.textContent = `
      照れ度: ${santaDamage}%
    `;
    santaProgressBarDetail.style.width = `${santaDamage}%`;

    const reindeerDamage = reindeerBattleState.getEmbarrassed();
    reindeerAreaDamage.textContent = `
      照れ度: ${reindeerDamage}%
    `;
    reindeerProgressBarDetail.style.width = `${reindeerDamage}%`;
  };

  const resetGame = () => {
    isEndBattle = false;
    battleButton.disabled = false;
    santaBattleState.resetEmbarrassed();
    reindeerBattleState.resetEmbarrassed();
    santaCompliment.textContent = `:santa::`;
    reindeerCompliment.textContent = `:deer::`;
    santaWinLose.textContent = "";
    reindeerWinLose.textContent = "";
    santaWinLose.classList.remove(...santaWinLose.classList);
    reindeerWinLose.classList.remove(...reindeerWinLose.classList);
    reflectDamage();
  };

  battleButton.onclick = () => {
    battleButton.disabled = true;
    resetButton.disabled = true;
    battleStart();
  };

  const battleStart = () => {
    battleIntervalId = setInterval(() => {
      if (isEndBattle) return;
      if (
        santaBattleState.getEmbarrassed() >= 100 ||
        reindeerBattleState.getEmbarrassed() >= 100
      ) {
        isEndBattle = true;
        clearInterval(battleIntervalId);
        if (
          santaBattleState.getEmbarrassed() >= 100 &&
          reindeerBattleState.getEmbarrassed() >= 100
        ) {
          santaWinLose.textContent = "Draw Game";
          reindeerWinLose.textContent = "Draw Game!";
          santaWinLose.classList.add("text-slate-500");
          reindeerWinLose.classList.add("text-slate-500");
        } else if (santaBattleState.getEmbarrassed() >= 100) {
          santaWinLose.textContent = "Your Lose!";
          reindeerWinLose.textContent = "Your Win!";
          santaWinLose.classList.add("text-indigo-900");
          reindeerWinLose.classList.add("text-rose-500");
        } else {
          santaWinLose.textContent = "Your Win!";
          reindeerWinLose.textContent = "Your Lose!";
          santaWinLose.classList.add("text-rose-500");
          reindeerWinLose.classList.add("text-indigo-900");
        }
        resetButton.disabled = false;
        return;
      }
      attackSanta();
      attackReindeer();
    }, 300);
  };

  resetButton.onclick = () => {
    resetGame();
  };

  setClosure();
}

画面ができました。楽しいゲームになりそうです!

バトル開始を押すとお互い褒めあって照れ度が上がっていってます。
先ほどでも触れた通り、照れ度の状態管理にクロージャが使われてます!
複数の内部変数を返却しても、ちゃんと各々が意図通りに内部変数を制御してくれてますね。
照れ度以外も全体的にクロージャに寄せてコードの冗長性を排除できそうですが、突貫作ったので工数が足りず。。
(リファクタ案件で別途チケット切らせてください)

バトルが完了すると状態に応じて結果が出力され、無事完了しました🎉

結果としてサンタさんとトナカイさんはもっと仲良くなれたかと思います!(きっと)

まとめ

総括に入りますが、今回クロージャで遊んでみようとすると色々JavaScriptの歴史が垣間見えました!(ほんの一部ですが)
現在フロントエンドのライブラリやフレームワークが進歩し発展著しいですが、状態管理などでクロージャが利用されているケースも多々あり気をつけないと泥沼にハマってしまうこともあるかと思います。
例えばReactでお馴染みのHooksでもクロージャが使われていたりします。
https://ja.legacy.reactjs.org/docs/hooks-effect.html
Hooksで状態管理している際にstale-closure(古いクロージャ)が悪さをすることもあります。 特に非同期系の処理と画面のレンダリング速度の兼ね合いでstale-closure問題を目の当たりにされた方々も多いのではないでしょうか。
(回避策もちゃんと用意してくれていますが、setXXX(alwaysActualStateValue => newStateValue)など)
また、vueでもHooksとComposition APIの比較でstale-closureの話題が取り上げていたりします。
https://ja.vuejs.org/guide/extras/composition-api-faq
solidjsでもクロージャを不用意の作らないことが説明されていたりします。
(solidjsパフォーマンス高いみたいですよね、あまり使ったことないですが少し気になってます)
https://www.solidjs.com/docs/latest
今回はJavaScript観点でクロージャを見てきましたが、他のプログラミング言語も含めて古くからある概念なので、今の技術の礎として歴史も含めて色々勉強になりました!(まだ奥は深そうですが)

最後に

最後まで読んでくださりありがとうございます! 弊社ではエンジニアを募集しております。 ご興味がありましたらカジュアル面談も可能ですので、下記リンクより是非ご応募ください!
iimon採用サイト / Wantedly / Green

次回のアドベントカレンダーの記事はmarumaruoさんです!
どんな記事が出てくるかお楽しみに!!

参考

developer.mozilla.org ja.vite.dev ja.legacy.reactjs.org ja.vuejs.org https://www.solidjs.com/docs/latest