「一度だけ実行」ユーティリティが欲しくなる場面
業務コードを書いていると、
「この処理は絶対に一回だけ動いてほしい」
という場面がよく出てきます。
初期化処理(同じ初期化を二回やると壊れる)
イベント登録(同じイベントを二重登録したくない)
課金処理(同じボタンを連打されても一回分だけにしたい)
ログ送信(同じエラーを何度も送信したくない)
こういうときに役立つのが「一度だけ実行」ユーティリティです。
“何度呼ばれても、実際に中身が動くのは一回だけ”という関数を作る小さな仕組みです。
基本形:一度だけ実行する関数を作る 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 関数の中からだけ参照されます。
この「外側の変数を内側の関数が覚えている」仕組みをクロージャと言います。
流れを言葉で追うとこうです。
once(fn)を呼ぶと、called = falseとresultが作られるwrapped関数が返される(ここではまだfnは実行されていない)wrapped()が初めて呼ばれたとき、calledが false なのでfn(...args)を実行し、called = trueにする- 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 を使いたい場合
fn が async 関数の場合でも、基本の 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(); // 二重登録されない
JavaScriptonce を挟むことで、「どこから呼ばれても安全」という設計にできます。
小さな練習で感覚をつかむ
次のような関数を自分で書いて、once を適用してみてください。
function logTime() {
console.log("実行時刻:", new Date().toISOString());
}
JavaScriptこれを
const logTimeOnce = once(logTime);
logTimeOnce();
logTimeOnce();
logTimeOnce();
JavaScriptと何度か呼んでみて、「本当に一回しかログが出ない」ことを確認してみてください。
そのうえで、「自分のプロジェクトの中で“本当は一回だけにしたい処理”ってどこだろう?」と探してみると、once がどれだけ業務コードと相性がいいか、実感として見えてきます。
