Node.js 2026 面试题汇总
聚焦 Node.js 18+ LTS 版本特性,覆盖事件循环、模块系统、Stream、进程管理、性能优化等核心主题。排除过时内容(如 callback hell、旧版 cluster 模式)。
目录
基础概念
1. Node.js 是什么?它的核心特性是什么?
答案要点:
- 定义:基于 Chrome V8 引擎的 JavaScript 运行时,用于构建高性能网络应用
- 核心特性:
- 事件驱动:基于事件循环处理并发
- 非阻塞 I/O:异步 I/O 操作,单线程高并发
- 跨平台:支持 Windows、Linux、macOS
- npm 生态:全球最大的开源库生态系统
// 非阻塞 I/O 示例
const fs = require('fs');
// 异步读取文件,不阻塞后续代码
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
console.log('继续执行其他任务'); // 先输出这行追问:Node.js 适合什么场景?不适合什么场景?
2. Node.js 的单线程是什么意思?它如何处理并发?
答案要点:
- 单线程:JavaScript 执行在单个主线程上,避免了多线程的锁和同步问题
- 并发处理:
- 事件循环:主线程处理事件队列中的任务
- libuv 线程池:I/O 操作(文件、DNS、加密)在 C++ 线程池中执行
- Worker Threads:CPU 密集型任务可以使用 worker_threads 模块
// libuv 线程池处理 I/O
const crypto = require('crypto');
// 这个操作在线程池中执行,不阻塞主线程
crypto.pbkdf2('password', 'salt', 100000, 64, 'sha512', (err, key) => {
console.log('密钥生成完成');
});
console.log('主线程继续执行'); // 先输出追问:libuv 线程池默认大小是多少?如何调整?
3. Node.js 18+ LTS 有哪些重要新特性?
答案要点:
Node.js 18 (2022 LTS):
- Fetch API:原生支持
fetch(),无需node-fetch - Test Runner:内置测试框架
node:test - Web Streams API:标准化的 Stream API
Node.js 20 (2023 LTS):
- 权限模型:
--experimental-permission限制文件/网络访问 - 稳定的 Test Runner:
node:test成为稳定特性 - 单可执行应用(SEA):打包成单个可执行文件
Node.js 22 (2024):
- require() ESM:实验性支持
require()导入 ESM 模块 - WebSocket 客户端:原生 WebSocket 支持
// Node.js 18+ 原生 fetch
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Node.js 18+ 内置测试
import { test } from 'node:test';
import assert from 'node:assert';
test('加法测试', () => {
assert.strictEqual(1 + 1, 2);
});追问:Node.js 的 LTS 版本发布周期是怎样的?
4. global 对象和浏览器的 window 有什么区别?
答案要点:
- Node.js 全局对象:
global(Node.js 20+ 也支持globalThis) - 特有属性:
process:进程信息和控制Buffer:二进制数据处理__dirname/__filename:当前文件路径(CommonJS)require/module/exports:模块系统(CommonJS)
// Node.js 特有的全局变量
console.log(process.version); // Node.js 版本
console.log(process.platform); // 操作系统平台
console.log(__dirname); // 当前目录路径
console.log(__filename); // 当前文件路径
// globalThis 统一浏览器和 Node.js
globalThis.myVar = 'hello';
console.log(global.myVar); // "hello"追问:ESM 模块中没有 __dirname,如何获取当前文件路径?
模块系统
5. CommonJS 和 ES Modules 的区别是什么?
答案要点:
| 特性 | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| 语法 | require() / module.exports | import / export |
| 加载时机 | 运行时同步加载 | 编译时静态分析 |
| 导出 | 值拷贝 | 活绑定(引用) |
| 顶层 await | ❌ 不支持 | ✅ 支持 |
| 文件扩展名 | .js / .cjs | .mjs / .js(需 "type": "module") |
| Tree-shaking | ❌ 不支持 | ✅ 支持 |
// ========== CommonJS ==========
// math.cjs
let count = 0;
module.exports = {
count,
increment: () => ++count
};
// main.cjs
const { count, increment } = require('./math.cjs');
increment();
console.log(count); // 0(值拷贝,不会更新)
// ========== ES Modules ==========
// math.mjs
export let count = 0;
export function increment() { count++; }
// main.mjs
import { count, increment } from './math.mjs';
increment();
console.log(count); // 1(活绑定,会更新)追问:如何在 Node.js 中混用 CJS 和 ESM?
6. package.json 的 type 字段有什么作用?
答案要点:
"type": "module":.js文件按 ESM 处理,CJS 需用.cjs扩展名"type": "commonjs"(默认):.js文件按 CJS 处理,ESM 需用.mjs扩展名- 不写
type字段:等同于"commonjs"
{
"name": "my-app",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}追问:exports 字段和 main 字段的区别?
7. Node.js 的模块解析算法是怎样的?
答案要点:
相对路径/绝对路径:
- 精确匹配文件(带扩展名)
- 尝试添加
.js/.json/.node - 尝试作为目录,查找
package.json的main或index.js
模块名(如 express):
- 从当前目录的
node_modules查找 - 逐级向上查找父目录的
node_modules - 查找全局
node_modules
// 相对路径
require('./utils'); // 查找 utils.js / utils.json / utils/index.js
// 模块名
require('express'); // 查找 node_modules/express
// 内置模块(优先级最高)
require('fs'); // 直接返回内置 fs 模块追问:node_modules 的查找顺序是怎样的?
8. 如何实现一个简单的模块缓存机制?
答案要点:
- Node.js 会缓存已加载的模块,存储在
require.cache中 - 同一模块多次
require()只执行一次 - 可以通过
delete require.cache[modulePath]清除缓存
// counter.js
let count = 0;
module.exports = {
increment: () => ++count,
getCount: () => count
};
// main.js
const counter1 = require('./counter');
const counter2 = require('./counter');
counter1.increment();
console.log(counter2.getCount()); // 1(共享同一实例)
// 清除缓存
delete require.cache[require.resolve('./counter')];
const counter3 = require('./counter');
console.log(counter3.getCount()); // 0(重新加载)追问:ESM 的模块缓存机制和 CJS 有什么不同?
异步编程
9. Node.js 中有哪些异步编程方式?
答案要点:
- Callback(旧,不推荐)
- Promise
- async/await(推荐)
- EventEmitter(事件驱动)
const fs = require('fs').promises;
// 1. Callback(旧方式)
require('fs').readFile('file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
// 2. Promise
fs.readFile('file.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));
// 3. async/await(推荐)
async function readFile() {
try {
const data = await fs.readFile('file.txt', 'utf8');
console.log(data);
} catch (err) {
console.error(err);
}
}
// 4. EventEmitter
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.on('data', (data) => console.log(data));
emitter.emit('data', 'hello');追问:如何将 callback 风格的 API 转换为 Promise?
10. util.promisify 的作用是什么?
答案要点:
- 将 callback 风格的函数转换为返回 Promise 的函数
- 要求 callback 遵循
(err, result)的 Node.js 约定
const util = require('util');
const fs = require('fs');
// 转换为 Promise 版本
const readFile = util.promisify(fs.readFile);
// 使用 async/await
async function main() {
const data = await readFile('file.txt', 'utf8');
console.log(data);
}
// 自定义 promisify
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
}追问:如果函数的 callback 不是 (err, result) 格式怎么办?
11. Promise.all 和 Promise.allSettled 的区别?
答案要点:
Promise.all:所有 Promise 成功才成功,任一失败立即失败Promise.allSettled:等待所有 Promise 完成(无论成功失败),返回结果数组
const promises = [
Promise.resolve(1),
Promise.reject(new Error('失败')),
Promise.resolve(3)
];
// Promise.all:遇到失败立即拒绝
try {
await Promise.all(promises);
} catch (err) {
console.log('有 Promise 失败了'); // 会执行
}
// Promise.allSettled:等待所有完成
const results = await Promise.allSettled(promises);
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: Error },
// { status: 'fulfilled', value: 3 }
// ]追问:Promise.race 和 Promise.any 的区别?
事件循环
12. Node.js 事件循环的六个阶段是什么?
答案要点:
┌───────────────────────────┐
┌─>│ timers │ 执行 setTimeout/setInterval 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ 执行延迟到下一轮的 I/O 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ 内部使用
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │ 执行 setImmediate 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ 执行 close 事件回调
└───────────────────────────┘关键阶段:
- timers:执行
setTimeout()和setInterval()的回调 - poll:检索新的 I/O 事件,执行 I/O 回调(几乎所有回调,除了 close、timers、setImmediate)
- check:执行
setImmediate()回调
追问:process.nextTick() 和 setImmediate() 的执行顺序?
13. process.nextTick() 和 setImmediate() 的区别?
答案要点:
process.nextTick():在当前操作完成后、事件循环继续之前立即执行setImmediate():在事件循环的 check 阶段执行- 执行顺序:
process.nextTick()> 微任务(Promise) >setImmediate()
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('Promise'));
console.log('同步代码');
// 输出顺序:
// 同步代码
// nextTick
// Promise
// setImmediate注意:过度使用 process.nextTick() 可能导致事件循环饥饿(I/O 永远得不到执行)。
追问:什么情况下应该用 setImmediate() 而不是 process.nextTick()?
14. 微任务和宏任务在 Node.js 中的执行顺序?
答案要点:
- 微任务(Microtask):
Promise.then/catch/finally、process.nextTick()、queueMicrotask() - 宏任务(Macrotask):
setTimeout、setInterval、setImmediate、I/O 操作
执行规则:
- 执行同步代码
- 清空
process.nextTick()队列 - 清空微任务队列(Promise)
- 执行一个宏任务
- 重复步骤 2-4
setTimeout(() => console.log('timeout1'), 0);
setImmediate(() => console.log('immediate1'));
Promise.resolve().then(() => {
console.log('promise1');
process.nextTick(() => console.log('nextTick inside promise'));
});
process.nextTick(() => {
console.log('nextTick1');
Promise.resolve().then(() => console.log('promise inside nextTick'));
});
// 输出顺序:
// nextTick1
// promise inside nextTick
// promise1
// nextTick inside promise
// timeout1 或 immediate1(顺序不确定,取决于事件循环启动时机)追问:为什么 setTimeout(fn, 0) 和 setImmediate(fn) 的执行顺序不确定?
Stream 流
15. Node.js 中有哪几种 Stream?各自的应用场景?
答案要点:
| 类型 | 说明 | 常见实例 |
|---|---|---|
| Readable | 可读流 | fs.createReadStream、http.IncomingMessage |
| Writable | 可写流 | fs.createWriteStream、http.ServerResponse |
| Duplex | 双工流(可读可写) | net.Socket、TCP sockets |
| Transform | 转换流(读写过程中转换数据) | zlib.createGzip、crypto.createCipher |
const fs = require('fs');
const zlib = require('zlib');
// 读取文件 → 压缩 → 写入新文件
fs.createReadStream('input.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('input.txt.gz'));
// 链式处理:Transform 流
const { Transform } = require('stream');
const upperCaseTransform = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase());
callback();
}
});
process.stdin.pipe(upperCaseTransform).pipe(process.stdout);追问:Stream 的背压(backpressure)机制是什么?
16. 什么是背压(Backpressure)?如何处理?
答案要点:
- 背压:当写入速度超过读取速度时,数据在内存中堆积,可能导致内存溢出
- 处理机制:
writable.write()返回false时,停止写入- 监听
drain事件,恢复写入
const fs = require('fs');
const readable = fs.createReadStream('large-file.txt');
const writable = fs.createWriteStream('output.txt');
readable.on('data', (chunk) => {
const canContinue = writable.write(chunk);
if (!canContinue) {
console.log('背压,暂停读取');
readable.pause();
}
});
writable.on('drain', () => {
console.log('缓冲区已清空,恢复读取');
readable.resume();
});
// 使用 pipe 自动处理背压(推荐)
readable.pipe(writable);追问:pipe() 内部是如何处理背压的?
17. 如何实现一个自定义的 Transform Stream?
答案要点:
- 继承
stream.Transform类 - 实现
_transform(chunk, encoding, callback)方法 - 可选实现
_flush(callback)方法(流结束时调用)
const { Transform } = require('stream');
class JSONParser extends Transform {
constructor(options) {
super(options);
this._buffer = '';
}
_transform(chunk, encoding, callback) {
this._buffer += chunk.toString();
// 按行分割
const lines = this._buffer.split('\n');
this._buffer = lines.pop(); // 保留不完整的行
for (const line of lines) {
try {
const obj = JSON.parse(line);
this.push(JSON.stringify(obj, null, 2) + '\n');
} catch (err) {
// 忽略无效 JSON
}
}
callback();
}
_flush(callback) {
// 处理最后剩余的数据
if (this._buffer) {
try {
const obj = JSON.parse(this._buffer);
this.push(JSON.stringify(obj, null, 2) + '\n');
} catch (err) {}
}
callback();
}
}
// 使用
process.stdin
.pipe(new JSONParser())
.pipe(process.stdout);追问:_transform 和 _flush 的区别?
文件系统
18. fs.readFile 和 fs.createReadStream 的区别?
答案要点:
| 特性 | fs.readFile | fs.createReadStream |
|---|---|---|
| 读取方式 | 一次性读取整个文件到内存 | 分块读取(Stream) |
| 内存占用 | 高(文件大小) | 低(固定缓冲区,默认 64KB) |
| 适用场景 | 小文件(< 10MB) | 大文件、实时处理 |
| 性能 | 小文件快 | 大文件快,内存友好 |
const fs = require('fs');
// readFile:适合小文件
fs.readFile('small.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data); // 完整内容
});
// createReadStream:适合大文件
const stream = fs.createReadStream('large.txt', { encoding: 'utf8' });
stream.on('data', (chunk) => {
console.log('收到数据块:', chunk.length);
});
stream.on('end', () => {
console.log('读取完成');
});
// Promise 版本(Node.js 10+)
const fsPromises = require('fs').promises;
const data = await fsPromises.readFile('file.txt', 'utf8');追问:如何读取超大文件(如 10GB)而不占用过多内存?
19. fs.watch 和 fs.watchFile 的区别?
答案要点:
fs.watch:基于操作系统的文件系统事件(inotify、FSEvents),性能高fs.watchFile:轮询文件状态(stat),性能低但兼容性好
const fs = require('fs');
// fs.watch(推荐)
fs.watch('file.txt', (eventType, filename) => {
console.log(`事件类型: ${eventType}`); // 'rename' 或 'change'
console.log(`文件名: ${filename}`);
});
// fs.watchFile(轮询,不推荐)
fs.watchFile('file.txt', { interval: 1000 }, (curr, prev) => {
console.log(`当前修改时间: ${curr.mtime}`);
console.log(`之前修改时间: ${prev.mtime}`);
});
// 第三方库 chokidar(生产推荐)
const chokidar = require('chokidar');
chokidar.watch('*.js').on('change', (path) => {
console.log(`${path} 已修改`);
});追问:fs.watch 在不同操作系统上的行为差异?
20. 如何安全地写入文件,避免数据丢失?
答案要点:
- 原子写入:先写入临时文件,再重命名覆盖原文件
- 使用
fsync:确保数据刷新到磁盘 - 错误处理:捕获所有可能的错误
const fs = require('fs').promises;
const path = require('path');
async function safeWriteFile(filePath, data) {
const tempPath = `${filePath}.tmp`;
try {
// 1. 写入临时文件
await fs.writeFile(tempPath, data, 'utf8');
// 2. 确保数据刷新到磁盘(可选,性能影响大)
const fd = await fs.open(tempPath, 'r+');
await fd.sync();
await fd.close();
// 3. 原子重命名
await fs.rename(tempPath, filePath);
console.log('文件写入成功');
} catch (err) {
// 清理临时文件
try {
await fs.unlink(tempPath);
} catch {}
throw err;
}
}
// 使用
await safeWriteFile('config.json', JSON.stringify({ key: 'value' }));追问:为什么 rename 是原子操作?
HTTP 与网络
21. 如何创建一个简单的 HTTP 服务器?
答案要点:
const http = require('http');
const server = http.createServer((req, res) => {
// 设置响应头
res.writeHead(200, { 'Content-Type': 'application/json' });
// 路由处理
if (req.url === '/api/users' && req.method === 'GET') {
res.end(JSON.stringify({ users: ['Alice', 'Bob'] }));
} else if (req.url === '/api/users' && req.method === 'POST') {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
const user = JSON.parse(body);
res.end(JSON.stringify({ success: true, user }));
});
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});追问:http 模块和 express 的区别?
22. Node.js 18+ 的原生 fetch API 和浏览器的有什么区别?
答案要点:
- Node.js 18+ 内置
fetch,基于undici实现 - 主要区别:
- Node.js 不支持 CORS(服务端没有同源策略)
- 默认不发送 cookies(需要手动设置)
- 支持
file://协议
// Node.js 18+ 原生 fetch
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' })
});
const data = await response.json();
// 错误处理
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 超时控制
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
const response2 = await fetch('https://slow-api.com', {
signal: controller.signal
});追问:如何在 Node.js 中实现请求重试?
23. 如何处理大文件上传?
答案要点:
- 使用 Stream 处理,避免一次性加载到内存
- 使用
multipart/form-data解析(如busboy、multer) - 分块上传(Chunked Upload)
const http = require('http');
const fs = require('fs');
const path = require('path');
const Busboy = require('busboy');
http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/upload') {
const busboy = Busboy({ headers: req.headers });
busboy.on('file', (fieldname, file, info) => {
const { filename } = info;
const savePath = path.join(__dirname, 'uploads', filename);
// 直接 pipe 到文件,不占用内存
file.pipe(fs.createWriteStream(savePath));
file.on('end', () => {
console.log(`文件 ${filename} 上传完成`);
});
});
busboy.on('finish', () => {
res.writeHead(200);
res.end('Upload complete');
});
req.pipe(busboy);
} else {
res.writeHead(404);
res.end();
}
}).listen(3000);追问:如何实现断点续传?
24. WebSocket 在 Node.js 中如何实现?
答案要点:
- 使用
ws库(最流行)或 Node.js 22+ 原生 WebSocket - 双向实时通信,适合聊天、实时推送
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('客户端已连接');
// 接收消息
ws.on('message', (data) => {
console.log('收到消息:', data.toString());
// 广播给所有客户端
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
});
// 发送消息
ws.send('欢迎连接!');
// 心跳检测
const interval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 30000);
ws.on('close', () => {
console.log('客户端已断开');
clearInterval(interval);
});
});追问:WebSocket 和 HTTP 长轮询的区别?
进程与集群
25. child_process 模块有哪些方法?各自的应用场景?
答案要点:
| 方法 | 说明 | 应用场景 |
|---|---|---|
exec() | 执行 shell 命令,缓冲输出 | 简单命令,输出较小 |
execFile() | 直接执行文件,不启动 shell | 性能更好,安全性更高 |
spawn() | 启动子进程,Stream 输出 | 长时间运行、大量输出 |
fork() | 专门用于运行 Node.js 脚本 | 多进程计算、IPC 通信 |
const { exec, spawn, fork } = require('child_process');
// exec:适合简单命令
exec('ls -la', (err, stdout, stderr) => {
if (err) throw err;
console.log(stdout);
});
// spawn:适合长时间运行
const child = spawn('find', ['.', '-type', 'f']);
child.stdout.on('data', (data) => {
console.log(`输出: ${data}`);
});
// fork:Node.js 进程间通信
const worker = fork('./worker.js');
worker.send({ task: 'compute', data: [1, 2, 3] });
worker.on('message', (result) => {
console.log('计算结果:', result);
});追问:exec 和 spawn 的内存占用差异?
26. 如何使用 cluster 模块实现多进程?
答案要点:
- Master-Worker 模式:主进程负责管理,工作进程处理请求
- 负载均衡:默认使用轮询(round-robin)策略
- 进程崩溃重启:监听
exit事件自动重启
const cluster = require('cluster');
const http = require('http');
const os = require('os');
if (cluster.isMaster) {
const numCPUs = os.cpus().length;
console.log(`主进程 ${process.pid} 正在运行`);
// 创建工作进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 工作进程崩溃时重启
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
cluster.fork(); // 重启
});
} else {
// 工作进程创建 HTTP 服务器
http.createServer((req, res) => {
res.writeHead(200);
res.end(`由进程 ${process.pid} 处理\n`);
}).listen(8000);
console.log(`工作进程 ${process.pid} 已启动`);
}追问:cluster 和 worker_threads 的区别?
27. worker_threads 适合什么场景?
答案要点:
- CPU 密集型任务:图像处理、加密、大数据计算
- 共享内存:通过
SharedArrayBuffer共享数据 - 不适合 I/O 密集型:I/O 操作已经是异步的,用 worker 反而增加开销
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
// 主线程
const worker = new Worker(__filename, {
workerData: { num: 1000000 }
});
worker.on('message', (result) => {
console.log('计算结果:', result);
});
worker.on('error', (err) => {
console.error('Worker 错误:', err);
});
} else {
// Worker 线程
const { num } = workerData;
// CPU 密集型计算
function isPrime(n) {
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) return false;
}
return n > 1;
}
const primes = [];
for (let i = 2; i <= num; i++) {
if (isPrime(i)) primes.push(i);
}
parentPort.postMessage(primes.length);
}追问:如何在 Worker 之间共享数据?
性能优化
28. Node.js 性能优化有哪些常见手段?
答案要点:
代码层面:
- 使用 Stream 处理大数据
- 避免同步阻塞操作(
fs.readFileSync) - 使用
Buffer而非字符串拼接 - 缓存计算结果(内存缓存、Redis)
架构层面:
- 使用
cluster或 PM2 多进程 - 反向代理(Nginx)
- 负载均衡
- CDN 静态资源
监控层面:
- 使用
clinic.js诊断性能瓶颈 --inspect开启 Chrome DevTools 调试- APM 工具(New Relic、Datadog)
// 错误示例:同步阻塞
const data = fs.readFileSync('large-file.txt'); // ❌ 阻塞事件循环
// 正确示例:异步 + Stream
fs.createReadStream('large-file.txt')
.on('data', chunk => process(chunk))
.on('end', () => console.log('完成'));
// 缓存示例
const cache = new Map();
async function getUser(id) {
if (cache.has(id)) {
return cache.get(id);
}
const user = await db.findUser(id);
cache.set(id, user);
return user;
}追问:如何诊断内存泄漏?
29. 如何监控 Node.js 应用的内存使用?
答案要点:
process.memoryUsage():查看当前内存使用情况- 堆快照(Heap Snapshot):使用 Chrome DevTools 分析
--max-old-space-size:调整 V8 堆内存上限
// 查看内存使用
const used = process.memoryUsage();
console.log({
rss: `${Math.round(used.rss / 1024 / 1024)}MB`, // 常驻内存
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`, // 堆总大小
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`, // 堆已使用
external: `${Math.round(used.external / 1024 / 1024)}MB` // C++ 对象
});
// 定期监控
setInterval(() => {
const usage = process.memoryUsage();
if (usage.heapUsed > 500 * 1024 * 1024) { // 超过 500MB
console.warn('内存使用过高!');
}
}, 60000);
// 生成堆快照
const v8 = require('v8');
const fs = require('fs');
const snapshot = v8.writeHeapSnapshot();
console.log('堆快照已保存:', snapshot);追问:常见的内存泄漏场景有哪些?
30. 什么是事件循环阻塞?如何避免?
答案要点:
- 阻塞:同步操作或长时间 CPU 计算占用事件循环,导致其他任务无法执行
- 检测:
blocked-at库、--perf-basic-prof性能分析 - 避免方法:
- 拆分大任务为小任务
- 使用
setImmediate()让出事件循环 - CPU 密集型任务用 Worker Threads
// ❌ 阻塞事件循环
function heavyCompute() {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
return sum;
}
// ✅ 拆分任务
function heavyComputeAsync(max, callback) {
let sum = 0;
let i = 0;
function chunk() {
const end = Math.min(i + 1e6, max);
for (; i < end; i++) {
sum += i;
}
if (i < max) {
setImmediate(chunk); // 让出事件循环
} else {
callback(sum);
}
}
chunk();
}
heavyComputeAsync(1e9, (result) => {
console.log('计算完成:', result);
});追问:如何测量事件循环延迟?
安全
31. Node.js 应用常见的安全问题有哪些?
答案要点:
- 依赖漏洞:使用
npm audit检查 - 注入攻击:SQL 注入、命令注入、XSS
- 敏感信息泄露:环境变量、日志
- 拒绝服务(DoS):正则表达式 ReDoS、大文件上传
- 不安全的反序列化:
JSON.parse原型污染
// ❌ SQL 注入风险
const userId = req.query.id;
db.query(`SELECT * FROM users WHERE id = ${userId}`); // 危险!
// ✅ 使用参数化查询
db.query('SELECT * FROM users WHERE id = ?', [userId]);
// ❌ 命令注入风险
const { exec } = require('child_process');
exec(`ping ${req.query.host}`); // 危险!
// ✅ 使用白名单验证
const allowedHosts = ['google.com', 'example.com'];
if (allowedHosts.includes(req.query.host)) {
exec(`ping ${req.query.host}`);
}
// 环境变量管理
require('dotenv').config();
const apiKey = process.env.API_KEY; // 不要硬编码密钥追问:如何防止原型污染攻击?
32. 如何安全地处理用户输入?
答案要点:
- 验证:使用 Joi、Zod 等库验证输入
- 转义:防止 XSS(使用
escape-html) - 限制大小:防止 DoS
- HTTPS:加密传输
const Joi = require('joi');
// 输入验证
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(150)
});
app.post('/register', (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
// 使用验证后的数据
createUser(value);
});
// 防止 XSS
const escapeHtml = require('escape-html');
const userInput = req.body.comment;
const safe = escapeHtml(userInput);
// 限制请求大小
app.use(express.json({ limit: '1mb' }));追问:什么是 CSRF 攻击?如何防御?