JavaScript Tips | 基本・共通ユーティリティ:汎用 – 一度だけ実行

JavaScript JavaScript
スポンサーリンク

「一度だけ実行」ユーティリティが欲しくなる場面

業務コードを書いていると、
「この処理は絶対に一回だけ動いてほしい」
という場面がよく出てきます。

初期化処理(同じ初期化を二回やると壊れる)
イベント登録(同じイベントを二重登録したくない)
課金処理(同じボタンを連打されても一回分だけにしたい)
ログ送信(同じエラーを何度も送信したくない)

こういうときに役立つのが「一度だけ実行」ユーティリティです。
“何度呼ばれても、実際に中身が動くのは一回だけ”という関数を作る小さな仕組みです。


基本形:一度だけ実行する関数を作る once ユーティリティ

まずはコードを見てみる

一番シンプルな「一度だけ実行」ユーティリティはこんな形です。

function once(fn) {
  let called = false;
  let result;

  return function wrapped(...args) {
    if (!called) {
      called = true;
      result = fn(...args);
    }
    return result;
  };
}
JavaScript

この once は、「元の関数 fn を受け取って、“一度だけ実行される版”を返す関数」です。

使い方はこうなります。

function init() {
  console.log("初期化しました");
}

const initOnce = once(init);

initOnce(); // 「初期化しました」と出る
initOnce(); // 何も出ない
initOnce(); // 何も出ない
JavaScript

何度 initOnce() を呼んでも、init の中身が実行されるのは最初の一回だけです。


仕組みをゆっくり分解して理解する

状態を閉じ込める「クロージャ」がポイント

once の中で重要なのは、この二つの変数です。

let called = false;
let result;
JavaScript

これらは once が呼ばれたときに一度だけ作られ、その後は wrapped 関数の中からだけ参照されます。
この「外側の変数を内側の関数が覚えている」仕組みをクロージャと言います。

流れを言葉で追うとこうです。

  1. once(fn) を呼ぶと、called = falseresult が作られる
  2. wrapped 関数が返される(ここではまだ fn は実行されていない)
  3. wrapped() が初めて呼ばれたとき、called が false なので fn(...args) を実行し、called = true にする
  4. 2回目以降に wrapped() が呼ばれても、called は true のままなので、中身は実行されず、前回の result だけ返す

「一度実行したら、二度と実行しない」という状態を、クロージャで覚え続けているのがポイントです。


戻り値をキャッシュする意味

「一度だけ実行+結果の再利用」というパターン

once の中で result を保存しているのは、
「一度だけ実行した結果を、二回目以降も返したい」からです。

function createConfig() {
  console.log("設定を計算中…");
  return { env: "prod" };
}

const getConfigOnce = once(createConfig);

const c1 = getConfigOnce(); // 「設定を計算中…」と出る、c1 は { env: "prod" }
const c2 = getConfigOnce(); // 何も出ない、c2 も { env: "prod" }
JavaScript

ここでの重要ポイントは二つです。

一度だけ重い処理を実行して、結果をキャッシュしておける
呼び出し側は「普通の関数」と同じ感覚で何度でも呼べる

「初期化」「設定生成」「接続確立」など、
“一回やれば十分で、結果は何度も使いたい”という処理にとても相性がいいです。


引数付きの once をどう扱うか

「最初の引数だけを採用する」という割り切り

once...args を受け取っていますが、
実際には「最初に呼ばれたときの引数だけを使う」ことになります。

function greet(name) {
  console.log(`こんにちは、${name} さん`);
}

const greetOnce = once(greet);

greetOnce("太郎"); // こんにちは、太郎 さん
greetOnce("花子"); // 何も起きない
JavaScript

二回目以降の引数は無視されます。
これは「一度だけ実行」という性質上、自然な挙動です。

もし「引数によって結果を変えたい」なら、
once ではなく「キャッシュ付き関数(メモ化)」の方が適しています。
once はあくまで「この処理自体を一回だけ許可する」ためのユーティリティだと考えてください。


非同期処理版の「一度だけ実行」

async 関数にも once を使いたい場合

fnasync 関数の場合でも、基本の once はそのまま使えます。
result に Promise が入るだけです。

async function fetchConfig() {
  console.log("サーバーから設定取得中…");
  const res = await fetch("/config");
  return res.json();
}

const fetchConfigOnce = once(fetchConfig);

await fetchConfigOnce(); // サーバーにリクエストが飛ぶ
await fetchConfigOnce(); // 2回目以降は同じ Promise の結果を待つだけ
JavaScript

一度目の呼び出しで返された Promise が result に保存されるので、
二回目以降は同じ Promise を返すことになります。

これにより、「設定取得 API を何度も叩かない」「同じ初期化処理を二重に走らせない」といったことが簡単に実現できます。


実務での具体的な利用イメージ

初期化処理を一度だけにする

例えば、アプリ全体の初期化処理を考えます。

function setupApp() {
  console.log("アプリ初期化");
  // イベント登録、設定読み込みなど…
}

const setupAppOnce = once(setupApp);

// どこから呼ばれても OK
setupAppOnce();
setupAppOnce();
setupAppOnce();
JavaScript

どこかのモジュールがうっかり二回呼んでも、
実際に初期化が走るのは一回だけです。

イベント登録の二重登録防止

イベントリスナーの登録も、「一度だけ」にしたいことが多いです。

function registerGlobalHandlers() {
  window.addEventListener("resize", () => {
    console.log("リサイズ");
  });
}

const registerGlobalHandlersOnce = once(registerGlobalHandlers);

// どこかで
registerGlobalHandlersOnce();
// 別の場所で
registerGlobalHandlersOnce(); // 二重登録されない
JavaScript

once を挟むことで、「どこから呼ばれても安全」という設計にできます。


小さな練習で感覚をつかむ

次のような関数を自分で書いて、once を適用してみてください。

function logTime() {
  console.log("実行時刻:", new Date().toISOString());
}
JavaScript

これを

const logTimeOnce = once(logTime);

logTimeOnce();
logTimeOnce();
logTimeOnce();
JavaScript

と何度か呼んでみて、「本当に一回しかログが出ない」ことを確認してみてください。

そのうえで、「自分のプロジェクトの中で“本当は一回だけにしたい処理”ってどこだろう?」と探してみると、
once がどれだけ業務コードと相性がいいか、実感として見えてきます。

タイトルとURLをコピーしました