Node.js面试题及详细答案120题(01-15) -- 基础概念篇
《前后端面试题
》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,MySQL,Linux… 。
文章目录
- 一、本文面试题目录
- 1. 什么是Node.js?它的特点是什么?
- 2. Node.js和浏览器的JavaScript有什么区别?
- 3. Node.js的运行环境由哪些部分组成?
- 4. 为什么Node.js适合处理高并发场景?
- 5. Node.js的事件循环(Event Loop)是什么?它的执行流程是怎样的?
- 6. 事件循环的六个阶段分别是什么?每个阶段做什么?
- 7. 什么是V8引擎?它在Node.js中的作用是什么?
- 8. Node.js的单线程模型是指什么?它有什么优缺点?
- 9. Node.js中的全局对象有哪些?和浏览器的全局对象有何不同?
- 10. 什么是进程(Process)和线程(Thread)?Node.js如何处理多线程?
- 11. Node.js的模块化系统遵循什么规范?和ES6模块有什么区别?
- 12. `require`方法的加载机制是怎样的?
- 13. `module.exports`和`exports`的区别是什么?
- 14. 什么是CommonJS模块?它的优缺点是什么?
- 15. Node.js中的`__dirname`和`__filename`有什么作用?
- 二、120道Node.js面试题目录列表
一、本文面试题目录
1. 什么是Node.js?它的特点是什么?
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,允许开发者在服务器端使用 JavaScript 编写代码。它不是一门编程语言,也不是一个框架,而是一个让 JavaScript 能够脱离浏览器运行的平台。
主要特点:
- 非阻塞 I/O 模型:在执行 I/O 操作(如文件读写、网络请求)时不会阻塞线程,提高了程序的执行效率。
- 事件驱动:通过事件循环机制处理各种事件(如请求、数据到达等),实现高效的并发处理。
- 单线程:主线程是单线程的,但通过事件循环和底层线程池处理并发任务。
- 跨平台:可在 Windows、Linux、macOS 等多个操作系统上运行。
- 丰富的生态系统:npm(Node Package Manager)提供了海量的第三方模块,方便开发者快速构建应用。
示例:使用 Node.js 创建一个简单的服务器
const http = require('http');const server = http.createServer((req, res) => {res.writeHead(200, { 'Content-Type': 'text/plain' });res.end('Hello, Node.js!\n');
});server.listen(3000, () => {console.log('Server running at http://localhost:3000/');
});
2. Node.js和浏览器的JavaScript有什么区别?
虽然 Node.js 和浏览器都使用 JavaScript 作为编程语言,但它们在运行环境、用途和特性上有显著区别:
区别 | Node.js | 浏览器JavaScript |
---|---|---|
运行环境 | 服务器端(独立运行,无需浏览器) | 客户端(嵌入在浏览器中) |
核心用途 | 构建服务器、后端服务、工具脚本等 | 实现网页交互、DOM操作、前端逻辑 |
API 差异 | 提供文件系统(fs)、网络(http)等模块 | 提供 DOM、BOM、Canvas 等浏览器相关API |
全局对象 | 全局对象为 global | 全局对象为 window |
模块系统 | 遵循 CommonJS 规范(使用 require ) | 主要使用 ES6 模块(import /export ) |
事件处理 | 基于 EventEmitter 实现事件驱动 | 基于 DOM 事件模型(如 onclick ) |
I/O 操作 | 支持非阻塞 I/O 操作 | 受浏览器安全限制,I/O 能力有限 |
示例:Node.js 中读取文件(浏览器无法直接实现)
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {if (err) throw err;console.log(data);
});
3. Node.js的运行环境由哪些部分组成?
Node.js 的运行环境主要由以下核心组件构成:
- V8 引擎:Google 开发的 JavaScript 引擎,负责解析和执行 JavaScript 代码,提供高效的代码编译和运行能力。
- Libuv:一个跨平台的异步 I/O 库,提供事件循环、非阻塞 I/O 操作(如文件系统、网络)、线程池等核心功能。
- Node.js 核心模块:由 C++ 和 JavaScript 编写的内置模块,如
http
、fs
、path
等,封装了底层功能。 - 第三方模块:通过 npm 安装的开源模块,扩展了 Node.js 的功能。
- 事件循环(Event Loop):Node.js 处理非阻塞 I/O 操作的核心机制,负责调度回调函数的执行。
- 线程池:由 Libuv 管理,用于处理耗时的 I/O 操作(如文件读写、DNS 解析),避免阻塞事件循环。
工作流程:当 Node.js 执行代码时,V8 引擎解析 JavaScript,遇到 I/O 操作时交给 Libuv 处理,Libuv 通过事件循环和线程池管理这些操作,完成后将结果通过回调函数返回给 V8 引擎执行。
4. 为什么Node.js适合处理高并发场景?
Node.js 适合高并发场景的核心原因在于其非阻塞 I/O 模型和事件驱动机制,具体表现为:
-
非阻塞 I/O:当执行 I/O 操作(如数据库查询、网络请求)时,Node.js 不会阻塞主线程,而是将操作交给底层线程池处理,主线程可以继续处理其他任务。
-
事件驱动:通过事件循环机制,Node.js 能够高效地监听和处理各种事件(如请求到达、数据返回),回调函数在事件触发时才会被执行,避免了资源浪费。
-
单线程优势:主线程是单线程的,避免了多线程切换的开销(如上下文切换、锁竞争),适合处理大量轻量级的并发请求(如 API 接口调用)。
-
高吞吐量:对于 I/O 密集型任务(如 Web 服务器、实时通信),Node.js 能在有限的资源下处理更多请求,吞吐量远高于传统多线程模型。
示例:Node.js 处理高并发请求(无需等待前一个请求完成)
const http = require('http');// 模拟一个耗时的I/O操作(非阻塞)
const asyncOperation = (callback) => {setTimeout(() => {callback('Operation done');}, 100);
};const server = http.createServer((req, res) => {asyncOperation((result) => {res.end(result);});
});server.listen(3000, () => {console.log('Server running on port 3000');
});
注意:Node.js 不适合 CPU 密集型任务(如大规模计算),因为单线程会被长时间阻塞,导致并发能力下降。
5. Node.js的事件循环(Event Loop)是什么?它的执行流程是怎样的?
事件循环是 Node.js 处理非阻塞 I/O 操作的核心机制,允许单线程的 Node.js 执行非阻塞操作。它不断从事件队列中取出任务并执行,实现了异步代码的有序执行。
执行流程:
- 执行同步代码,将异步任务(如
setTimeout
、fs.readFile
)放入对应的队列(宏任务队列或微任务队列)。 - 同步代码执行完毕后,进入事件循环。
- 依次执行当前微任务队列中的所有任务(直到清空)。
- 进入事件循环的某个阶段,执行该阶段的任务队列中的任务。
- 执行完该阶段任务后,检查是否有微任务,若有则重复步骤 3。
- 重复步骤 4-5,直到所有任务队列清空。
示例:事件循环执行顺序演示
console.log('同步代码开始');setTimeout(() => {console.log('setTimeout(宏任务)');
}, 0);Promise.resolve().then(() => {console.log('Promise.then(微任务)');
});console.log('同步代码结束');// 输出顺序:
// 同步代码开始
// 同步代码结束
// Promise.then(微任务)
// setTimeout(宏任务)
6. 事件循环的六个阶段分别是什么?每个阶段做什么?
Node.js 的事件循环分为六个阶段,按顺序依次执行,每个阶段都有一个任务队列:
-
timers(定时器阶段):
- 执行
setTimeout()
和setInterval()
调度的回调函数。 - 检查是否有到期的定时器,若有则执行其回调。
- 执行
-
pending callbacks(待定回调阶段):
- 执行延迟到下一个循环迭代的 I/O 回调(如某些系统操作的回调)。
-
idle, prepare:
- 内部使用,开发者几乎不会涉及。
-
poll(轮询阶段):
- 检索新的 I/O 事件(如文件读写、网络请求)。
- 执行 I/O 相关的回调(除了 timers、close callbacks 等)。
- 若轮询队列不为空,依次执行回调;若为空,等待新的事件到达或进入下一个阶段。
-
check(检查阶段):
- 执行
setImmediate()
调度的回调函数(立即执行,优先级高于setTimeout
)。
- 执行
-
close callbacks(关闭回调阶段):
- 执行关闭相关的回调(如
socket.on('close', ...)
)。
- 执行关闭相关的回调(如
示例:setImmediate
与 setTimeout
的执行顺序
// 在 I/O 回调中,setImmediate 先于 setTimeout 执行
const fs = require('fs');
fs.readFile(__filename, () => {setTimeout(() => {console.log('setTimeout');}, 0);setImmediate(() => {console.log('setImmediate');});
});
// 输出:setImmediate → setTimeout
7. 什么是V8引擎?它在Node.js中的作用是什么?
V8 是由 Google 开发的开源 JavaScript 引擎,采用 C++ 编写,最初用于 Chrome 浏览器,后被 Node.js 采用作为其 JavaScript 执行引擎。
V8 在 Node.js 中的作用:
- 代码解析与编译:将 JavaScript 代码解析为抽象语法树(AST),再通过 JIT(即时编译)将 AST 转换为机器码,提高执行效率。
- 内存管理:负责 JavaScript 对象的内存分配、垃圾回收(采用分代回收机制)。
- 执行代码:运行 JavaScript 代码,包括同步代码、回调函数等。
- 提供基础 API:如
Object
、Array
等内置对象和方法。
特点:
- 高效的 JIT 编译:将热点代码(频繁执行的代码)编译为机器码,避免解释执行的性能损耗。
- 快速的垃圾回收:采用分代回收策略,对年轻代和老年代对象分别进行回收,减少停顿时间。
Node.js 结合 V8 引擎和 Libuv 库,既实现了高效的 JavaScript 执行,又提供了非阻塞 I/O 能力。
8. Node.js的单线程模型是指什么?它有什么优缺点?
Node.js 的单线程模型是指其主线程(执行 JavaScript 代码的线程)是单线程的,即同一时间只能执行一个任务。但 Node.js 底层通过 Libuv 管理的线程池(默认4个线程)处理 I/O 操作,实现了并发。
优点:
- 减少线程开销:避免了多线程切换的上下文切换成本和锁竞争问题。
- 简化编程模型:开发者无需处理多线程同步问题,降低了代码复杂度。
- 高效处理 I/O 密集型任务:适合处理大量并发的 I/O 请求(如 API 服务、实时通信)。
缺点:
- 不适合 CPU 密集型任务:长时间的 CPU 计算会阻塞主线程,导致事件循环无法执行,影响整个应用的响应。
- 单线程崩溃风险:主线程一旦抛出未捕获的异常,可能导致整个应用崩溃。
- 无法充分利用多核 CPU:单线程只能使用一个 CPU 核心,需通过 Cluster 模块实现多核利用。
示例:CPU 密集型任务阻塞事件循环
// 耗时的计算函数(阻塞主线程)
function heavyCalculation() {let result = 0;for (let i = 0; i < 1e9; i++) {result += i;}return result;
}console.log('开始计算');
heavyCalculation();
console.log('计算结束'); // 需等待计算完成才会执行,期间无法处理其他请求
No. | 大剑师精品GIS教程推荐 |
---|---|
0 | 地图渲染基础- 【WebGL 教程】 - 【Canvas 教程】 - 【SVG 教程】 |
1 | Openlayers 【入门教程】 - 【源代码+示例 300+】 |
2 | Leaflet 【入门教程】 - 【源代码+图文示例 150+】 |
3 | MapboxGL 【入门教程】 - 【源代码+图文示例150+】 |
4 | Cesium 【入门教程】 - 【源代码+综合教程 200+】 |
5 | threejs 【中文API】 - 【源代码+图文示例200+】 |
6 | Shader 编程 【图文示例 100+】 |
9. Node.js中的全局对象有哪些?和浏览器的全局对象有何不同?
Node.js 中的全局对象是 global
,类似于浏览器中的 window
,但提供的属性和方法有所不同。
Node.js 主要全局对象/属性:
global
:全局对象本身,所有全局变量都是其属性。console
:用于输出日志(如console.log
)。process
:提供当前 Node.js 进程的信息和控制方法(如process.env
、process.exit()
)。Buffer
:用于处理二进制数据。setTimeout
/setInterval
/clearTimeout
:定时器函数。setImmediate
/clearImmediate
:立即执行的回调函数。require
:加载模块的函数。module
/exports
:模块相关对象。__dirname
/__filename
:当前模块的目录和文件路径。
与浏览器全局对象的区别:
- 浏览器的全局对象是
window
,Node.js 是global
。 - 浏览器中全局变量自动成为
window
的属性,Node.js 中只有显式赋值给global
的变量才是全局的。 - 浏览器提供
document
、location
等 DOM/BOM API,Node.js 没有。 - Node.js 提供
process
、Buffer
等服务器端特有的全局对象,浏览器没有。
示例:Node.js 中使用全局对象
console.log(global === this); // true(在模块顶层,this 指向 module.exports,此处为 false;在函数中 this 指向 global)
console.log(__dirname); // 当前模块所在目录
console.log(process.pid); // 当前进程 ID
10. 什么是进程(Process)和线程(Thread)?Node.js如何处理多线程?
进程(Process):操作系统分配资源的基本单位,拥有独立的内存空间、文件描述符等,进程间相互隔离。
线程(Thread):进程内的执行单元,共享进程的资源(如内存),一个进程可以包含多个线程,线程间切换成本低于进程。
Node.js 的多线程处理:
- Node.js 主线程是单线程的(执行 JavaScript 代码),但底层通过 Libuv 管理一个线程池(默认4个线程),用于处理耗时的 I/O 操作(如文件读写、DNS 解析)。
- 对于 CPU 密集型任务,可通过
child_process
模块创建子进程,或使用worker_threads
模块创建工作线程,实现多线程并行处理。
child_process 示例:创建子进程
const { fork } = require('child_process');// 创建子进程
const child = fork('./child.js');// 主进程与子进程通信
child.on('message', (msg) => {console.log('主进程收到:', msg);
});
child.send('Hello from parent');
worker_threads 示例:创建工作线程
const { Worker } = require('worker_threads');const worker = new Worker(`const { parentPort } = require('worker_threads');parentPort.postMessage('Hello from worker');
`, { eval: true });worker.on('message', (msg) => {console.log('主线程收到:', msg);
});
11. Node.js的模块化系统遵循什么规范?和ES6模块有什么区别?
Node.js 的模块化系统默认遵循 CommonJS 规范,通过 require
加载模块,module.exports
导出模块。ES6 模块(ESM)是 JavaScript 官方的模块化标准,使用 import
和 export
。
CommonJS 与 ES6 模块的区别:
区别 | CommonJS | ES6 模块 |
---|---|---|
加载方式 | require('module') | import module from 'module' |
导出方式 | module.exports 或 exports | export 或 export default |
加载时机 | 运行时动态加载(同步) | 编译时静态分析(可异步) |
值的传递 | 拷贝导出的值(原始值)或引用(对象) | 导出值的引用(动态绑定) |
this 指向 | 模块内 this 指向 module.exports | 模块内 this 指向 undefined |
文件扩展名 | 默认 .js 、.json 、.node | 通常使用 .mjs 或在 package.json 中指定 "type": "module" |
CommonJS 示例:
// 导出(module.js)
module.exports = {foo: 'bar',add: (a, b) => a + b
};// 导入(main.js)
const mod = require('./module');
console.log(mod.foo); // 'bar'
ES6 模块示例:
// 导出(module.mjs)
export const foo = 'bar';
export default (a, b) => a + b;// 导入(main.mjs)
import add, { foo } from './module.mjs';
console.log(foo); // 'bar'
console.log(add(1, 2)); // 3
注意:Node.js 中使用 ES6 模块需将文件扩展名改为 .mjs
,或在 package.json
中设置 "type": "module"
。
12. require
方法的加载机制是怎样的?
require
是 Node.js 中用于加载模块的函数,其加载机制遵循以下流程:
-
缓存检查:优先从缓存中加载模块(
require.cache
),若已加载则直接返回缓存的模块,避免重复加载。 -
判断模块类型:
- 核心模块(如
fs
、http
):直接加载 Node.js 内置的核心模块,优先级最高。 - 路径模块:以
./
、../
或/
开头的路径,按路径查找文件。 - 第三方模块:非路径模块,从当前目录的
node_modules
文件夹开始查找,若未找到则向上级目录的node_modules
查找,直到根目录或找到模块。
- 核心模块(如
-
文件查找规则:
- 若路径指向文件,直接加载(支持
.js
、.json
、.node
扩展名,可省略)。 - 若路径指向目录,查找该目录下的
package.json
中的main
字段指定的入口文件;若没有package.json
或main
字段,默认加载index.js
。
- 若路径指向文件,直接加载(支持
-
编译执行:找到模块后,Node.js 会将其编译为函数并执行,最终返回
module.exports
的值。
示例:require
加载不同类型的模块
// 加载核心模块
const fs = require('fs');// 加载路径模块(相对路径)
const myModule = require('./myModule');// 加载第三方模块(需先通过 npm 安装)
const lodash = require('lodash');
13. module.exports
和exports
的区别是什么?
module.exports
和 exports
都是 Node.js 中用于导出模块内容的对象,它们的关系和区别如下:
- 关系:
exports
是module.exports
的引用(即exports = module.exports
),初始时指向同一个空对象。 - 区别:
module.exports
是模块实际导出的对象,require
最终返回的是module.exports
的值。- 若直接给
exports
赋值(如exports = { foo: 'bar' }
),会切断其与module.exports
的关联,导致导出失效;而修改module.exports
则会直接改变导出结果。
正确用法示例:
// 方式1:修改 exports 的属性(推荐)
exports.foo = 'bar';
exports.add = (a, b) => a + b;// 方式2:修改 module.exports(适用于导出单个值)
module.exports = (a, b) => a + b;// 错误用法:直接赋值 exports 会导致导出失效
exports = { foo: 'bar' }; // 外部 require 无法获取该对象
注意:若同时使用 exports
和 module.exports
,module.exports
的值会覆盖 exports
的属性(因为最终导出的是 module.exports
)。
14. 什么是CommonJS模块?它的优缺点是什么?
CommonJS 是一套用于 JavaScript 模块化的规范,主要用于服务器端(如 Node.js),定义了模块的导入、导出和依赖管理方式。
核心规范:
- 每个文件是一个独立的模块,拥有独立的作用域。
- 通过
module.exports
或exports
导出模块内容。 - 通过
require()
导入其他模块。 - 模块加载是同步的,且加载后会被缓存。
优点:
- 简单易用:语法简洁,易于理解和使用。
- 同步加载:适合服务器端环境,模块文件存储在本地,同步加载性能影响小。
- 缓存机制:模块加载后会被缓存,提高重复加载的效率。
- 广泛支持:Node.js 原生支持,是后端 JavaScript 模块化的事实标准。
缺点:
- 同步加载不适合浏览器:浏览器加载模块需从网络获取,同步加载会阻塞页面渲染,因此浏览器端更适合异步加载的 ES6 模块。
- 动态加载限制:
require
可以在代码任意位置调用(动态加载),但不利于静态分析和 Tree-Shaking(消除未使用的代码)。 - 循环依赖处理复杂:当模块间存在循环依赖时,可能导致部分导出内容未定义。
示例:CommonJS 模块的循环依赖
// a.js
const b = require('./b');
console.log('a.js 中 b.foo:', b.foo);
module.exports = { foo: 'a' };// b.js
const a = require('./a');
console.log('b.js 中 a.foo:', a.foo); // 输出 undefined(此时 a 模块尚未完成导出)
module.exports = { foo: 'b' };
15. Node.js中的__dirname
和__filename
有什么作用?
__dirname
和 __filename
是 Node.js 模块中的全局变量(实际是模块作用域的变量,而非 global
的属性),用于获取当前模块的路径信息。
__dirname
:返回当前模块所在目录的绝对路径(字符串)。__filename
:返回当前模块文件的绝对路径(包括文件名)。
特点:
- 路径是绝对路径,不受执行 Node.js 命令的工作目录影响。
- 仅在模块文件中有效,在 REPL 环境或非模块文件中未定义。
- 可结合
path
模块处理路径(如拼接、解析)。
示例:
const path = require('path');console.log('当前文件路径:', __filename);
// 输出:/home/user/project/main.jsconsole.log('当前目录路径:', __dirname);
// 输出:/home/user/project// 拼接路径
const configPath = path.join(__dirname, 'config', 'app.json');
console.log('配置文件路径:', configPath);
// 输出:/home/user/project/config/app.json
注意:在 ES6 模块(.mjs
或 "type": "module"
)中,__dirname
和 __filename
未定义,需通过 import.meta.url
手动计算:
import { fileURLToPath } from 'url';
import { dirname } from 'path';const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
二、120道Node.js面试题目录列表
文章序号 | Node.js面试题120道 |
---|---|
1 | Node.js面试题及详细答案120道(01-15) |
2 | Node.js面试题及详细答案120道(16-30) |
3 | Node.js面试题及详细答案120道(31-42) |
4 | Node.js面试题及详细答案120道(43-55) |
5 | Node.js面试题及详细答案120道(56-68) |
6 | Node.js面试题及详细答案120道(69-80) |
7 | Node.js面试题及详细答案120道(81-92) |
8 | Node.js面试题及详细答案120道(93-100) |
9 | Node.js面试题及详细答案120道(101-110) |
10 | Node.js面试题及详细答案120道(111-120) |