← 返回题目列表

如何手写 promisify?Node 风格回调如何转 Promise?

中等 第 22 / 27 题 更新于 2026/07/29
手写代码PromiseNode.js回调

简化版

promisify 用来把 Node 风格的回调函数转换成返回 Promise 的函数。Node 风格回调通常是 (err, data) => {},如果 err 存在就 reject,否则 resolve 结果。实现时要保留 this、透传参数、处理多结果场景,并确保回调只结算一次。

详细版

最基础实现是返回一个新函数,调用原函数时追加回调。

细节说明
错误优先第一个回调参数是错误
thisfn.call(this, ...) 保留调用者
参数原函数参数原样透传
多结果可 resolve 数组或只取第一个结果
function promisify<T = unknown>(fn: Function) {
  return function (this: unknown, ...args: unknown[]) {
    return new Promise<T>((resolve, reject) => {
      fn.call(this, ...args, (err: unknown, data: T) => {
        if (err) reject(err)
        else resolve(data)
      })
    })
  }
}

这题常见坑是忘记保留 this,导致对象方法被包装后内部上下文丢失。

完整版教学

一、Node 风格回调是什么

Node 早期大量 API 使用错误优先回调,也就是:

fs.readFile('a.txt', 'utf8', (err, data) => {
  if (err) return console.error(err)
  console.log(data)
})

第一个参数表示错误,后面的参数表示成功结果。

二、promisify 的核心思路

Promise 的结算方式是 resolvereject。所以 promisify 的本质是搭一座桥:回调收到错误就 reject,收到成功结果就 resolve。

function promisify(fn: Function) {
  return (...args: unknown[]) =>
    new Promise((resolve, reject) => {
      fn(...args, (err: unknown, result: unknown) => {
        err ? reject(err) : resolve(result)
      })
    })
}

这是最小版本,但面试里还要补上下文和边界。

三、为什么要保留 this

如果原函数是对象方法,它内部可能使用 this

const obj = {
  prefix: 'hi',
  get(cb: Function) {
    cb(null, this.prefix)
  }
}

包装后如果直接 fn(...args)this 会丢。更稳的写法是返回普通函数,并使用 fn.call(this, ...)

四、处理多个成功结果

有些回调不是只有一个成功值,例如 (err, stdout, stderr)。可以选择只 resolve 第一个,也可以提供 multiArgs 选项。

function promisifyMulti(fn: Function) {
  return function (this: unknown, ...args: unknown[]) {
    return new Promise((resolve, reject) => {
      fn.call(this, ...args, (err: unknown, ...values: unknown[]) => {
        if (err) reject(err)
        else resolve(values.length > 1 ? values : values[0])
      })
    })
  }
}

五、回调多次调用怎么办

Promise 只能结算一次。即使原函数错误地多次调用回调,后续的 resolve/reject 也不会改变状态。

不过工程上仍可加一个 called 标记,避免执行额外逻辑。

六、和 util.promisify 的关系

Node 内置 util.promisify 已经提供成熟实现,还支持自定义 util.promisify.custom。面试手写是为了考察你是否理解回调与 Promise 的转换机制。

能力手写基础版Node 内置版
错误优先回调支持支持
this 处理取决于实现调用方式仍需注意
自定义转换通常无支持
类型完善简单更成熟

七、常见误区与追问

  • 误区:promisify 可以包装任意回调。 它默认适合错误优先回调,不适合事件监听或多次触发的回调。
  • 误区:箭头函数返回包装函数更方便。 箭头函数没有自己的 this,包装对象方法时容易丢上下文。
  • 误区:回调多次调用会让 Promise 多次 resolve。 Promise 状态只会第一次结算。
  • 追问:如何处理多个成功参数? 可以 resolve 数组,或提供配置决定只取第一个。
  • 追问:事件监听能 promisify 吗? 单次事件可以封装成 Promise,多次事件更适合 Observable 或 EventEmitter。
  • 追问:如何保留 this? 返回普通函数,并用 fn.call(this, ...args, callback)

八、加强记忆

promisify 记成“回调出口改成 Promise 出口”。错误优先回调里有 err 就 reject,没有 err 就 resolve;真正拉开差距的是 this、多参数和不适用场景。