先记住这个答案
外面拿到的是 Promise,不是值。async 函数调用后会立刻返回一个新的 Promise,函数体里 return 的值会成为这个 Promise 的兑现值。如果 return 的本身就是一个 Promise,外层 Promise 会跟随它的状态,不会出现 Promise 套 Promise。要拿到里面的值,只能在另一个 async 函数里 await,或者调用 .then()。
- async 函数的返回值一定是 Promise
return 42等于返回一个以 42 兑现的 Promisereturn另一个 Promise 会被展开,不会套两层- 返回的是新 Promise,和
return的那个不是同一个对象
先看代码和输出
很多人刚接触 async 时会写 const data = getData(),然后发现 data 打印出来是个 Promise。下面把这个现象放大看。
return 42 之后外面拿到了什么JavaScriptasync function getNum() {
return 42
}
const result = getNum()
console.log(result instanceof Promise) // true
console.log(result === 42) // false
result.then((v) => console.log('then 拿到', v))
console.log('同步结束')查看输出与解释
true
false
同步结束
then 拿到 42getNum() 一调用就返回了,返回的是 Promise。值要通过 .then() 拿,而 .then() 的回调是微任务,排在同步代码之后,所以 同步结束 先打印。
可以把 async 函数看成这样一层包装:function getNum() { return new Promise((resolve) => resolve(42)) }。这个写法只用来理解「为什么是 Promise」,它表达不了 await 的暂停,也没有自动捕获函数体里的 throw。
return 一个 Promise 会怎样
实际项目里 async 函数经常直接 return request(),这时返回的本身就是 Promise。外面不会拿到 Promise 套 Promise,而是被展开了。
const inner = Promise.resolve('用户数据')
async function getUser() {
return inner // return 一个已有的 Promise
}
const outer = getUser()
console.log(outer === inner) // false,引擎新建了一个
console.log(Promise.resolve(inner) === inner) // true,原样返回
outer.then((v) => console.log('拿到', v, typeof v))查看输出与解释
false
true
拿到 用户数据 stringouter 是 async 函数自己新建的 Promise,它用 inner 去 resolve,于是跟随 inner 的状态,最后拿到的是字符串,不是 Promise。这和 Promise.resolve 不同:Promise.resolve 收到原生 Promise 会原样返回,async 函数永远新建。
- thenable 也会被展开
- 展开规则不只认原生 Promise。只要返回的对象有
then方法(这类对象叫 thenable),引擎就会调用它的then来取值。比如return { then(resolve) { resolve(1) } },外面拿到的是 1。老项目里 jQuery 的 Deferred 能直接await,靠的就是这条规则。反过来,如果你的业务对象恰好有个叫then的方法,从 async 函数返回它就会出怪问题。
容易答错的地方
- 以为
await能让函数「变成同步」,返回值就是普通值 await只是暂停当前这个 async 函数内部的执行,函数对外依然是立刻返回 Promise。调用方如果不是 async 函数,不能await,只能用.then()。所以 async 具有传染性,一个底层函数改成 async,调用链上用到它结果的函数通常都要跟着改。- 以为
return一个 rejected Promise 会被当成普通值 return Promise.reject(err)不会让外面拿到一个「装着失败的 Promise」作为值,外层 Promise 会直接变成 rejected,效果和throw err一样。区别只在于写在try里时,throw会被本函数的catch接住,return出去的 rejected Promise 不会。- 以为返回的 Promise 就是
return的那个 - 示例里已经看到
outer === inner是false。如果你在函数里给inner挂了自定义属性,或者拿返回值和缓存里的 Promise 做全等比较,都会对不上。
面试官还会怎么问?
async 函数没写 return 会返回什么?
返回一个以 undefined 兑现的 Promise。所以 await 一个没有 return 的 async 函数,拿到的是 undefined。
TypeScript 里 async 函数的返回类型怎么写?
必须写成 Promise<T>,比如 async function getNum(): Promise<number>。写成 number 会直接报错,因为 async 函数的返回值一定是 Promise。
能不能在普通的同步代码里拿到 async 函数的结果?
不能。Promise 的值要等微任务阶段才可用,同步代码跑完之前拿不到。在 ES Module 里可以用顶层 await,模块本身会等这个 Promise 完成,但这是把整个模块变成了异步,并不是把 Promise 变成了同步。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。