Skip to content

第 21 章 · 面试题集(Node.js 基础)

Q1:Node.js 和浏览器里的 JavaScript 有什么区别?

考察点:JS 运行时差异。

标准答案

维度浏览器Node.js
引擎V8 / SpiderMonkey 等V8
全局对象window / selfglobal / globalThis
DOM / BOM✅ 有❌ 没有
文件系统❌ 没有(只有 File API)fs
模块系统原生 ESMCJS + ESM 共存
事件循环浏览器规范(task / microtask)libuv 6 阶段 + microtask
独有 APIdocumentlocalStoragefetchprocessBufferrequire

通俗理解: 浏览器是 JS 的"剧场"——有舞台(DOM)和观众;Node 是 JS 的"工厂"——有车床(fs)和发货口(http)但没有观众。

加分回答

  • 两者都用 V8,但 Node 直接拿 V8 的 C++ API 封装了一组 OS 能力(libuv)。
  • 浏览器有 Web Worker,Node 有 Worker Threads,机制相似但 API 不同。
  • Node 18+ 内置了 fetch / Web Streams / Blob,正在向 Web 标准靠拢。

追问

  • Node 的 setTimeout 和浏览器的有什么区别?(最小延迟、与 setImmediate 的协作)

Q2:Node.js 是单线程的吗?为什么能高并发?

考察点:异步 I/O 模型 + libuv。

标准答案主线程(执行 JS 的那条 V8 线程)是单线程的,但整个 Node 进程是多线程的:

  1. 网络 I/O 通过操作系统的异步接口(epoll / kqueue / IOCP)完成,不占用线程
  2. 文件 I/O / DNS / 部分加密 通过 libuv 线程池(默认 4 个线程)完成。
  3. 完成后回调被推入事件循环对应阶段,主线程依次执行。

主线程不被阻塞 → 可以同时"挂起"成千上万个 I/O 请求 → 高并发。

通俗理解: 餐厅前台只有 1 个服务员(主线程),但后厨有一队厨师(libuv 线程池)。服务员只负责接单上菜,做菜的事都交给后厨。前台永远不会被卡住,所以能服务很多人。

代码示例

js
// 模拟 1 万个并发"假"请求
for (let i = 0; i < 10000; i++) {
  setTimeout(() => {/* nothing */}, 100);
}
// Node 毫无压力。如果是同步:for 循环里 sleep 100ms × 10000 = 1000 秒

加分回答

  • libuv 线程池大小可通过环境变量 UV_THREADPOOL_SIZE 调整(默认 4,最大 1024)。
  • CPU 密集任务(图像处理、加密)会阻塞主线程,应该放到 worker_threads 或子进程。
  • Node 的高并发只在 I/O 密集场景成立,CPU 密集场景反而不如多线程语言。

追问

  • 如果某个回调里写死循环会怎样?(事件循环卡死,整个服务无响应)

Q3:Node 的事件循环有几个阶段?分别处理什么?

考察点:核心机制——面试必考

标准答案: 6 个阶段,按顺序循环:

阶段处理内容
timers到期的 setTimeout / setInterval 回调
pending callbacks部分系统级回调(TCP 错误等)
idle, prepareNode 内部使用
pollI/O 回调(fs / 网络),无回调时阻塞等待
checksetImmediate 回调
close callbackssocket.on('close', ...)

每个阶段切换之间,会清空一次微任务队列:先 process.nextTick,再 Promise .then

通俗理解: 事件循环像一个"单向环形跑道",分 6 个站点。每到一个站点处理对应的回调;每到下一个站点之前,先把"加急任务"(微任务)清空。

代码示例

js
console.log("1");
setTimeout(() => console.log("2"), 0);
setImmediate(() => console.log("3"));
process.nextTick(() => console.log("4"));
Promise.resolve().then(() => console.log("5"));
console.log("6");

// 输出:1, 6, 4, 5, 2/3 (后两者顺序不定)

加分回答

  • process.nextTick 优先级比 Promise 还高(在 microtask 队列前面)。
  • I/O 回调里 setImmediate 一定先于 setTimeout(fn, 0),因为 poll 结束直接进 check。
  • React 的 useEffect 在浏览器对应"宏任务后微任务",没有 Node 这种 6 阶段概念。

