人人都会AI编程

8.4 工具函数:util 模块

更新时间:2026-07-10

Node.js 的 util 模块提供了一系列实用函数,主要用来支持内部 API 的实现,但对开发者同样非常有用。它最常被用到的功能包括:将回调风格的函数转为 Promise、类型判断、继承、格式化等。掌握这些工具能让代码更简洁、更符合现代异步编程习惯。

8.4.1 util.promisify:将回调转为 Promise

在 Node.js 的早期,大部分异步 API 都采用“错误优先”的回调风格((err, result) => {})。虽然现在 fs.promisesdns.promises 等已经原生返回 Promise,但仍有大量老模块和自定义函数保留着回调接口。util.promisify 可以将遵循这种回调约定的函数转换为返回 Promise 的函数,省去手动包装的繁琐。

基本用法

const util = require('util');
const fs = require('fs');

// 原始回调风格
fs.readFile('/path/to/file', (err, data) => {
  if (err) throw err;
  console.log(data);
});

// promisify 转换
const readFileAsync = util.promisify(fs.readFile);

// 现在可以 async/await
(async () => {
  try {
    const data = await readFileAsync('/path/to/file');
    console.log(data);
  } catch (err) {
    console.error('读取失败', err);
  }
})();

util.promisify 的成功条件是:原函数最后一个参数必须是回调,并且回调的第一个参数为错误对象((err, result))。如果回调返回多个成功值(如 (err, value1, value2)),可以用 util.promisify{ multiArgs: true } 选项(已被符号 util.promisify.custom 替代,但在 Node.js 10+ 中不推荐使用 multiArgs)。更通用的做法是手动包装。

自定义类的 promisify

如果你的对象有自定义的异步方法,可以通过 Symbol.for('util.promisify.custom')(即 util.promisify.custom)定义其 promisified 版本:

const util = require('util');

class MyModule {
  fetchData(options, callback) {
    // 模拟异步操作
    setTimeout(() => callback(null, { data: 'result' }), 100);
  }

  // 定义 custom promisify
  [util.promisify.custom](options) {
    return new Promise((resolve, reject) => {
      this.fetchData(options, (err, result) => {
        if (err) reject(err);
        else resolve(result);
      });
    });
  }
}

const myMod = new MyModule();
const fetchAsync = util.promisify(myMod.fetchData).bind(myMod);
// 或者直接调用 myMod[util.promisify.custom]?.(options)

实际上,对于 Node.js 10+,你也可以直接使用 require('fs').promisesrequire('stream').promises(如果 API 支持)来避免大部分 promisify 的需要,但在处理遗留代码或第三方库时,util.promisify 依然非常有用。

8.4.2 类型判断:util.typesutil.isXxx

在 JavaScript 中,类型判断经常需要用 typeofinstanceof,但存在一些跨上下文的问题或对于内置对象的判断不够精细。util 模块提供了一套辅助函数,大多数已经弃用(如 util.isArrayutil.isRegExp),推荐使用 Array.isArrayinstanceof。但仍有一些难以替代的,特别是 util.types 子模块。

util.types 提供的细化判断

util.types 加入于 Node.js 10.0.0,可以精确判断 V8 内部类型和 JavaScript 类型:

const util = require('util');

// JavaScript 基本类型判断
util.types.isDate(new Date());          // true
util.types.isMap(new Map());            // true
util.types.isSet(new Set());            // true
util.types.isWeakMap(new WeakMap());    // true
util.types.isWeakSet(new WeakSet());    // true
util.types.isRegExp(/abc/);             // true

// 函数和 Promise 判断
util.types.isAsyncFunction(async () => {}); // true
util.types.isGeneratorFunction(function*(){}); // true
util.types.isPromise(Promise.resolve());    // true
util.types.isProxy(new Proxy({}, {}));      // true

// 特殊值判断
util.types.isArgumentsObject(arguments);    // true
util.types.isArrayBuffer(new ArrayBuffer(16)); // true
util.types.isDataView(new DataView(new ArrayBuffer(16))); // true
util.types.isTypedArray(new Uint8Array(4)); // true

// 模块命名空间(如 import * as ns)
util.types.isModuleNamespaceObject(someNs); // true

这些判断比 typeofObject.prototype.toString.call() 更加准确和直观,尤其在处理 TypedArray、Promise、Proxy 等特殊对象时非常有用。它们不会受到跨 Realm(如 vm 模块、iframe)上下文差异的影响,因为它们直接调用 V8 的内部检查。

已弃用但仍存在的旧判断函数

早期 Node.js 提供了 util.isArray(obj), util.isRegExp(obj) 等,但这些由于在内部实现上不够严谨且可以被 ES 原生方法替代,已从 Node.js 4.x 开始弃用,在实际项目中应尽量避免使用。官方建议:

