跳到主要内容

基础模块✅

CommonJS 与 ESM 模块系统区别?

答案
系统CommonJS(CJS)ESM
加载同步 require异步,支持顶层 await
导出语义导出为对象快照(非 live binding)live binding,静态可分析(利于 tree-shaking)
解析与定位传统解析,可省扩展名,支持目录 indexURL/显式扩展名;受 exports/imports 影响
互操作可 require JSON、.node;用 import() 加载 ESMCJS 需用动态 import;ESM 不可直接用 require
文件/配置.cjs 或 package.json 设置 "type":"commonjs".mjs 或 package.json 设置 "type":"module"
实现本质上是一个必包函数,默认非严格模式V8 引擎内置支持,默认严格模式

示例说明

// package.json
{ "type": "module", "exports": "./dist/index.js" }

// CJS 中加载 ESM(需异步)
/* index.cjs */
(async () => {
const { sum } = await import('./esm.mjs')
console.log(sum(1, 2))
})()

/* esm.mjs */
export const sum = (a, b) => a + b
const cfg = await import('./cfg.json', { assert: { type: 'json' } })
console.log(cfg.default.name)
提示

ESM 需写全扩展名,路径以 ./ 或 ../ 开头;库发布优先使用 package.json 的 exports 控制入口,避免隐式深引用与解析歧义。

延伸阅读

  • Node.js Modules: CJS CommonJS 规范与解析
  • Node.js ESM node.js API 文档,对 ESM 支持的全面说明,同时详细讲解了和 commonjs 相关的兼容差异

ESM 支持顶层异步,为什么 Node.js 中可以加载 ESM 模块?

答案

Node.js Common.js 模块只支持引入没有顶层异步的 ESM 模块, 如果存在异步会抛出 ERR_REQUIRE_ASYNC_MODULE 错误,如果希望在在 Common.js 中引用异步可采用 import 语句,运行时需要开启 --experimental-print-required-tla 配置。

import('./asyncAdd.mjs').then(({ add }) => {
console.log(add(1, 2)) // 3
})

延伸阅读

Node 原生支持哪些模块,有什么区别

答案
类型扩展名/入口加载与执行导出语义解析与定位
CommonJS(CJS).cjs 或 .js(默认 commonjs)同步 require值快照(非 live binding),require 缓存传统解析,可省扩展名,支持目录 index
ES Module(ESM).mjs 或 .js(type:module)异步,支持顶层 awaitlive binding,可静态分析URL/显式扩展名;受 package.json exports/imports 影响
JSON 模块.jsonCJS 同步加载,ESM 需要使用 import attributes 申明为 with {type: 'json'}默认为 default 的 JS 对象ESM 导入必须申明文件后缀
原生扩展.node同步装载本机动态库导出 N-API/C++ 绑定对象由 Node 动态装载,跨 CJS/ESM 可用
内置核心模块node:fs、node:path零网络/磁盘解析稳定 API 对象通过 node: 方案名直接解析

示例说明

// CJS
const cfg = require('./cfg.json')
const addon = require('./addon.node')
;(async () => {
const { sum } = await import('./esm.mjs') // 加载 ESM 用 import()
console.log(cfg.name, addon.version, sum(1, 2))
})()

// ESM
import fs from 'node:fs'
import cfg from './cfg.json' assert { type: 'json' }
export { fs, cfg }

常见误区

  • CJS 不能直接 require 含顶层 await 的 ESM,请用 import()。
  • ESM 路径需写全扩展名,受 exports 字段影响解析;避免深引用内部文件。
  • JSON 在 ESM 中必须使用 assert { type: 'json' },并以 default 形式导出。
提示

优先使用 node: 前缀引用核心模块(如 node:fs),可避免与用户空间同名包冲突,并利于静态分析。

延伸阅读

用过 Node.js 哪些内置模块或 API?

答案

