深入解析JavaScript Promise类型检测:从原理到who-is-actor库实战
1. 项目概述:从“演员是谁”到代码世界的身份识别
最近在GitHub上看到一个挺有意思的项目,叫who-is-actor,作者是Wscats。光看名字,你可能会以为这是个娱乐八卦工具,用来查某个明星是谁。但实际上,这是一个非常典型的、用于解决前端开发中一个特定“身份识别”问题的工具库。它的核心功能,是帮助开发者快速、准确地判断一个JavaScript对象是否是一个“演员”——这里说的“演员”,当然不是指好莱坞明星,而是指在异步编程中扮演重要角色的Promise对象。
为什么需要这样一个库?在现代前端开发中,异步操作无处不在。从发起一个网络请求(fetch),到读取一个本地文件,再到执行一个定时任务,我们大量依赖Promise来处理这些非阻塞操作。然而,在复杂的业务逻辑或封装通用工具函数时,我们常常会遇到一个场景:传入的参数可能是一个Promise,也可能是一个普通值。为了进行统一的后续处理,我们必须先判断它的“身份”。who-is-actor这个库,就是为解决这个判断问题而生的。它提供了一个简洁、可靠且经过充分测试的isPromise函数,让你无需重复造轮子,也无需担心自己写的判断逻辑有边缘 case 的漏洞。
我自己在开发一个数据聚合的SDK时就深有体会。我们需要从多个第三方API获取数据,这些API的调用函数有些返回Promise,有些在特定条件下会直接返回缓存的对象。在核心的聚合逻辑里,我必须确保所有数据源都能被await或通过.then()处理。最初我写了一个简单的val && typeof val.then === 'function'来判断,结果在遇到某些自定义的thenable对象(即拥有then方法的非标准Promise)时,程序行为变得诡异。后来我发现了who-is-actor这类专门处理此问题的工具,它帮我规避了这些隐藏的坑,让代码更加健壮。这个项目虽然小巧,但切中了前端开发中一个非常实际且高频的痛点,特别适合那些编写底层工具库、框架或需要处理多种输入类型的通用函数的开发者。
2. 核心原理:深入拆解一个健壮的isPromise是如何炼成的
一个看似简单的isPromise函数,要做得健壮、无懈可击,需要考虑的边界情况远比想象中多。who-is-actor的实现,正是对这些边界情况逐一审视后的成果。我们不能只满足于“它能用”,更要理解它“为什么这样写”,这样才能在即使不使用这个库的时候,也能写出同样健壮的代码。
2.1 基础认知:什么是 Promise/A+ 规范?
在深入代码之前,我们必须先明确判断标准。我们判断的是不是浏览器原生的Promise?不完全是。我们判断的是一个对象是否符合Promise/A+ 规范。这是社区公认的 Promise 标准。一个符合该规范的“Promise-like”对象(通常称为Thenable),最核心的特征就是:它是一个对象或函数,并且拥有一个符合规范的then方法。
所以,最直观的判断就是:obj && typeof obj.then === 'function'。这确实是判断Thenable的黄金准则,也是许多简易判断函数的起点。who-is-actor的基石也在于此。
2.2 层层防御:从基础判断到边界处理
然而,仅仅检查then方法的存在是远远不够的。在实际的 JavaScript 运行环境中,存在着各种“奇葩”的值和对象。一个健壮的实现必须像洋葱一样,一层层包裹上防御性代码。
第一层防御:处理非对象类型这是最先要过滤掉的。null,undefined, 数字,字符串,布尔值,symbol,这些显然不是 Promise。所以函数开头通常会有类似if (obj === null || typeof obj !== 'object' && typeof obj !== 'function') return false的逻辑。注意,这里检查的是object或function,因为函数在JS中也是对象,并且理论上一个函数如果定义了then方法,它也可以被当作 Thenable(虽然罕见)。
第二层防御:安全地访问then属性我们不能直接obj.then,因为obj可能是通过Object.create(null)创建的无原型对象,其then属性可能是一个getter,并且在读取时抛出错误。一个健壮的实现会使用try...catch或Object.prototype.hasOwnProperty/in操作符的变体来安全地检查属性是否存在。更常见的做法是使用Promise.resolve()的机制,我们稍后会讲到。
第三层防御:区分真正的 Thenable 与偶然拥有then的对象这是最微妙的一层。想象一下,你有一个普通的字典对象,它恰好有一个叫then的属性,其值是一个函数(比如一个叫做then的回调函数)。按照基础判断,它会被误认为是 Promise。如何避免? 一种高级的检查方式是:使用Promise.resolve()方法。Promise.resolve()的内部逻辑规范中,包含了对参数是否为 Thenable 的鉴别。如果参数是 Thenable,它会“展开”这个 Thenable;如果不是,它会用该参数创建一个已解决的 Promise。我们可以利用这个行为:
function isPromise(value) { try { // Promise.resolve 会立即返回,不会等待异步。 // 如果 value 是 thenable,Promise.resolve 会递归展开它。 // 这里比较返回的 promise 和原值,如果相同,说明 value 不是 thenable, // 因为 Promise.resolve(非thenable) 会创建一个新的Promise对象。 // 但实际上,更常见的做法是直接看 value 是否有标准的 Promise 特征。 // 社区更推崇另一种方法: return value instanceof Promise || ( value !== null && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function' ); } catch (e) { // 如果在读取 .then 时出错,那肯定不是合法的 Promise return false; } }实际上,who-is-actor很可能采用了类似上述,但更为严谨的变体。它可能不仅检查then的类型,还可能通过Object.prototype.toString.call(value)来检查[object Promise],这对于识别原生 Promise 尤其准确。同时,对于运行环境不支持原生Promise的情况(虽然现在极少见),它也会有回退方案。
注意:这里需要特别小心
async function。一个async function返回的是一个 Promise,但函数本身并不是 Promise。isPromise(async () => {})应该返回false,因为函数本身没有then方法。只有调用它之后返回的值才是 Promise。
2.3 与其他方案的对比
为了更清楚who-is-actor这类库的价值,我们对比一下常见的“野路子”判断方法:
| 判断方法 | 代码示例 | 优点 | 缺点与风险 |
|---|---|---|---|
instanceof | value instanceof Promise | 简单直接,对原生Promise准确 | 1. 无法识别来自其他iframe或realm的Promise。 2. 无法识别用户自定义的、符合Promise/A+规范但非 Promise构造函数的库(如Bluebird)。 |
检查then方法 | value && typeof value.then === 'function' | 符合Promise/A+规范,识别范围广 | 1. 可能误判拥有then方法的普通对象。2. 可能触发 then属性的getter异常。 |
Promise.resolve | Promise.resolve(value) === value | 非常严谨,符合语言规范 | 1. 性能开销相对较大(创建微任务)。 2. 对于已经是fulfilled状态的Promise,此等式可能成立,造成误判?实际上,规范中 Promise.resolve(p)总会返回p本身。这个方法主要用于检测“thenable”。 |
who-is-actor类库 | isPromise(value) | 集成以上优点:通常结合多种检查,安全、准确、有良好的浏览器和Node.js兼容性处理。 | 需要引入一个外部依赖。 |
从对比可以看出,一个生产级的isPromise函数需要融合多种策略。who-is-actor的价值就在于它已经帮你完成了这种融合与测试,你无需再担心跨环境、跨Realm、自定义Promise库等复杂情况。
3. 实战应用:在真实项目中引入与使用who-is-actor
了解了原理,我们来看看怎么用它。假设我们正在构建一个通用的数据加载器UniversalLoader,它需要处理同步数据源、异步Promise数据源,甚至可能是回调函数风格的数据源。我们的目标是将其统一为Promise接口。
3.1 安装与引入
首先,通过npm安装它:
npm install @wscats/who-is-actor # 或者使用 yarn yarn add @wscats/who-is-actor然后,在你的工具模块或业务代码中引入:
// ES Module import { isPromise } from '@wscats/who-is-actor'; // CommonJS const { isPromise } = require('@wscats/who-is-actor');这个库通常非常小巧,只导出核心的isPromise函数,可能还会有一个默认导出。你可以查看其package.json中的main和module入口来确认具体用法。
3.2 核心使用场景与代码示例
场景一:统一异步处理入口这是最经典的用法。在你的函数入口,判断输入是否为Promise,如果是,则等待其解决;如果不是,则将其包装为Promise。
import { isPromise } from '@wscats/who-is-actor'; /** * 通用数据处理器 * @param {any} input - 可能是普通值,也可能是Promise或Thenable * @returns {Promise<any>} 始终返回一个Promise,其值为处理后的结果 */ async function universalDataProcessor(input) { // 1. 使用 who-is-actor 进行安全判断 if (isPromise(input)) { // 输入本身就是异步的,等待它 try { const rawData = await input; return processRawData(rawData); } catch (error) { // 对Promise的拒绝进行统一错误处理 console.error('异步数据源失败:', error); throw new DataProcessError('Failed to fetch async data', { cause: error }); } } else { // 输入是同步值,直接处理 // 这里也返回Promise以保持接口一致 return Promise.resolve(processRawData(input)); } } // 使用示例 const result1 = await universalDataProcessor(fetch('/api/data')); // 处理Promise const result2 = await universalDataProcessor({ id: 1, name: 'sync' }); // 处理普通对象 const result3 = await universalDataProcessor(someThirdPartyLib.getData()); // 处理未知返回类型的库场景二:优化条件逻辑,避免不必要的awaitawait一个非Promise值虽然不会报错,但会产生微任务,有一定的性能开销。在极高性能敏感的场景下,我们可以利用判断来优化。
function highPerformanceMerge(config, asyncData) { // 如果 asyncData 已经是最终数据,直接合并 if (!isPromise(asyncData)) { return { ...config, ...asyncData }; } // 如果是Promise,则返回一个新的Promise来处理合并 return asyncData.then(data => ({ ...config, ...data })); } // 优化前(总是await): // async function merge() { return { ...config, ...(await asyncData) }; } // 优化后,当asyncData是同步值时,避免了整个函数变成async函数和产生微任务。场景三:在自定义Hook或Composable中(以Vue 3为例)在组合式函数中,我们经常需要处理可能是异步也可能是同步的响应式数据源。
// useAsyncState.js import { ref, unref, watchEffect } from 'vue'; import { isPromise } from '@wscats/who-is-actor'; export function useAsyncState(source) { const state = ref(null); const isLoading = ref(false); const error = ref(null); watchEffect(async () => { // 使用unref获取Vue响应式源的可能原始值 const rawSource = unref(source); error.value = null; if (isPromise(rawSource)) { isLoading.value = true; try { state.value = await rawSource; } catch (e) { error.value = e; } finally { isLoading.value = false; } } else { // 同步值直接赋值 state.value = rawSource; isLoading.value = false; } }); return { state, isLoading, error }; } // 在组件中使用 // 可以传入一个Promise const { state: userData } = useAsyncState(fetchUserApi()); // 也可以直接传入一个ref或普通值 const { state: staticConfig } = useAsyncState({ theme: 'dark' });3.3 与异步函数(async function)的特殊处理
这里有一个非常重要的细节需要强调。isPromise检测的是对象,而不是函数。一个async function本身是一个函数,调用它才会返回一个 Promise。
const asyncFn = async () => 'hello'; console.log(isPromise(asyncFn)); // false console.log(isPromise(asyncFn())); // true因此,如果你的API设计是允许传入一个返回Promise的函数(即“工厂函数”),那么你的判断逻辑需要有两步:
function handleInput(maybePromiseOrFactory) { if (typeof maybePromiseOrFactory === 'function') { // 如果是函数,先执行它得到结果 const result = maybePromiseOrFactory(); if (isPromise(result)) { return result.then(handleResult); } else { return handleResult(result); } } else if (isPromise(maybePromiseOrFactory)) { // 如果本身就是Promise return maybePromiseOrFactory.then(handleResult); } else { // 普通值 return handleResult(maybePromiseOrFactory); } }这种模式在诸如数据获取、配置加载等场景中非常常见,它给予了调用者最大的灵活性。
4. 源码解析与自定义实现启示
虽然直接使用who-is-actor是最高效安全的选择,但通过剖析这类库的源码,我们能学到很多防御性编程的技巧。我们可以尝试实现一个自己的、教学版本的isPromise,并对比其与生产级版本的差距。
4.1 一个“教科书式”的实现及其缺陷
我们先写一个基础版本:
function isPromiseSimple(value) { return ( value !== null && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function' ); }这个版本的问题:
value.then可能触发getter抛出错误。例如:Object.defineProperty({}, 'then', { get() { throw new Error('hacked!'); } })。- 无法区分来自不同“领域(Realm)”的Promise。在浏览器中,iframe有自己独立的全局对象,
instanceof检查会失败。 - 对非标准Thenable的兼容性。虽然检查
then方法符合A+规范,但某些极端情况可能需要更复杂的处理。
4.2 进阶实现:增加健壮性
我们针对上述问题1进行改进:
function isPromiseRobust(value) { if (value === null || (typeof value !== 'object' && typeof value !== 'function')) { return false; } // 关键改进:使用 try...catch 安全地访问 then 属性 let then; try { then = value.then; } catch (e) { // 如果连读取 then 属性都出错,那肯定不是合法的 Promise return false; } // 检查 then 是否为函数 return typeof then === 'function'; }这个版本解决了getter抛异常的问题。但它仍然无法完美解决Realm问题和一些极其特殊的边界情况(例如,修改了Object.prototype.then?虽然这很疯狂)。
4.3 生产级实现的思路
一个像who-is-actor这样的生产级库,可能会采取以下策略的组合拳:
优先使用
Promise.resolve行为检测(标准推荐):这是最符合语言规范的方法。Promise.resolve(value)在规范中,如果value是 thenable,会返回一个基于该 thenable 的新 Promise(实际上会尝试“跟随”这个 thenable)。但有一个关键特性:如果value已经是一个由当前全局环境的Promise构造函数创建的、状态已确定的Promise,Promise.resolve可能会直接返回它自己。不过,利用Promise.resolve进行纯粹的类型判断并不直接。更常见的生产实践是结合其他方法。使用
Object.prototype.toString进行精准类型识别:对于原生Promise,这是最准确的方法之一。Object.prototype.toString.call(value) === '[object Promise]'这个方法可以跨Realm工作(只要两个Realm都有标准的
Promise),并且不受instanceof限制。结合
instanceof检查以提升常见路径性能:对于同Realm的原生Promise,instanceof检查速度最快。最终的
thenable检查作为兜底:如果以上都没命中,再使用我们上面写的健壮版的thenable检查。
一个综合版本的伪代码可能如下:
function isPromiseProduction(value) { // 快速排除非对象/函数 if (value === null || (typeof value !== 'object' && typeof value !== 'function')) { return false; } // 1. 快速路径:检查原生Promise (通过toString) if (Object.prototype.toString.call(value) === '[object Promise]') { return true; } // 2. 如果环境有全局Promise,检查instanceof (同Realm下性能好) if (typeof Promise !== 'undefined' && value instanceof Promise) { return true; } // 3. 兜底:安全地检查thenable // 这里需要非常小心地处理thenable的检查,避免递归Promise.resolve导致死循环 // 一个安全的方法是检查是否存在 then 方法,并且 then 方法不是来自 Object.prototype (防止污染) try { const then = value.then; // 检查 then 是否是函数,并且确保这个 then 属性是对象自身的或原型链上非Object的 // 更严格的检查可以加上:`&& then !== Object.prototype.then` // 但通常 then 方法不会是继承自 Object.prototype,除非被恶意修改。 return typeof then === 'function'; } catch (e) { return false; } }实操心得:在真实项目中,除非有极强的控制欲和代码洁癖,否则我强烈建议直接使用像
who-is-actor这样经过社区考验的微库。自己实现一个完全健壮的版本所花费的测试和调试时间,远远超过引入一个依赖的成本。这些库通常小于1KB,且无其他依赖,对项目体积的影响微乎其微,带来的却是可靠性和心智负担的降低。
5. 生态对比与选型建议
who-is-actor并非孤例。在 npm 生态中,存在多个用于检测 Promise 的库。了解它们之间的差异,有助于我们在不同场景下做出最合适的选择。
5.1 主流is-promise类库对比
| 库名 | 周下载量 (约) | 大小 | 特点与实现倾向 |
|---|---|---|---|
is-promise | 数千万 | ~300B | 历史最悠久、最流行。实现非常简洁:obj !== null && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'。但它没有用try-catch包裹obj.then,在极端情况下可能不安全。 |
p-is-promise | 数百万 | ~300B | 来自知名开发者 Sindre Sorhus。实现与is-promise类似,同样简洁。社区认可度高,但基础实现上也未处理then的getter异常。 |
@wscats/who-is-actor | 相对较少 | 未知 | 从命名看更个性化。作为后起之秀,很可能吸收了前者的经验,可能在健壮性上做了更多增强(比如增加了安全访问、更严格的类型判断)。需要查看其源码确认。 |
is-thenable | 数十万 | ~200B | 顾名思义,专注于检查是否为Thenable,实现与上述类似。 |
核心结论:从实现原理上讲,这些库的核心逻辑大同小异,都围绕着“检查then方法”这一Promise/A+规范的核心。主要的区别在于:
- 健壮性细节:是否用
try-catch保护属性访问?是否考虑了Object.create(null)的对象? - 额外检查:是否结合了
instanceof或Object.prototype.toString.call来加速原生Promise的判断? - 包体积与树摇:是否以ES Module格式发布,便于打包工具优化。
- 维护活跃度:最近是否有更新,是否处理了新的边缘Case?
5.2 如何选择?
给你的选型建议:
追求极致可靠和现代浏览器支持:如果你非常关心健壮性,担心那些极端边缘案例,并且项目环境较新(支持ES6+),可以选择一个明确使用了
try-catch和Object.prototype.toString进行检查的库。你需要花几分钟阅读一下目标库的源码(通常就十几行)。who-is-actor如果在这方面有加强,会是一个好选择。追求最大兼容性和社区共识:如果你的项目需要支持非常古老的环境,或者你希望使用一个经过海量项目验证、几乎成为“标准”的库,那么
is-promise仍然是安全且普遍的选择。虽然它在理论上存在极小的风险,但在实际应用中,触发这个风险的条件非常苛刻,几乎可以忽略不计。许多大型框架(如Vue Router的早期版本)也使用了它。对包体积极度敏感:如果你在开发一个需要分发给前端的库,且对字节数锱铢必较,可以对比一下这些库的压缩后大小(通常都在300字节以下,差异极小)。也可以考虑自己内联那几行核心代码,但务必加上安全访问逻辑。
框架或工具链已内置:首先检查你使用的框架或工具链是否已经提供了类似工具。例如,Vue 3 的源码中就有
isPromise的工具函数;许多测试框架(如Jest)也有自己的实现。避免重复引入。
我个人在实际项目中的选择策略:对于大多数中大型商业项目,我会直接使用is-promise,因为它的普及度本身就是一种保障,出问题的概率极低,而且容易得到其他开发者的理解。如果是在编写一个极其基础、要求零风险的工具库,我可能会选择p-is-promise(因为Sindre Sorhus的库以高质量著称)或者仔细考察像who-is-actor这样可能更注重健壮性的新库。无论如何,阅读源码是做出明智选择的最佳途径。
6. 常见问题与排查实录
即使使用了成熟的库,在集成isPromise功能时,你仍可能会遇到一些意料之外的问题。下面是我在项目中实际遇到或看到过的一些案例。
6.1 问题一:为什么我的async function被判断为false?
问题描述:
import { isPromise } from '@wscats/who-is-actor'; const asyncFunc = async () => 'data'; console.log(isPromise(asyncFunc)); // 输出 false,与预期不符?原因与排查: 这是一个最常见的理解偏差。isPromise检测的是一个对象是否是 Promise(或Thenable)。一个async function在JavaScript中是一个函数。当你调用它时,它才会返回一个Promise。所以,检测函数本身返回false是正确的行为。
解决方案: 你需要检测的是函数的返回值,而不是函数本身。
function mightReturnPromise(fn) { const returnValue = fn(); // 或其他方式获取返回值 if (isPromise(returnValue)) { // 处理异步逻辑 return returnValue.then(handleData); } else { // 处理同步逻辑 return handleData(returnValue); } } // 或者,如果你接收的参数可能是函数也可能是值 function handleInput(input) { const value = typeof input === 'function' ? input() : input; if (isPromise(value)) { return value.then(process); } return process(value); }6.2 问题二:在Node.js回调风格函数中误判
问题描述: 在Node.js早期或某些API中,大量使用回调函数风格(Error-First Callback)。有时你会看到一些对象拥有.then方法,但这并不是Promise,而是某些ORM或工具为了兼容两种风格而添加的。
// 假设一个虚构的数据库查询对象 const fakeQueryResult = { then: function(onFulfilled, onRejected) { // 但它内部可能调用的是回调,而不是返回Promise this.exec((err, data) => { if (err) onRejected?.(err); else onFulfilled?.(data); }); // 注意:它可能没有返回一个Promise!这不符合Promise/A+规范。 }, exec: function(callback) { /* ... */ } }; console.log(isPromise(fakeQueryResult)); // 可能返回 true await fakeQueryResult; // 这里的行为可能无法预测或出错原因与排查:isPromise(或任何基于thenable的检查)会认为这是一个Promise,因为它有then方法。但如果这个对象的then方法实现不符合Promise/A+规范(例如,没有返回一个新的Promise),那么用await或.then()去处理它就会出问题。
解决方案:
- 查阅文档:首先确认这个库或API的设计意图。它是否真的提供了Promise接口?
- 使用
Promise.resolve进行标准化:最安全的方法是使用Promise.resolve()包裹它。Promise.resolve()的内部逻辑会处理非规范的Thenable,并总返回一个符合规范的Promise。
这样,即使// 无论input是什么,都将其转化为标准的Promise const standardPromise = Promise.resolve(input); // 然后放心地使用 await 或 .then const data = await standardPromise;input是一个不规范的Thenable,Promise.resolve也会尽力将其“消化”成一个标准Promise。这比单纯用isPromise判断更健壮。
6.3 问题三:在微前端或iframe环境中判断失败
问题描述: 主应用使用isPromise判断来自子应用(不同iframe或微前端沙箱)传递过来的对象,即使它是一个真正的Promise,判断也可能为false。
原因与排查: 这是因为instanceof检查在不同JavaScript执行环境(即不同的全局对象,不同的Realm)下会失效。如果库的实现中优先使用了value instanceof Promise这条快速路径,而该Promise来自另一个Realm,检查就会失败。
解决方案: 一个健壮的isPromise库应该能处理跨Realm的情况。它应该:
- 优先使用不依赖构造函数引用的方法,如
Object.prototype.toString.call(value) === '[object Promise]'。这个方法在不同Realm下,只要都是标准Promise,就会返回相同的字符串。 - 将
instanceof检查仅作为同Realm下的性能优化路径。 - 兜底的
thenable检查是跨Realm有效的。
如果你发现使用的库在跨Realm场景下有bug,可以考虑提Issue,或者换用另一个明确处理了此问题的库。在自行判断时,也应避免依赖instanceof。
6.4 性能考量与不必要的优化
有些开发者会担心isPromise函数被频繁调用(例如在渲染循环中)的性能影响。
实测与建议: 对于一个类似who-is-actor的实现,单次调用的开销是纳秒级的,通常可以忽略不计。绝大多数业务场景的性能瓶颈都不在这里。
真正的性能陷阱在于错误的使用模式:
// 反例:在循环或高频函数中创建临时Promise来检测 function badCheck(val) { return Promise.resolve(val) === val; // 这会产生一个Promise对象,开销更大 } // 正例:使用纯同步的类型检查 function goodCheck(val) { return isPromise(val); // 同步操作,无额外对象创建 }所以,请放心使用isPromise这类同步检查函数,它本身就是为高效而设计的。将优化重点放在算法复杂度、不必要的DOM操作、网络请求次数等更重要的地方。
7. 扩展思考:isPromise在工程化中的角色
who-is-actor这样的工具,其价值不仅仅在于提供了一个函数。它更体现了一种清晰的类型边界处理和接口契约思想,这对于构建健壮的软件系统至关重要。
7.1 作为“适配器”模式的关键零件
在系统设计中,我们经常需要整合不同来源的模块,这些模块的接口可能不一致。有的返回Callback,有的返回Promise,有的直接返回数据。我们可以利用isPromise构建一个通用的“适配器”层。
// 一个通用的适配器函数 function adaptToPromise(maybeAsyncThing) { // 情况1:已经是Promise,直接返回 if (isPromise(maybeAsyncThing)) { return maybeAsyncThing; } // 情况2:是Node.js风格的回调函数(假设约定最后一个参数是callback) if (typeof maybeAsyncThing === 'function' && maybeAsyncThing.length > 0) { return new Promise((resolve, reject) => { maybeAsyncThing((err, ...results) => { if (err) reject(err); else resolve(results.length === 1 ? results[0] : results); }); }); } // 情况3:是同步值,包装成Promise return Promise.resolve(maybeAsyncThing); } // 现在,你可以用统一的方式处理任何输入 async function process(input) { const standardized = await adaptToPromise(input); // ... 处理 standardized }这个适配器让你的核心业务逻辑只需要关心处理Promise,将接口差异性的处理隔离在边缘。
7.2 在TypeScript中增强类型安全
在TypeScript项目中,isPromise可以作为一个类型守卫,帮助编译器缩小类型范围,写出更安全的代码。
import { isPromise } from '@wscats/who-is-actor'; // 没有类型守卫,类型是模糊的 function handleUnsafe(input: unknown) { if (isPromise(input)) { // 在这里,TS不知道input是Promise,仍然认为是unknown // input.then(...) // 错误:Property 'then' does not exist on type 'unknown'. } } // 自定义一个类型守卫版本 function isPromiseTS<T>(value: unknown): value is Promise<T> { return isPromise(value); // 复用核心逻辑 } function handleSafe(input: unknown) { if (isPromiseTS<MyData>(input)) { // 现在,在这个块里,TS知道input是Promise<MyData> return input.then(data => data.id); // 安全,可以访问data的属性 } // 这里,TS知道input不是Promise console.log(input); }你可以将who-is-actor的运行时检查与TypeScript的类型系统结合,创建出既安全又灵活的工具函数。
7.3 测试策略:如何测试使用了isPromise的代码
当你编写一个依赖isPromise进行逻辑分叉的函数时,如何为它编写全面的单元测试?
测试要点:
- 同步值:测试传入
string,number,object,array,null,undefined等,确保它们被正确识别为非Promise,并走同步处理分支。 - 标准Promise:测试传入
new Promise(...),Promise.resolve(),Promise.reject(),async function的返回值,确保它们被识别为Promise,并走异步处理分支。 - 第三方Promise:测试传入其他库(如
Bluebird)构造的Promise,确保兼容性。 - 非标准Thenable:测试传入一个拥有
then方法的普通对象(不符合A+规范),观察函数行为是否符合预期(是抛出错误,还是被Promise.resolve消化)。 - 跨Realm Promise:如果环境允许,测试来自iframe或Web Worker的Promise。
// 使用Jest示例 import { myAsyncHandler } from './myHandler'; import { isPromise } from '@wscats/who-is-actor'; // 模拟一个非标准Thenable const nonStandardThenable = { then: (onSuccess) => { onSuccess('hacked'); return 'not a promise'; } }; describe('myAsyncHandler', () => { it('should handle sync value', async () => { const result = await myAsyncHandler(42); expect(result).toBe(84); // 假设处理逻辑是乘以2 }); it('should handle native promise', async () => { const promise = Promise.resolve(42); const result = await myAsyncHandler(promise); expect(result).toBe(84); }); it('should handle non-standard thenable (if supported)', async () => { // 注意:这里的行为取决于 myAsyncHandler 的内部实现。 // 如果它直接用 await,可能会出问题。 // 如果它用了 Promise.resolve 包裹,则可能正常工作。 // 测试的目的是验证我们代码的健壮性,而不是第三方对象。 const result = await myAsyncHandler(Promise.resolve(nonStandardThenable)); // 断言结果符合预期 }); });通过这样的测试,你不仅能验证isPromise是否工作,更能验证你整合了它之后的业务逻辑是否足够健壮,能够应对各种稀奇古怪的输入。这正是将who-is-actor这类工具的价值真正落到实处的关键一步。
