跳到正文
前端知识库
后端与网络

Node.js 面试真题补充

本篇整理自 5.Node.js面试真题-71页.pdf 的代码和场景,补充现有主题文章中较容易被追问的工程细节。

7 分钟

Node.js 面试真题补充

本篇整理自 5.Node.js面试真题-71页.pdf 的代码和场景,补充现有主题文章中较容易被追问的工程细节。

一、文件系统与同步边界

1. readFilereadFileSync 的差别

fs.readFile 通过回调(或 fs.promises)异步返回结果;未传 encoding 时得到 Buffer,传入 utf8 时得到字符串。 readFileSync 会阻塞当前 JavaScript 线程,适合启动阶段读取一次配置,不应放在请求处理路径。(知识点:同步与异步的选择

const fs = require('node:fs');

const bytes = fs.readFileSync('data.txt');
const text = fs.readFileSync('data.txt', 'utf8');

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

写入、追加和复制同样分别有同步与异步版本。异步回调中的 err 必须先处理,再使用 data;连续文件操作应使用 Promise 或 pipeline 组织,避免回调嵌套。(知识点:Stream 管道与错误处理

2. 路径与目录

PDF 示例区分了文件操作和目录操作。生产代码应使用 path.join/path.resolve 组合路径,并在需要递归创建目录时显式声明:

const path = require('node:path');
fs.mkdir(path.join(process.cwd(), 'a/b/c'), { recursive: true }, (err) => {
  if (err) throw err;
});

不要把用户输入直接拼接成路径;上传和静态文件场景要校验规范化后的路径,防止 ../ 穿越。(知识点:路径处理

二、Buffer 与编码

1. 字符串和字节数组不是一回事

Buffer.from('10') 会把字符 10 编码成两个字节(0x31 0x30);Buffer.from([10]) 只有一个值为十进制 10 的字节。 Buffer.alloc(size) 会进行零填充,适合接收外部数据;不确定内容时不要把未初始化内存暴露给调用方。(知识点:Buffer 的创建方式知识点:内存安全

2. UTF-8 截断陷阱

中文等 UTF-8 字符占多个字节。按字节切片可能得到非法的半个字符,toString('utf8') 会出现替换字符 。按字符截断应先在字符串层处理,或确保切片落在完整编码边界。(知识点:Buffer 编码处理

const buf = Buffer.from('Node.js 技能', 'utf8');
console.log(buf.length);             // 字节数,不是字符数
console.log(buf.toString('utf8'));   // 解码时指定同一编码

三、Stream、pipe 与背压

PDF 通过读流、写流和 source.pipe(dest) 展示了大文件处理。四类流的回答框架如下:

类型 作用 常见例子
Readable 产生数据 fs.createReadStream、HTTP 请求
Writable 消费数据 fs.createWriteStream、HTTP 响应
Duplex 同时可读可写 TCP socket
Transform 读入后转换再输出 压缩、加密、编码转换

流按 chunk 处理,首块数据到达即可开始响应,峰值内存远低于一次性 readFilepipe 会在目标流变慢时暂停源流,形成背压;复杂链路应监听错误并在结束时释放资源。(知识点:Stream 的四种类型知识点:背压与错误处理

四、process 与全局对象

五、EventEmitter 的监听语义

PDF 的实现示例覆盖了 on/addListenerprependListeneremitonceremoveListener/off

const { EventEmitter } = require('node:events');
const bus = new EventEmitter();

const handler = (name) => console.log(name);
bus.once('ready', handler);       // 只触发一次
bus.prependListener('ready', () => console.log('first'));
bus.emit('ready', 'service');
bus.off('ready', handler);        // 必须传入同一个函数引用

emit 会同步调用当前事件的监听器;监听器内部的异常不会自动变成业务错误响应,应在应用边界统一处理。需要移除监听器时不能用“长得一样”的新函数替代原引用。(知识点:EventEmitter 的核心方法知识点:错误处理

六、CommonJS 加载与模块缓存

1. 导出对象

exports 初始时只是 module.exports 的引用。给 exports.name 赋值可以增加属性,但整体替换导出对象必须写 module.exports = ...。(知识点:exports 和 module.exports

// user.js
exports.name = 'Ada';
module.exports.getName = () => 'Ada';

// 若需要导出函数本身:
// module.exports = createUser;

2. require 的解析顺序

相对路径通常依次尝试文件(.js.json.node)、目录的 package.json main,再尝试 index 文件;裸模块名则沿当前目录向上查找 node_modules。首次加载后模块会进入缓存,后续 require 返回同一个导出对象。修改导出或测试隔离时要意识到这份缓存的存在。(知识点:Require 查找优先级知识点:循环依赖处理

七、Koa 中间件与请求体

Koa 中间件是“洋葱模型”:进入阶段执行 await next() 前的逻辑,返回阶段执行 await next() 后的逻辑。日志、鉴权、错误处理应据此安排顺序。(知识点:洋葱圈模型

app.use(async (ctx, next) => {
  const start = Date.now();
  try {
    await next();
  } finally {
    console.log(ctx.method, ctx.url, 'elapsed=' + (Date.now() - start) + 'ms');
  }
});

PDF 还展示了手写 body parser 和静态文件中间件:读取请求 chunk 后用 Buffer.concat 汇总,再按 Content-Type 解析 URL 编码或 JSON;静态文件则先 stat 判断文件/目录,并用读流作为响应体。实际项目优先使用成熟中间件,同时限制 body 大小并捕获 JSON 解析错误。(知识点:封装 koa-bodyparser知识点:静态文件服务

八、事件循环输出题

PDF 示例的典型顺序是:同步代码先执行;当前阶段结束后 process.nextTick 优先于 Promise 微任务;随后才进入定时器或 setImmediate 阶段。 setTimeout(fn, 0)setImmediate(fn) 在主模块中谁先执行并不稳定,在 I/O 回调内部通常先看到 setImmediate。(知识点:process.nextTick知识点:setTimeout 与 setImmediate

console.log('sync');
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
setTimeout(() => console.log('timer'), 0);
setImmediate(() => console.log('immediate'));

不要把某一次运行的 timer/immediate 顺序当成语言级保证;回答时应说明所在事件循环阶段。(知识点:事件循环的 6 个阶段

九、性能、内存与常见反例

1. 监控指标

process.memoryUsage() 可观察 rssheapUsedheapTotalos.totalmem()/os.freemem() 反映系统内存。持续增长的 heapUsed、事件循环延迟或 CPU 飙高,分别提示泄漏、阻塞或计算密集问题。(知识点:性能监控指标获取

2. PDF 中的三个优化场景

  • 大文件响应:用 createReadStream().pipe(res),避免 readFile 一次性读入。(知识点:Stream 优化大文件处理
  • N+1 查询:先批量查出用户,再建立 userId -> account 映射,循环内只做内存查找。
  • 内存泄漏:不要把每次请求读出的 Buffer 持续 push 到全局数组;缓存必须有上限、淘汰策略和生命周期。(知识点:内存泄漏检测

CPU 密集任务不能靠增加异步回调解决,应考虑 worker_threads、独立进程或任务队列。(知识点:CPU 密集型任务的边界

十、文件上传与鉴权场景

1. multipart/form-data

上传请求用 boundary 分隔字段,每个 part 通过 Content-Disposition 声明 name/filename,并可带自己的 Content-Type。Koa 示例使用 koa-bodykoa-multer 接收文件,再以读流写入目标目录。(知识点:multipart/form-data 协议

落地时至少限制单文件大小和总请求大小,使用服务端生成的文件名,校验 MIME 与文件签名,并把上传目录置于不可执行位置。(知识点:文件上传安全风险

2. JWT 的回答要点

JWT 由 header.payload.signature 三段组成。 payload 只是 Base64URL 编码,不是加密数据;客户端的 decode 不能代替服务端 verify。登录成功后通常通过 Authorization: Bearer <token> 传递,服务端校验签名、exp、发行者和受众。HS256 使用共享密钥,RS256 使用私钥签名、公钥验证,分布式服务更易进行公钥分发。(知识点:JWT 结构知识点:JWT 安全性考虑

3. 分页接口

页码分页通常计算 start = (page - 1) * pageSize,查询 LIMIT pageSize OFFSET start,同时返回 totalCounttotalPagescurrentPagedatapagepageSize 要做正数和上限校验;SQL 使用参数化查询,不能把请求参数直接插入模板字符串。