Node.js API 文档 详细罗列了 Node 支持的内部能力,可按照如下逻辑分类

  1. 核心基础 全局和常用的模块
    1. Modules 模块系统, ESM 模块
    2. global api Node 宿主环境支持的全局对象,例如 __filename、__dirname、process
    3. Events 事件模块
    4. Timer 定时器相关 API
  2. 文件系统
    1. File system(文件系统)
    2. Readline(行读取)常用在日志处理等场景
    3. Path(路径处理)
    4. Stream(流式数据), 注意和 webstreams 浏览器端实现的区别
    5. String decoder(字符串解码)
  3. 进程与多线程 处理并发、子进程、集群和多线程的 API,用于高性能和分布式场景。
    1. Environment Variables(环境变量)
    2. Process node 进程信息及监控
    3. Child processes 提供子进程创建能力
    4. Cluster(多进程集群)
    5. Worker threads(工作线程)
  4. 网络与通信 涉及 TCP/UDP、HTTP/HTTPS、DNS 等网络通信,以及 URL 解析和域名处理。
    1. Net TCP socket 编程相关能力
    2. UDP/datagram(UDP 套接字)
    3. HTTP, 配合 Query stringURL
    4. HTTP/2
    5. HTTPS
    6. TLS/SSL
    7. DNS(域名解析)
  5. 工具类 涉及 Buffer、编解码、加密、国际化等功能
    1. Buffer Node 提供的二进制数据容器,基于 uint8Array 实现,Buffer 脱离 V8 垃圾回收机制
    2. String decoder 配合 Buffer 使用实现常见 UTF-8、UTF-16 等编码解码
    3. Zlib 压缩能力
    4. OS 获取系统相关信息
    5. Sqlite 集成 SQLite 数据库
    6. Crypto (加密模块),还有 webcrypto web 端加解密 API
    7. Internationalization 国际化
    8. Permissions(权限控制)
    9. Util 内部集成的工具类
    10. Asynchronous context tracking 提供了一步上下文获取能力,在 Koa 中利用 AsyncLocalStorage 实现了异步上下文获取
  6. 工程化类 涉及调试、测试、性能等
    1. Console 控制台输出
    2. Errors(错误处理)
    3. Performance 性能监控与分析
    4. Assert 断言库
    5. Test Runner Node 内部集成的测试运行器
  7. 高级扩展 提供 Node.js 底层扩展、嵌入、虚拟机和 WASI 等高级功能。
    1. N-API 提供了一组 C/C++ API,用于构建原生插件和扩展。
    2. V8 获取 V8 内部信息,比如 getHeapSpaceStatistics 获取堆的快照
    3. vm 提供沙盒环境运行 JavaScript 代码的能力
    4. WASI web assembly 能力

面试官视角

此问题可作为热身题,基于面试者对常用 API 的分类和罗列可以评估面试者对 Node.js 整体熟悉程度,结合面试者提及的内容可以做进一步下探

node 是如何实现 require 的?

答案