追问

  • setImmediate(fn)setTimeout(fn, 0) 谁先?(主模块不确定;I/O 回调里 setImmediate 必先)

Q4:CommonJS 和 ESM 在 Node 中有什么区别?

考察点:模块系统。

标准答案

维度CJSESM
文件后缀.cjs.js(默认).mjspackage.json "type":"module"
导入require()import
导出module.exports / exports.xexport / export default
加载时机运行时同步加载编译时静态分析
顶层 await
__dirname / __filename❌(用 import.meta.url
Tree Shaking难(动态求值)易(静态结构)
互相调用ESM 可 import CJS;CJS 需 await import() ESM

通俗理解: CJS 像是"运行到这一行才去取货",ESM 是"出发前就把货单列好"。

代码示例

js
// CJS
const fs = require("fs");

// ESM
import fs from "node:fs";
import { readFile } from "node:fs/promises";

加分回答

  • Node 22+ 实验性允许 CJS 同步 require ESM(要求 ESM 模块没有 top-level await)。
  • 现代项目(Vite / Next.js)几乎都全 ESM,加 "type":"module"
  • ESM 的好处是支持 tree shaking → 打包后体积更小。

追问

  • ESM 怎么取 __dirname?(import.meta.url + fileURLToPath

Q5:process.nextTickPromise.thensetImmediatesetTimeout(fn, 0) 的执行顺序?

考察点:事件循环细节。

标准答案主模块代码中

  1. process.nextTick —— 微任务,优先级最高
  2. Promise.then —— 微任务,但在 nextTick 之后
  3. setTimeout(fn, 0) —— timers 阶段,至少 1ms
  4. setImmediate —— check 阶段 (3 和 4 顺序不固定)

I/O 回调内部

  1. process.nextTick
  2. Promise.then
  3. setImmediate(一定先)
  4. setTimeout

代码示例

js
import fs from "node:fs";

fs.readFile("./test.txt", () => {
  setTimeout(() => console.log("timeout"), 0);
  setImmediate(() => console.log("immediate"));
  process.nextTick(() => console.log("nextTick"));
  Promise.resolve().then(() => console.log("promise"));
});

// 输出:nextTick → promise → immediate → timeout(一定如此)

加分回答

  • process.nextTick 滥用会导致后续阶段饿死(永远进入不了 timer / poll)。
  • 浏览器没有 nextTick 和 setImmediate;Node 早期为兼容浏览器一起加进来的。

Q6:Node 的核心模块有哪些?分别用来干什么?

考察点:基础认知。

标准答案

模块用途
fs文件系统:读/写/监听
path路径处理:拼接、解析、跨平台
http / https起 HTTP 服务、发请求
urlURL 解析 / 拼装
eventsEventEmitter 发布订阅基类
stream流式处理大文件 / 网络
process进程信息、环境变量
Buffer二进制数据操作
os操作系统信息(cpu、内存、平台)
child_process启动子进程
worker_threadsWorker 线程
crypto加密、哈希

加分回答

  • 推荐用 node: 前缀:import fs from "node:fs" 显式区分内置模块和 npm 包。
  • fs/promises 是 promise 化版本,比回调风格更现代。

Q7:写一个最小的 Node HTTP 服务器,并用 Express 重写。

考察点:原生 vs 框架的对比。

标准答案

原生 http

js
import http from "node:http";

const server = http.createServer((req, res) => {
  if (req.method === "GET" && req.url === "/") {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end("Hello");
  } else if (req.method === "POST" && req.url === "/echo") {
    let body = "";
    req.on("data", (c) => (body += c));
    req.on("end", () => {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(body);
    });
  } else {
    res.writeHead(404);
    res.end("Not Found");
  }
});

server.listen(3000);

Express 版

js
import express from "express";
const app = express();
app.use(express.json());

app.get("/", (req, res) => res.send("Hello"));
app.post("/echo", (req, res) => res.json(req.body));

app.listen(3000);

通俗理解: 原生 http 像"自己烧瓦砌墙盖房子",Express 像"买了精装房毛坯房"——封装了路由、body 解析、中间件。

加分回答

  • Express 中间件机制是核心:(req, res, next) => {},调用 next 进入下一个。
  • Koa(Express 团队下一代)用 async/await,错误处理更优雅。
  • Fastify 性能 ≈ Express 的 2-3 倍,自带 schema 校验。

追问

  • 为什么 Express body 要用中间件解析而不是默认?(兼容历史,body 可能是 form-urlencoded、multipart、json…)

Q8:Node 的 stream 解决了什么问题?有几种类型?

考察点:流式 I/O。

标准答案stream(流) 解决"大数据无法一次性放入内存"的问题。把数据切成一段段,边读边处理边写,内存占用恒定。

4 种 Stream

类型例子
Readablefs.createReadStreamhttp.IncomingMessage
Writablefs.createWriteStreamhttp.ServerResponse
DuplexTCP socket(既可读又可写)
Transformgzip / 加密(边读边变换边写)

通俗理解

  • 不用 stream:用桶接水(一次性放内存)。
  • 用 stream:用水管接水(流式,恒定内存)。

代码示例

js
import { createReadStream, createWriteStream } from "node:fs";
import { createGzip } from "node:zlib";

// 一行实现:读 → gzip 压缩 → 写
createReadStream("input.txt")
  .pipe(createGzip())
  .pipe(createWriteStream("input.txt.gz"));

加分回答

  • HTTP 响应本身就是 Writable Stream,所以可以 fs.createReadStream("video.mp4").pipe(res) 实现视频流式响应。
  • Node 18+ 支持 Web Streams(ReadableStream / WritableStream),与浏览器统一。
  • pipeline()pipe() 更安全:自动处理错误和关闭。

追问

  • 背压(backpressure)是什么?(消费者来不及处理,让生产者暂停推送的机制)

Q9(加分题):什么是 Buffer?为什么 Node 需要它?

考察点:二进制处理。

标准答案Buffer 是 Node 中操作二进制数据的类,本质是固定长度的字节数组。

为什么需要:JS 原生没有处理二进制的能力,但服务器经常要:

  • 处理图片 / 音视频
  • 网络协议字节解析
  • 加密 / 哈希
  • 文件 I/O(默认拿到的就是 Buffer)

代码示例

js
const buf = Buffer.from("你好", "utf8");
console.log(buf);                  // <Buffer e4 bd a0 e5 a5 bd>
console.log(buf.length);           // 6(每个汉字 UTF-8 占 3 字节)
console.log(buf.toString("hex"));  // "e4bda0e5a5bd"
console.log(buf.toString("base64"));// "5L2g5aW9"

const buf2 = Buffer.alloc(10);     // 10 字节、全 0
const buf3 = Buffer.allocUnsafe(10); // 不清零(更快但有脏数据)

加分回答

  • Buffer 在 V8 堆外分配(避免 GC 压力大)。
  • ES2017 后浏览器有了 Uint8Array / ArrayBuffer,Buffer 实际是 Uint8Array 的子类。
  • Node 18+ 推荐慢慢迁移到 Uint8Array 以与 Web API 兼容。

追问

  • Buffer.allocUnsafe 为什么不安全?(不清零,可能含其它请求的旧数据,泄漏风险)

Q10(加分题):作为前端工程师,哪些场景必须用到 Node?

考察点:场景化理解。

标准答案

场景例子
构建工具Vite / Webpack / Rollup / esbuild
CLI 脚本create-next-app / 项目内的 release.js / 自动改 i18n
SSR 运行时Next.js / Nuxt / Remix
BFF 中间层聚合多个微服务接口给前端
Serverless / EdgeVercel / Netlify / Cloudflare Functions
桌面应用Electron / Tauri
Mock Serverjson-server、msw
WebSocket 实时协作编辑、聊天
测试运行器Jest / Vitest / Playwright

加分回答

  • 现代前端工具链 90% 用 Node 写:package.json 里几乎所有 devDependencies 都是 Node 工具。
  • "前端工程师懂 Node" 不等于 "会写后端"——更多是工程化能力。
  • 学完 Node,再写一些自己的 CLI(比如自动 commit、自动同步 i18n)会让效率飞跃。

追问

  • BFF 和后端有什么区别?(BFF 不放业务逻辑,只做接口聚合 / 字段裁剪 / 鉴权透传)