17370845950

如何让一个异步函数既能被 await 也能直接调用
异步函数默认返回Promise,支持await;若需同步取值,应通过缓存机制(如闭包缓存Promise结果)或自定义Promise子类实现,而非强行同步等待。

一个异步函数默认返回 Promise,所以它天然支持 await;但“直接调用”(即不加 await)时,你拿到的是 Promise 对象本身,不是最终值——这不是“不能直接调用”,而是调用后行为不同。真正想问的通常是:如何让一个函数既能 await fn() 获取结果,又能在同步上下文中像普通函数一样使用(比如 fn() 直接返回值)?

核心思路:返回一个“可 await 也可同步取值”的对象

JavaScript 中无法让一个函数同时是同步的又返回 Promise,但可以通过封装返回一个自定义对象,让它:

  • 作为 Promise 被 await(实现 thencatch 等)
  • 提供同步读取结果的方法(如 .value.unwrap()),但需确保调用时 Promise 已完成
  • 或更实用的方式:用 async 函数 + 包装器,配合状态缓存(适合幂等、可缓存的场景)

方案一:返回带 .then 的 Promise,并缓存结果(推荐用于初始化/配置类场景)

适用于执行一次、结果可复用的异步操作(如加载配置、连接数据库)。通过闭包缓存 Promise 和最终值:

function createAsyncValue(asyncFn) {
  let promise = null;
  let result = undefined;
  let error = undefined;
  let resolved = false;

  return function() {
    if (!promise) {
      promise = asyncFn().then(
        (val) => { result = val; resolved = true; return val; },
        (err) => { error = err; throw err; }
      );
    }
    // 返回同一个 Promise,支持 await
    const p = promise;
    // 扩展 Promise 实例,添加同步访问能力(仅当已完成)
    if (resolved) {
      p.value = result;
      p.unwrap = () => result;
      p.isResolved = true;
    } else {
      p.isResolved = false;
      p.unwrap = () => { throw new Error('Promise not settled yet'); };
    }
    return p;
  };
}

// 使用示例
const fetchUser = createAsyncValue(() => fetch('/api/user').then(r => r.json()));

// ✅ 可 await
const user = await fetchUser();

// ✅ 也可直接调用,后续调用能同步取值(前提是已 resolve 过)
if (fetchUser().isResolved) {
  console.log(fetchUser().value); // 不再发起请求,直接拿缓存值
}

方案二:返回 Promise 子类(高级,需谨慎)

继承 Promise,重写 then 并添加同步属性(如 .value)。但注意:Promise 构造器内部状态不可直接暴露,必须靠 .then 回调捕获结果,因此 .value 只能在 resolve 后安全读取:

class AsyncValue extends Promise {
  constructor(executor) {
    let _resolve, _reject;
    super((resolve, reject) => {
      _resolve = resolve;
      _reject = reject;
      executor(resolve, reject);
    });

    this._value = undefined;
    this._error = undefined;
    this._settled = false;

    const originalThen = this.then.bind(this);
    this.then = (onFulfilled, onRejected) => {
      const wr

appedFulfilled = (val) => { this._value = val; this._settled = true; return onFulfilled?.(val); }; const wrappedRejected = (err) => { this._error = err; this._settled = true; return onRejected?.(err); }; return originalThen(wrappedFulfilled, wrappedRejected); }; Object.defineProperty(this, 'value', { get() { if (!this._settled) throw new Error('Not settled yet'); if (this._error) throw this._error; return this._value; } }); } } // 创建函数 function asyncFn() { return new AsyncValue((resolve) => setTimeout(() => resolve('done'), 100) ); } // ✅ await 正常工作 console.log(await asyncFn()); // 'done' // ✅ 同步取值(仅在 resolve 后) const p = asyncFn(); setTimeout(() => { console.log(p.value); // 'done' —— 成功读取 }, 200);

方案三:不推荐的做法——试图“同步等待”

不要用 Atomics.waitwhile(!done)eval(`await ${...}`) 等方式强行同步阻塞。这会冻结主线程、破坏异步模型,违背 JavaScript 设计原则,在浏览器中尤其危险。

如果你的业务逻辑**必须同步执行**,说明设计上应重新评估:是否本就不该是异步?能否把异步前置(如启动时预加载),后续全部走同步路径?

本质上,JavaScript 的异步本质决定了“真正同步取值”和“原生 await 支持”无法在单个裸函数上无缝共存。最自然、健壮的做法是:明确区分场景——该 await 的地方就 await,需要同步值的地方,确保它来自已 resolve 的缓存或初始化完成的状态。