核心概念

  1. 解析 Module._resolveFilename 基于模块 id 返回模块的绝对路径,核心规则包括,细节参考 模块解析

    1. 如果是核心模块解析后直接返回
    2. 如果是绝对路径,直接返回绝对路径
    3. 如果是相对路径,先走文件查找,在基于目录查找,会逐级向上查找,此外可以通过 NODE_PATH 来增加解析路径。
    4. 如果以 # 开头,基于 imports 配置
    5. 如果以 @xx 开头按照作用域包加载
    6. 如果以 xx 开头按照普通 node 包加载
  2. 缓存 Module._cache 以绝对文件路径缓存已加载模块,二次 require 命中缓存

  3. 文件类型 Node 支持多种文件类型,可以通过 `console.log(require('module')._extensions) 获取内部支持的文件后缀

    1. .js:会更具 type 配置来确定按照 commonjs 还是 ESM 方式加载
    2. .json 采用 JSON.parse 解析返回,注意 esm 需要通过 with {type : 'json'} 识别
    3. .node 通过 process.dlopen 装载原生扩展
    4. cjs 按照 commonjs 处理,内部调用 cjs loader, 需要显示包含文件名不会默认扩展
    5. .mjs 内部调用 esm loader
    6. .ts、.cts、mts 先通过
      1. 内部采用 amaro 剔除 ts 类型,也可通过 node 传入 --experimental-transform-types 配置使能 ts 转换,代码详见 typescript.js
      2. 然后在基于文件类型走 Commonjs 或 ESM 处理
提示

对于循环依赖 cjs 会默认暂停后续解析,esm 由于 live binding 不受影响但是会基于场景抛出相关警告,在 TopLevelAwait 避免循环引用会抛出警告无法正常加载循环模块

延伸阅读

exports 和 module.exports 有什么区别?

答案
说明
返回值require() 返回的永远是 module.exports
关系exports 只是 module.exports 的初始别名(同一引用):var exports = module.exports =
赋值差异exports.foo = 1 等价于 module.exports.foo = 1exports = {...} 只是改了局部变量引用,失效
覆盖规则一旦对 module.exports 重新赋值(如函数/对象),它将成为最终导出,之前挂在 exports 上的成员不会被导出

核心概念

  • Node 在执行 CJS 时用包装函数注入 (exports, require, module, __filename,__dirname),并令 exports 与 module.exports 指向同一对象。
  • require 取的是 module.exports 的引用,只有修改 module.exports 或向其上挂载属性才会生效。

示例说明

// 正确:挂载属性
exports.a = 1
module.exports.b = 2

// 正确:整体导出(常用于导出函数/类/单例)
module.exports = function Service () {}

// 反例:无效的整体替换(不会改变 module.exports)
exports = { x: 1 } // ❌ 仅改了局部变量 exports 的指向

// 冲突示例:以下最终只导出函数(对象属性丢失)
exports.a = 1
module.exports = function main () {}

// 典型策略, 共享引用修复此问题
module.exports = exports = function main () {}
exports.a = 1

node 如何处理循环依赖

答案
  • CommonJS require 在执行前就把模块对象放入缓存;循环依赖时,下游拿到的是“部分初始化”的 module.exports(可能缺成员),执行完后再补齐。
  • ESM:基于 live binding。依赖图先创建绑定,后求值;若在求值过程中读取尚未初始化的导出,会触发 TDZ 语义(ReferenceError)。导出函数/常量通常更安全。涉及 Top-level await 的循环会导致依赖图挂起并报错,应避免。
提示

在使用 Top-level await 时,浏览器会丢弃无法正常加载的循环依赖,Node.js 会抛出警告

// CJS:得到“半成品”导出
// a.cjs
exports.ready = false
const b = require('./b.cjs')
console.log('b.ready in a:', b.ready) // 可能为 false(部分导出)
exports.ready = true

// b.cjs
exports.ready = false
const a = require('./a.cjs')
console.log('a.ready in b:', a.ready) // false
exports.ready = true
// ESM:不安全示例(TDZ)
/*a.mjs*/
import { fromB } from './b.mjs'
export const fromA = 1
console.log(fromB) // 可能触发 ReferenceError(fromB 依赖未初始化)

/*b.mjs*/
import { fromA } from './a.mjs'
export const fromB = fromA + 1
// ESM:安全模式(导出函数/延迟访问)
/*a.mjs*/
export function getA() { return 1 }
import { getB } from './b.mjs'
console.log(getB()) // 2

/*b.mjs*/
import { getA } from './a.mjs'
export function getB() { return getA() + 1 }
注意

实践建议:CJS 避免在顶层立即使用来自对端的值,改为在函数内或异步回调中读取;ESM 避免在顶层读取尚未初始化的绑定,优先导出函数/常量。含顶层 await 的循环应打散(移到入口 await、或改用动态 import)。

讲一下 node:module 模块?

答案
  • node:module 提供 Node 模块系统底层 API,解决 CJS/ESM 互操作与工具化需求
  • 关键 API createRequire(在 ESM 中构造 require)、builtinModules/isBuiltin(内置模块检测)、findSourceMap/SourceMap(读取 sourcemap)
  • 使用 node: 前缀如 node:module、node:fs 可避免与用户同名包冲突并利于静态分析
  • 典型场景:ESM 中加载 CJS/JSON/.node,检测依赖是否为内置模块,调试与定位源码

示例说明:

// demo.mjs(Node 16+)
// 在 ESM 中使用 createRequire 加载 CJS/内置模块;检测内置模块
import { createRequire, builtinModules, isBuiltin } from 'node:module'
const require = createRequire(import.meta.url)

// 通过 require 访问内置模块(或第三方 CJS)
const fsByRequire = require('node:fs')

// 检测内置模块
console.log('isBuiltin("fs") =', isBuiltin('fs')) // true
console.log('builtinModules has fs =', builtinModules.includes('fs')) // true
提示

findSourceMap/SourceMap 用于在带有 //# sourceMappingURL 的文件上读取并解析 Source Map,便于调试与错误定位。

用过 Events 模块么,讲解一下?

答案
  • Events 提供 EventEmitter 发布-订阅机制;emit 是同步广播,监听回调按注册顺序执行
  • 关键 API:on/off(或 add/removeListener)、once、emit、prependListener、setMaxListeners、listenerCount
  • 特殊事件 error:若未注册 error 监听器,emit('error') 会抛出异常并使进程崩溃
  • 监听数超过阈值(默认 10)会告警,可 setMaxListeners(0) 取消或调大阈值;避免意外内存泄漏
  • 与 Web 的 EventTarget 不同:Node 的回调签名自由、无捕获/冒泡;Node 亦提供 node:events 的 once/ on 以 Promise/AsyncIterator 方式使用
提示

未监听 error 时触发将抛出异常;生产环境务必统一注册 error 监听器或隔离风险域。

相关示例

// 基础用法:on/once/emit、listenerCount、setMaxListeners
import { EventEmitter } from 'node:events'

const bus = new EventEmitter()
// 设置最大监听器数量
bus.setMaxListeners(20)
// 添加监听器, 注意 on 为同步触发
bus.on('data', (v) => console.log('data1:', v))
bus.on('data', (v) => console.log('data2:', v))
// 添加一次性监听器
bus.once('ready', () => console.log('ready once'))

console.log('listeners(data)=', bus.listenerCount('data'))
// 触发一次性监听器
bus.emit('ready')
// 触发普通监听器
bus.emit('data', 1)

setTimeout(() => {
  bus.emit('data', 2)
}, 0)

Open browser consoleTerminal

延伸阅读: