Node.js 的 util 模块提供了一系列实用函数,主要用来支持内部 API 的实现,但对开发者同样非常有用。它最常被用到的功能包括:将回调风格的函数转为 Promise、类型判断、继承、格式化等。掌握这些工具能让代码更简洁、更符合现代异步编程习惯。
8.4.1 util.promisify:将回调转为 Promise
在 Node.js 的早期,大部分异步 API 都采用“错误优先”的回调风格((err, result) => {})。虽然现在 fs.promises、dns.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').promises 或 require('stream').promises(如果 API 支持)来避免大部分 promisify 的需要,但在处理遗留代码或第三方库时,util.promisify 依然非常有用。
8.4.2 类型判断:util.types 与 util.isXxx
在 JavaScript 中,类型判断经常需要用 typeof、instanceof,但存在一些跨上下文的问题或对于内置对象的判断不够精细。util 模块提供了一套辅助函数,大多数已经弃用(如 util.isArray、util.isRegExp),推荐使用 Array.isArray 或 instanceof。但仍有一些难以替代的,特别是 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
这些判断比 typeof 或 Object.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.proto 和 constructor.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+ 开始,class 和 extends 已经全面可用,提供了更清晰、更标准的继承方式:
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.log 和 console.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:将任意对象转为字符串,对于调试和序列化输出很有用,接受depth、colors、showHidden等选项。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 风格代码的重要一步。