once是手动实现的高阶函数,通过闭包封装布尔状态确保内部函数仅执行一次并缓存返回值;支持同步与Promise异步场景,适用于全局初始化、惰性单例等,需配合模块系统避免重复定义。
要让某个初始化逻辑在全局范围内只执行一次,可以用 once 包装器 —— 它本质是闭包 + 状态标记,确保内部函数无论被调用多少次,实际逻辑仅运行首次。
理解 once 的核心机制
once 不是语言内置(如 JavaScript),而是手动实现的高阶函数:接收一个函数,返回一个新函数;后者内部用布尔变量记录是否已执行,首次调用时运行原函数并标记为“已执行”,后续调用直接返回缓存结果(或不执行)。
- 关键点:状态(如
called = false)必须封闭在闭包中,对外不可篡改 - 返回值通常为首次执行的结果,后续调用复用该值(适合初始化返回配置、实例等)
- 不适用于需每次获取最新值的场景(如实时 DOM 查询)
基础实现(JavaScript 示例)
以下是一个轻量、可复用的 once 函数:
<font color="#888"><pre class="brush:php;toolbar:false;">function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
result = fn.apply(this, args);
called = true;
}
return result;
};
}
使用方式:
<font color="#888"><pre class="brush:php;toolbar:false;">const initConfig = once(() => {
console.log('正在加载全局配置...');
return { theme: 'dark', apiBase: '/v1' };
});
initConfig(); // 输出日志,返回配置
initConfig(); // 无日志,直接返回上次结果
initConfig(); // 同上
处理异步初始化(Promise 场景)
若初始化逻辑是异步(如请求接口、加载脚本),once 需适配 Promise,避免重复触发请求:
<font color="#888"><pre class="brush:php;toolbar:false;">function onceAsync(fn) {
let promise;
return function(...args) {
if (!promise) {
promise = Promise.resolve().then(() => fn(...args));
}
return promise;
};
}
const loadUser = onceAsync(async () => {
const res = await fetch('/api/user');
return res.json();
});
loadUser().then(u => console.log(u)); // 发起请求
loadUser().then(u => console.log(u)); // 复用同一 Promise,不会重复请求
在模块/全局上下文中安全使用
为避免多个文件重复定义或污染全局,推荐结合模块系统封装:
- ESM 中导出
once工具函数,供各模块按需 import - 初始化逻辑单独写成函数,再用
once包装后导出(即“惰性单例”) - 例如:
export const getRouter = once(() => new Router()),首次导入时才创建实例
这样既保证全局唯一,又延迟执行、按需加载,兼顾性能与清晰性。










