Appearance
Node.js 面试题
📄 来源图片:node.jpeg 🕐 解析时间:2025-01-13
📋 面试题目
Q1: CommonJS和ES6中模块引入的区别?
A:
- 加载方式:CommonJS 是运行时加载,ES6 模块是编译时输出接口(静态加载)。
- 加载效率:ES6 模块由于是静态解析,效率更高,且支持 Tree-shaking。
- 值的引用:CommonJS 模块输出的是一个值的拷贝,一旦输出,模块内部的变化不会影响这个值;ES6 模块输出的是值的引用,模块内部的变化会反映在外部。
- 加载顺序:CommonJS 是同步加载,适用于服务端(文件在本地磁盘);ES6 模块支持异步加载,适用于浏览器端(需要网络请求)。
- this 指向:CommonJS 中
this指向当前模块;ES6 模块中this为undefined。
Q2: 为什么Node在使用ES Module时必须加上文件扩展名?
A: 这是为了遵循浏览器的规范以及 Node.js 的设计决策:
- 浏览器规范一致性:在浏览器中,URL 必须明确且完整。ESM 最初是为浏览器设计的,要求明确的路径和扩展名以减少服务器猜测开销。
- 减少推测开销:不带扩展名会导致 Node.js 必须尝试查找
.js、.json、.node等后缀,这涉及多次文件系统 I/O,会降低性能。 - 明确性:在分布式网络环境中,明确的扩展名可以避免模块解析歧义。
Q3: 浏览器和 Node 中的事件循环有什么区别?
A:
- 宏任务/微任务队列数:
- 浏览器:通常只有一个宏任务队列(Task Queue)和一个微任务队列(Microtask Queue)。
- Node:包含 6 个阶段的宏任务队列(如 timers, pending, idle, poll, check, close)。
- 执行顺序:
- 浏览器:每执行完一个宏任务,就会清空整个微任务队列。
- Node (Node 11 之前):执行完一个阶段的所有宏任务后,才执行微任务。
- Node (Node 11 之后):行为��浏览器趋于一致,每个宏任务执行完后立即执行微任务。
- 特有 API:Node 环境中有
process.nextTick(优先级高于所有微任务)和setImmediate(在 check 阶段执行)。
Q4: Node性能如何进行监控以及优化?
监控方案:
- 内置工具:使用
process.memoryUsage()查看内存,process.cpuUsage()查看 CPU 使用情况。 - 日志分析:通过 PM2 监控日志,或使用 ELK 堆栈(Elasticsearch, Logstash, Kibana)。
- APM 工具:使用 SkyWalking、Elastic APM、New Relic 等专业性能监控工具。
- Heapdump:在内存泄露时生成快照,通过 Chrome DevTools 分析。
优化策略:
- 代码层面:避免同步 I/O 操作(使用异步版本),减少内存泄露(及时释放闭包、定时器),使用更高效的算法。
- 并发处理:合理利用
Promise.all并发请求。 - 多核利用:使用
cluster模块或 PM2 的 cluster 模式开启多进程。 - 静态资源分离:将静态资源交给 Nginx 或 CDN,减轻 Node 服务负担。
Q5: 如果让你来设计一个分页功能,你会怎么设计?前后端如何交互?
设计思路:
- 前端传参:
page:当前页码(从 1 开始)。pageSize:每页显示的条数。keyword/filter:可选的搜索关键词或过滤条件。
- 后端处理:
- 计算偏移量:
offset = (page - 1) * pageSize。 - SQL 查询:
SELECT * FROM table LIMIT pageSize OFFSET offset。 - 获取总数:
SELECT COUNT(*) FROM table(用于前端计算总页数)。
- 计算偏移量:
- 后端返回:json
{ "code": 200, "data": { "list": [...], "total": 100, "page": 1, "pageSize": 10 } } - 交互流程:前端初始化请求第一页数据 -> 后端返回数据和总条数 -> 前端渲染列表及分页器 -> 用户点击翻页 -> 重复上述流程。
Q6: 如何实现文件上传?说说你的思路
A:
- 前端实现:
- 使用
<input type="file">选择文件。 - 使用
FormData对象包装文件对象。 - 通过 Axios 或 Fetch 发送
multipart/form-data格式的 POST 请求。
- 使用
- 后端实现:
- 使用中间件解析请求体,如
multer、formidable或busboy。 - 存储策略:
- 本地存储:将文件移动到服务器指定目录。
- 云存储:将文件上传到阿里云 OSS、腾讯云 COS 或 AWS S3。
- 使用中间件解析请求体,如
- 大文件优化:
- 分片上传:前端将大文件切分成小块,并发上传。
- 断点续传:后端记录已上传的分片索引,中断后只需补传缺失分片。
- 秒传:前端计算文件 MD5,后端匹配已存在的文件则直接返回成功。
Q7: 如何实现 JWT 鉴权机制?说说你的思路
A:
- JWT 结构:由
Header(算法/类型)、Payload(用户信息/载荷)、Signature(签名)三部分组成,通过.连接。 - 登录流程:用户提交用户名密码 -> 后端校验成功 -> 生成包含用户 ID 的 JWT(使用私钥签名)-> 返回给前端。
- 存储与发送:前端通常将 JWT 存入
localStorage,后续请求在 Header 的Authorization: Bearer<token>`` 中携带。 - 校验流程:后端拦截请求 -> 提取 Token -> 使用密钥校验签名是否有效、Token 是否过期 -> 校验成功则将用户信息存入
req.user并放行。 - 优点:无状态、可扩展性强、支持跨域。
Q8: 说说对中间件概念的理解,如何封装Node中间件?
理解:中间件是介于请求(Request)和响应(Response)之间的一个处理环节,它本质上是一个函数,可以访问请求对象、响应对象和下一个中间件函数 next。
封装方式:
普通中间件 (Express 风格):
javascriptfunction logger(req, res, next) { console.log(`${req.method} ${req.url}`); next(); // 必须调用,否则请求会挂起 }Koa 风格 (洋葱模型):
javascriptconst myMiddleware = async (ctx, next) => { console.log('进入中间件'); await next(); console.log('退出中间件'); };工厂函数封装(支持配置参数):
javascriptconst auth = (role) => (ctx, next) => { if (ctx.user.role !== role) ctx.throw(403); return next(); };
Q9: 说说 Node 文件查找的优先级以及 Require 方法的文件查找策略?
查找优先级:
- 缓存:优先从
require.cache中读取。 - 核心模块:如
fs、http等,优先级最高。 - 文件模块:以
/、./、../开头的路径。 - 包模块:非路径开头的模块,会递归查找当前及上级目录的
node_modules。
具体策略:
- 如果路径带扩展名,直接查找。
- 不带扩展名时,按顺序尝试:
.js->.json->.node。 - 如果是文件夹,查找
package.json中的main字段;若无,查找index.js->index.json->index.node。
Q10: 说说对Node.js中的事件循环机制理解?
A: Node.js 的事件循环是 Libuv 库实现的异步执行机制。它包含 6 个主要阶段:
- timers:执行
setTimeout和setInterval的回调。 - pending callbacks:执行某些系统操作的回调(如 TCP 错误)。
- idle, prepare:内部使用。
- poll:检索新的 I/O 事件;执行 I/O 相关回调。
- check:执行
setImmediate的回调。 - close callbacks:执行关闭连接的回调,如
socket.on('close', ...)。
特别说明:process.nextTick 不属于事件循环的任何阶段,它在每个阶段完成后的切换间隙被执行,且优先级高于其他微任务。
Q11: 说说Node中的EventEmitter?如何实现一个EventEmitter?
EventEmitter 是 Node.js 核心模块 events 提供的一个类,用于实现观察者模式(发布/订阅)。
手动实现思路:
javascript
class EventEmitter {
constructor() {
this.events = {};
}
on(name, cb) {
if (!this.events[name]) this.events[name] = [];
this.events[name].push(cb);
}
emit(name, ...args) {
if (this.events[name]) {
this.events[name].forEach(cb => cb(...args));
}
}
off(name, cb) {
if (this.events[name]) {
this.events[name] = this.events[name].filter(fn => fn !== cb);
}
}
once(name, cb) {
const wrapper = (...args) => {
cb(...args);
this.off(name, wrapper);
};
this.on(name, wrapper);
}
}Q12: 说说对 Node 中的 Stream 的理解?应用场景?
理解:Stream(流)是处理流式数据的抽象接口。它可以像水流一样,分段读取和写入数据,而不需要将整个文件一次性加载到内存中,极大地节省了内存空间。
类型:
- Readable:可读流(如
fs.createReadStream)。 - Writable:可写流(如
fs.createWriteStream)。 - Duplex:双工流(可读可写,如 TCP socket)。
- Transform:转换流(读写过程中修改数据,如
zlib压缩)。
应用场景:处理大文件上传/下载、视频串流、日志实时处理、网络请求转发等。
Q13: 说说对Node中的Buffer的理解?应用场景?
理解:Buffer 是 Node.js 中用于处理二进制数据的类。由于 JavaScript 最初只支持字符串,Buffer 提供了一种在 V8 堆外分配固定大小内存的方法,用于存放原始二进制字节。
应用场景:
- 文件 I/O:读写文件内容。
- 网络传输:处理 TCP 流或网络数据包。
- 图片/音视频处理:对多媒体文件进行编码或像素级操作。
- 加密/解密:转换哈希值或密钥。
Q14: 说说对Node中的fs模块的理解?有哪些常用方法
A:fs (File System) 模块提供了一组 API 用于与文件系统进行交互。
常用方法:
- 读取:
readFile(异步),readFileSync(同步),createReadStream(流)。 - 写入:
writeFile,appendFile,createWriteStream。 - 目录操作:
mkdir,readdir,rmdir。 - 信息获取:
stat(获取文件大小、创建时间等)。 - 权限/链接:
chmod,rename,unlink(删除文件)。 - 监听:
watchFile,watch。
Q15: 说说对 Node 中的 process 的理解?有哪些常用方法?
A:process 是一个全局对象,提供了当前 Node.js 进程的相关信息和控制能力。
常用属性/方法:
process.argv:命令行参数数组。process.env:系统环境变量对象。process.cwd():当前工作目录。process.nextTick():将回调放入微任务队列。process.exit():强制退出当前进程。process.on('uncaughtException', ...):监听未捕获异常。process.memoryUsage():内存占用详情。
Q16: Node.js 有哪些全局对象?
A:
- 基础对象:
global(全局命名空间)、process、console。 - 定时��:
setTimeout、setInterval、setImmediate、clearTimeout等。 - 模块系统相关:
__dirname(当前目录路径)、__filename(当前文件路径)、require、module、exports。(注意:这些在 ESM 中不可用,需要通过import.meta模拟)。 - 数据处理:
Buffer、各种TypedArray。 - URL 处理:
URL、URLSearchParams。
Q17: 说说你对Node.js的理解?优缺点?应用场景?
理解:Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时。它采用单线程、非阻塞 I/O 和事件驱动模型。
优点:
- 高性能:V8 引擎解析 JS 速度快。
- 高并发:非阻塞 I/O 能轻松处理数万个并发连接。
- 生态繁荣:NPM 拥有全球最大的开源包仓库。
- 全栈开发:前后端统一语言,降低沟通成本。
缺点:
- 不适合 CPU 密集型任务:长时间计算会阻塞事件循环。
- 可靠性挑战:单线程下,一个未处理异常可能导致整个服务崩溃。
应用场景:I/O 密集型应用(聊天室、实时推送)、中台/网关服务、SSR(服务端渲染)、前端工具链(Webpack, Vite)。
Q18: body-parser 这个中间件是做什么用的?
A:body-parser 是 Express 的中间件,用于解析 HTTP 请求体(Request Body)。
功能:
- 解析 JSON:解析内容类型为
application/json的请求。 - 解析 URL-encoded:解析 HTML 表单提交的数据(
application/x-www-form-urlencoded)。 - 解析文本/原始数据:支持纯文本或 Buffer。
- 数据填充:将解析后的结果挂载到
req.body上,方便后续中间件使用。
Q19: Koa中,如果一个中间件没有调用 await next(),后续的中间件还会执行吗?
不会执行。 Koa 的核心机制是 next() 控制权的显式传递。如果不调用 await next(),执行流程就会在当前中间件停止并开始向上回溯(即"洋葱模型"的返回阶段),下游的中间件和业务逻辑(路由处理等)都将被跳过。
Q20: 在没有async await 的时候,koa是怎么实现的洋葱模型?
A: 在 async/await 普及之前,Koa 1.x 使用了 Generator 函数(生成器)配合 co 库来实现。
- 中间件定义为
function* (next) { ... }。 - 通过
yield next交出执行权。 co库会自动执行 Generator 并将控制权交回,从而模拟出类似的同步编写、异步执行的洋葱模型效果。
Q21: koa框架中,该怎么处理中间件的异常?
A:
- 全局错误处理中间件:在中间件链的最前端(第一个)使用
try-catch包裹next()。javascriptapp.use(async (ctx, next) => { try { await next(); } catch (err) { ctx.status = err.status || 500; ctx.body = { message: err.message }; ctx.app.emit('error', err, ctx); // 触发全局 error 事件 } }); - 监听 error 事件:使用
app.on('error', (err, ctx) => { ... })进行集中的日志记录。 - 使用 ctx.throw:在业务逻辑中通过
ctx.throw(400, '参数错误')抛出规范的异常。
Q22: Node.js 如何调试?
A:
- Console 调试:简单粗暴的
console.log。 - Node 内置调试器:运行
node inspect app.js。 - Chrome DevTools:
- 运行
node --inspect app.js。 - 打开 Chrome 浏览器,输入
chrome://inspect,点击 "Open dedicated DevTools for Node"。
- 运行
- VS Code 调试:在编辑器中按 F5 启动调试配置,支持设置断点、单步执行和变量监视。
- NDB:Google 推出的更强大的调试工具。
Q23: 说说你对 koa 洋葱模型的理解
洋葱模型(Onion Model)是指 Koa 中间件的一种执行机制。
- 进入阶段:请求从第一个中间件开始执行,遇到
next()时进入下一个中间件。 - 核心阶段:依次进入,直到最后一个中间件(通常是路由或业务逻辑)。
- 回溯阶段:业务逻辑执行完后,控制权会按照相反的顺序(从内向外)回到每个中间件
next()之后的代码。
优势:这种模式让中间件可以非常方便地在"请求前"和"响应后"执行逻辑,例如计算请求耗时、统一处理错误或格式化响应结果。
💡 参考答案提示
Q1 CommonJS vs ES Module:
- CommonJS:动态加载、运行时同步加载、值拷贝、this指向当前模块
- ES Module:静态解析、编译时加载、值引用(只读)���this指向undefined
Q3 事件循环区别:
- 浏览器:宏任务(setTimeout/DOM事件)+ 微任务(Promise/mutationObserver)
- Node:6个阶段(timers/pending/check等)+ process.nextTick和微任务队列
Q7 JWT鉴权: Header(算法类型)+ Payload(用户信息/过期时间)+ Signature(密钥签名),服务端验证签名有效性
Q8 中间件: 函数形式 (req, res, next) => {},按顺序执行,调用next()进入下一个
Q11 EventEmitter: Node内置事件模块,通过on监听、emit触发、off移除,实现发布订阅模式
Q12 Stream: 流式处理数据,类型:Readable/Writable/Duplex/Transform,适合大文件处理
Q18 body-parser: 解析HTTP请求体,支持JSON/Raw/Text/URL-encoded格式数据
Q19 Koa next(): 不会执行,洋葱模型要求必须通过await next()将控制权交给下游中间件
Q23 洋葱模型: 请求从外层进入,依次穿过所有中间件,到达最内层后反向返回,每个中间件可以执行两次(进和出)