| 弃用方法 | 推荐替代 |
|--------------|------------------|
| util.isArray() | Array.isArray() |
| util.isRegExp()| obj instanceof RegExp |
| util.isDate() | obj instanceof Date |
| util.isError() | obj instanceof Error |
| ... | ... |

唯一仍可保留使用的是 util.isDeepStrictEqual(val1, val2)(自 9.0.0 起增加),但通常由断言库提供。

8.4.3 继承:util.inherits

在 Node.js 0.x 和 4.x 时代,util.inherits(constructor, superConstructor) 是推荐的继承方式,它基于原型链设置了 constructor.prototype.protoconstructor.prototype.constructor,但并不复制父类的实例属性(只继承原型方法)。语法如下:

const util = require('util');
const EventEmitter = require('events');

function MyStream() {
  EventEmitter.call(this); // 继承实例属性
}

util.inherits(MyStream, EventEmitter);

// 现在 MyStream 可以使用 EventEmitter 的方法
MyStream.prototype.write = function(data) {
  this.emit('data', data);
};

上面的代码要求手动调用父类构造函数(EventEmitter.call(this)),否则父类实例属性无法继承。

现代替代:ES6 class 语法

从 ES6/Node.js 6+ 开始,classextends 已经全面可用,提供了更清晰、更标准的继承方式:

const EventEmitter = require('events');

class MyStream extends EventEmitter {
  write(data) {
    this.emit('data', data);
  }
}

这种方式不需要手动调用父类构造函数,语法更加直观,同时避免了 util.inherits 只能继承原型的限制。因此,util.inherits 已被视为几乎弃用,除非维护极老的代码,否则应该全部使用 ES6 的 class 继承。

8.4.4 格式化:util.format

util.format 是一个类似于 C 语言 printf 的字符串格式化工具,它返回格式化后的字符串而不是直接打印。Node.js 的 console.logconsole.error 内部实际上也使用了 util.format

基本用法

const util = require('util');

// 占位符替换
util.format('%s:%d', 'foo', 42);          // 'foo:42'
util.format('%s %s', 'hello', 'world');    // 'hello world'

// 若占位符数量不足,多余参数会附加在末尾
util.format('%s', 'hello', 'world', 123); // 'hello world 123'

// 若没有占位符,参数用空格连接
util.format(1, 2, 3);                     // '1 2 3'

// 对象会自动转换为 util.inspect 输出
util.format('user: %o', { name: 'Alice' }); // 'user: { name: \'Alice\' }'

支持的占位符

  • %s – 字符串。
  • %d – 数字(包括整数和浮点数)。
  • %i – 整数(同 %d)。
  • %f – 浮点数(已经被 %d 包含,较少用)。
  • %j – JSON 字符串(如果参数不是可 JSON 化的对象,可能抛出异常)。
  • %o – 对象(util.inspect 浅层格式化,深度通过 util.inspect.defaultOptions 控制)。
  • %O – 对象(util.inspect 浅层格式化,但允许修改深度等选项)。
  • %% – 单独输出一个百分号,不消耗参数。

实际应用

需要构建日志消息、拼接请求信息时,util.format 非常方便,比手动字符串连接可读性更好,也比模板字符串提供了更多类型转换。

// 日志输出
const logMessage = util.format(
  '[%s] %s %s %dms',
  new Date().toISOString(), req.method, req.url, responseTime
);

// 如果直接用于 console,其实已经内置了 format
console.log('[%s] %s %s %dms', ...params); // console.log 内部调用 util.format

在某些极端情况下(比如你需要格式化一个参数本身包含 % 字符),最好使用 %% 或直接避开占位符模式(作为字符串拼接)。

8.4.5 其他实用工具

虽然题目限于四个子主题,但 util 模块还有其他几个常用函数值得一提,它们经常与前面这些一起使用:

  • util.types(前面已详述)提供精确的类型检查。
  • util.inspect:将任意对象转为字符串,对于调试和序列化输出很有用,接受 depthcolorsshowHidden 等选项。
  • util.callbackify:与 promisify 相反,把一个返回 Promise 的函数转回回调风格((err, res) => {}),便于兼容老接口。
  • util.deprecate:包装一个函数,当它被调用时会发出弃用警告,适合开发和维护大型库。
  • util.TextEncoder / util.TextDecoder:ES6 标准的文本编码解码器,Node.js 将它们挂载在 util 下作为稳定的全局替代(自 11.0.0 起作为全局,但仍可通过 util 访问)。

util 模块虽然都是小工具,但在日常开发中能显著提升代码的可维护性和可读性。特别是 promisify 让遗留异步代码焕发新生,类型判断让我们少写很多 typeof 和异常处理,格式化则让日志输出更加清晰。掌握这些 API,是写出更符合 Node.js 风格代码的重要一步。