Node.js 基础
Node.js 是一个开源的、跨平台的 JavaScript 运行时环境
。这里要敲黑板划重点了,JavaScript 运行时环境
。 构建在 Chrome 的 V8 引擎
之上。Node.js 是异步事件驱动
的单线程模型
。由于 Node.js 异步非阻塞的特性,因此适用于 I/O 密集型
的应用场景。需要注意的是,Node.js 是单线程模型,需要避免 CPU 的耗时操作。 Node.js 并不是语言,而是一个 JavaScript 运行时环境
,它的语言是 JavaScript。这就跟 PHP、Python、Ruby 这类不一样,它们既代表语言,也可代表执行它们的运行时环境(或解释器)。
大纲
- 什么是 Node.js
- Node.js 环境搭建
- CommonJS 规范
- CommonJS 原理
Node.js 特性:
- 开源、跨平台的 JavaScript 运行时环境
- 构建在 chrome 的 V8 引擎上
- 事件驱动、非阻塞 I/O,适用于 IO 密集型应用
- 单线程模型
Node.js 并非是一门语言,只是一个 JavaScript 的运行时环境
- 什么是 JS 运行时环境
- 为什么 JS 需要特别的运行时环境
- JS 引擎是什么
- v8 引擎是什么
JS 无处不在
- JS 代码在浏览器中如何被执行
浏览器内核
- Gecko
- Trident 微软 ie4 ------ ie11
- WebKit 苹果 khtml 用于 Safari
- Blink WebKit 一个分支 Google 开发
浏览器 | 内核 | JavaScript 引擎 | User Agent 关键词 |
---|---|---|---|
Chrome/Edge | Blink | V8 | AppleWebKit/537.36 |
Safari | WebKit | JavaScriptCore | AppleWebKit/ |
Firefox | Gecko | SpiderMonkey | Gecko/或 Firefox/ |
旧版 IE | Trident | Chakra(旧) | Trident/ |
事实上,我们经常说的浏览器内核指的是浏览器的排版引擎
: 排版引擎
(layout engine),也称为浏览器引擎
(browser engine)、页面渲染引擎
(rendering engine)或样板引擎
。是浏览器的核心组件,负责将网页代码(HTML、CSS、JavaScript)转换为用户可视化的页面。
渲染引擎工作过程

如上图:
- HTML 和 CSS 经过对应的 Parser 解析之后,会形成对应的 DOM Tree 和 CSS Tree;
- 它们经过附加合成之后,会形成一个 Render Tree,同时生成一个 Layout 布局,最终通过浏览器的渲染引擎帮助我们完成绘制,展现出平时看到的 Hmtl 页面;
- 在 HTML 解析过程中,如果遇到了
<script src='xxx'>
,会停止解析 HTML,而优先去加载和执行 JavaScript 代码(此过程由 JavaScript 引擎完成) - 因为 JavaScript 属于高级语言(Python、C++、Java),所以 JavaScript 引擎会先把它转换成汇编语言,再把汇编语言转换成机器语言(二进制 010101),最后被 CPU 所执行。
JS 引擎
为什么需要 JavaScript 引擎呢?
-
事实上我们编写的 JavaScript 无论你交给浏览器或者 Node 执行,最后都是需要被 CPU 执行的;
-
但是 CPU 只认识自己的指令集,实际上是机器语言,才能被 CPU 所执行;
-
所以我们需要 JavaScript 引擎帮助我们将 JavaScript 代码翻译成 CPU 指令来执行;
比较常见的 JavaScript 引擎有哪些呢?
引擎名称 | 所属浏览器/环境 | 特点 |
---|---|---|
SpiderMonkey | Firefox | 首个 JavaScript 引擎(由 Brendan Eich 开发),逐步引入 JIT 优化。 |
Chakra | 旧版 Edge(已弃用) | 微软开发,曾支持异步编译,现被 V8 取代。 |
V8 | Chrome、Edge、Node.js | Google 开发的强大 JavaScript 引擎,也帮助 Chrome 从众多浏览器中脱颖而出。高性能,首创 JIT 分层编译(Ignition + TurboFan),支持 WebAssembly。 |
JavaScriptCore | Safari、iOS 应用 | 苹果开发(原名 Nitro),WebKit 中的 JavaScript 引擎,注重能效比,优化移动端性能。 |
Hermes | React Native | Facebook 专为移动端优化,减少内存占用,提升启动速度。 |
JS → 汇编 → 机器语言 → CPU (指令集)
- SpiderMonkey js 引擎. Firefox 使用
- V8 js 引擎. Chrome、edge、Node.js 使用
- javaScriptCore js 引擎. Safari、ios 使用,优化移动端
- Hermes js 引擎. react-native 使用, facebook 移动端优化
JavaScript引擎
和浏览器内核
之间的联系和不同

- 实际上 Webkit 是有两部分组成:
WebCore
:负责 HTML,CSS 解析,布局,渲染等操作;JavaScriptCore
(JScore):用于解析和执行 JS 代码;JavaScriptCore
(JScore)是 Webkit 中默认的 JS 引擎。另外一个强大的 JavaScript 引擎就是 V8 引擎。
v8 引擎
V8 引擎作为现代 Web 生态的核心技术支撑(Chrome、Node.js、Deno 等均依赖其运行),其设计融合了高性能编译、内存优化与安全增强等关键技术
官方对 V8 引擎的定义:
- 支持语言:V8 是用 C ++编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于 Chrome 和 Node.js 等;
- (译:V8 可以运行 JavaScript 和 WebAssembly 引擎编译的汇编语言等)
- 跨平台:它实现 ECMAScript 和 WebAssembly,并在 Windows 7 或更高版本,macOS 10.12+和使用 x64,IA-32,
- ARM 或 MIPS 处理器的 Linux 系统上运行;
- 嵌入式:V8 可以独立运行,也可以嵌入到任何 C ++应用程序中;
v8 原理
其中的
Parse(解析器)
、lgnition(解释器)
、TurboFan(优化编译器)
都是 V8 引擎的内置模块
js
console.log("hello world");
function sum(num1, num2) {
return num1 + num2;
}
Parse
模块 会将 JavaScript 代码转换成 AST(抽象语法树),这是因为解释器并不直接认识 JavaScript 代码;- 如果函数没有被调用,那么是不会被转换成 AST 的;
Parse
的 V8 官方文档:Parser
lgnition
模块 Ignition 是一个解释器,会将 AST 转换成 ByteCode(字节码);- 同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
- 如果函数只调用一次,Ignition 会执行解释执行 ByteCode;
- Ignition 的 V8 官方文档:Ignition
TurboFan
模块 TurboFan 是一个编译器,可以将字节码编译为 CPU 可以直接执行的机器码;- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过 TurboFan 转换成优化的机器码,提高代码的执行性能;
- 但是,机器码实际上也会被还原为 ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如 sum 函数原来执行的是 number 类型,后来执行变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
- TurboFan 的 V8 官方文档:TurboFan
上面是 JavaScript 代码的执行过程,事实上V8 的内存回收也是其强大的另一个原因
Orinoco
模块:负责垃圾回收,将程序中不需要的内存回收Orinoco
的 V8 官方文档:Orinoco
编程语言会大体分为两大类
- 解释性语言:运行效率相对较低(e.g.
JavaScript
) - 编译性语言:运行效率相对较高(e.g.
C++
)
上述情况对应的是 JavaScript 解释性语言的大体执行流程,但编译型语言往往不是。比如 C++,例如系统内的某些应用程序用 C++编写的,它们在执行的时候会直接转化为机器语言(二进制格式 010101),并交给 CPU 统一执行,这样的运行效率自然相对较高了些。
v8 引擎也对解释性的编程语言做了一个优化,就是上面的TurboFan 优化编译器
,如果一个 JavaScript 函数被多次调用,那么它就会经过TurboFan
转成优化后的机器码,交由 CPU 执行,提高代码的执行性能
浏览器和 Node.js 架构区别

- 在 Chrome 浏览器中
- V8 引擎只是其中一小部分,用来辅助 JavaScript 代码的运行
- 还有一些浏览器的内核用来负责 HTML 解析、布局、渲染等相关工作
- 中间层和操作系统( 网卡、硬盘、显卡......)
- 比如发送网络请求,中间层会调用操作系统中的网卡
- 读取一些本地文件,中间层会调用操作系统中的硬盘
- 浏览器页面的渲染工作,中间层会调用操作系统中的显卡
- 在 Node.js 中
- V8 引擎
- 中间层(libUv) 包括
EventLoop
等 - 操作系统( 网卡、硬盘、显卡...)
Node.js 和 浏览器的区别
相同:
- 都可以运行 JavaScript 代码,都支持 JS 的基本语法 不同:
- node 服务端没有 BOM、DOM。node 不能使用 BOM、DOM 方法
- 浏览器端是不能操作文件的,没有内置模块。浏览器端不可以使用 JS 操作文件
Node.js 架构
- JavaScript 代码会经过 V8 引擎,在通过 Node.js 的
bindings
(Node.js API),将任务派发到 Libuv 的EventLoop
(事件循环) Libuv
提供了事件循环、文件系统读写、网络 IO、线程池等内容Libuv
是使用 C 语言实现的库,运行效率相对较高,适用于 IO 密集型应用
Node.js 代码主要分为三个部分,分别是 C、C++、JavaScript
- JS 代码就是我们平时在使用的那些 JS 模块。比如,
fs
模块,http
模块,path
模块等 - C++ 代码主要分三个部分。
- 第一部分主要是封装
Libuv
和第三方库的C++
代码,比如net
,fs
这些模块都会对应一个 C++ 模块,它主要是对底层的一些封装,暴露给开发者使用 - 第二部分是不依赖
Libuv
和第三方库的C++
代码,比如Buffer
模块的实现 - 第三部分 C++ 代码是 V8 本身的代码
- 第一部分主要是封装
- C 语言代码主要是包括
Libuv
和第三方库的代码,它们都是纯 C 语言实现的代码
Node.js 中主要各部分实现:

Node.js 与 JavaScript 分层
从下往上
梳理:
- 最下面一层是脚本语言规范(Spec),由于我们讲的是 Node.js,所以这里最下层只写 ECMAScript。
- 再往上一层就是对于该规范的实现,如 JavaScript、JScript 以及 ActionScript 等都属于对 ECMAScript 的实现。
- 再往上一层是执行引擎,JavaScript 常见的引擎有 V8、SpiderMonkey、QuickJS 等,解释 js 代码。Node.js 的引擎是 V8。
- 再往上一层是运行时环境,比如基于 V8 封装的运行时环境有 Chromium、Node.js、Deno、CloudFlare Workers 等等。而我们所说的 Node.js 就是在运行时环境这一层。
总之,以后出去千万别再说 Node.js 语言 这个词啦,要说 Node.js 运行时 ,编程语言还是 javascript
。
Node 历史
Node.js 的历史可以追溯到 2009 年,它的诞生和发展深刻影响了现代 Web 开发。以下是其关键阶段的梳理:
- 2009 年:Node.js 的诞生
- 创始人:Ryan Dahl,一位对服务器性能瓶颈不满的开发者。
- 灵感来源:受 **V8 引擎(Chrome 的 JavaScript 引擎)**启发,Dahl 希望用 JavaScript 实现高性能、事件驱动的服务器端开发。
- 首次发布:2009 年 11 月,Node.js 在 JSConf EU 大会亮相,核心思想是非阻塞 I/O 和事件循环,解决了传统服务器(如 Apache)的并发处理瓶颈。
- 早期发展与争议(2010-2014)
npm
的诞生:2010 年,Isaac Schlueter 创建了 Node 包管理器npm
,迅速成为生态系统的基石(现托管超 200 万个包)。- 社区增长:Express.js(2010)、Socket.IO(2010)等框架涌现,推动 Node.js 在实时 Web 应用中的应用。
- 管理争议:Joyent 公司主导 Node.js 期间,社区对开发进度不满,导致io.js 分叉(2014 年),采用更开放的治理模式。
Node.js 能做什么
- 服务端开发:用于做服务器端开发 web 服务器
- 工具:可以基于 node 构建一些工具,构建工作流,比如 npm,webpack,gulp,less,sass 等 vue-cli
- 开发工具或者客户端应用:开发桌面应用程序(借助 node-webkit、electron 等框架实现),比如:vscode、语雀等
- 同构:SSR,借助 Node.js 完成服务端渲染+前后端同构
- serverless
- 流式 SSR
- 游戏
- npm、yarn,pnpm 工具成为前端开发使用最多的工具;
- 爬虫
Node.js 环境搭建
如何选择 Node.js 版本
下载地址:Download | Node.js LTS VS Current Current 版本:当前最新的版本。 LTS 版本 :Long Term Support,即长期维护版本。 说明参照:Release
Node.js 版本可以分为三个阶段:Current
、Active LTS
和Maintenance
当一个奇数版本发布后,最近的一个偶数版本会立即进入 LTS 维护阶段,一直持续 18 个月。LTS 维护阶段结束以后,进入 12 个月的 Maintenance
阶段。
注意:奇数版本
不包含 Active LTS
和 Maintenance
这两个阶段。
三个阶段主要做的事情
Current
版本:包含了主分支上非重大的更新。Active LTS
版本:经过 LTS 团队审核的新功能、错误修复和更新,已被确定为适用于发布线且稳定。Maintenance
版本:关键错误修复和安全更新。
建议:由于偶数版本获得的支持时间比较长,推荐在生产环境中使用偶数版本 的 Node.js
。
安装 Node.js
确定安装的软件版本后,只需要下载操作系统对应的软件即可。 本教程安装的是 MacOS 系统 V20.8.0 版本。
Node.js 版本管理器 - nvm
通过 nvm 工具可以快速安装和使用不同版本的 node。 下载
bash
// 下载命令
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
// 或者
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
nvm 对 windows 系统支持的不太好,只支持在部分情况下的 windows 中使用。因此,在 windows 中推荐使用 nvm-windows。
命令
// 选择版本
nvm use 16
// 查看列表
nvm ls
// 安装版本
nvm install 16
// 卸载版本
nvm uninstall 16
// 查看全部命令
nvm -h
// 检查版本是否正确
node -v
// 设置默认版本
nvm alias default 18.16.0
Node.js 版本管理器 - n
非常轻量级的 Node.js 版本管理器。和 nvm 类似,可以通过其提供的命令快速安装和使用不同版本的 node。 遗憾的是,n 也对 windows 操作系统支持的不太好,只可以在部分情况下的 windows 中使用。 下载
bash
npm install -g n
命令
// 安装版本
n 20.8.0
// 查看列表
n ls
// 卸载版本
n rm 20.8.0
// 选择版本
n
// 查看全部命令
n -h
// 检查版本是否正确
node -v
CommonJS 模块化及源码解析
四大模块体系
在IIFE
之后,业界迸发出几类 JavaScript 的模块体系。其中最流行的四大体系分别为:
- CommonJS,2009 年出现,模块规范发布
- AMD
- CMD
- UMD
AMD、CMD、UMD
这里三大模块体系中,只有首字母不一样,而后两个字母则都是 Module Definition 的缩写。
AMD
是Asynchronous Module Definition
,即异步模块定义CMD
是Common Module Definition
,即一般模块定义,虽然 Common 也含通用意思,但这里将其译为"一般"是为了不与后面 UMD 冲突UMD
则是Universal Module Definition
,即通用模块定义。
AMD
最开始在 require.js 中被使用,其首个提交是在 2009 年发出的。CMD
与 AMD
很类似,不同点在于:AMD 推崇依赖前置、提前执行 ,CMD 推崇依赖就近、延迟执行 。CMD 是在推行 (Sea.js)[seajs.github.io/seajs/docs/] 中产生的,而 Sea.js 则是玉伯大佬多年前的作品。 UMD
是个"大一统",在当时的野心是对 CommonJS、AMD 和 CMD 做兼容。
由于这三种模块方式与 Node.js 几乎没有关系,就不继续展开了。
CommonJS
CommonJS 模块规范发布于 2009 年,由 Mozilla 工程师 Kevin Dangoor 起草,他于当年 1 月发表了一篇文章《What Server-side JavaScript Needs》。注意这个时间,2009 年。嘿!这不巧了吗! 其实 AMD 这类也基本上是在 2009、2010 时间点出现的。以及其依赖的 V8 也都是相仿阶段出生的。我们说 2010 年前后几年是泛前端体系开始觉醒的两年是丝毫不怵的。 CommonJS 最初的主要目的是为除浏览器环境之外的 JavaScript 环境建立模块生态系统公约。继续注意这个词,"除浏览器之外的 JavaScript 环境"。答案是不是呼之欲出?其实 CommonJS 最初不叫 CommonJS,而是叫 ServerJS。后来觉得路走窄了没朋友,就把 Server 改成了 Common------把浏览器又给包括回来了。
按其说法,在 CommonJS 规范之下,你可以写:
- 服务端 JavaScript 应用;
- 命令行工具;
- 桌面 GUI 应用;
- 混合应用(Titanium,Adobe AIR......)。
CommonJS 缩写就是 CJS。所以,这个就是 Node.js 一直以来的模块规范。
CommonJS 规范
Node.js 应用由模块组成,默认采用的是 CommonJS 规范。即每个文件是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类等都是私有的,对其它文件不可见。 不过我们可以通过在模块中通过 exports
或者 module.exports 命令将内容导出,其它文件就可以通过 require 命令访问到这些被导出的内容。
Module
module
对象是模块系统的核心, 每个文件都是一个模块,有自己的作用域,文件中定义的变量、类、函数都是私有的,都有一个独立的module
对象,用于管理模块的导出、依赖和元信息 module
是对当前模块对象的引用
module.exports
用于定义模块导出的内容- 内含该模块元信息,比如一个
id
字段 - 实际上,Node.js 中的
module
下还含了初始的exports
对象
yaml
console.log(module)
{
id: '.',
path: '/Users//FeProjects/about-node/learn-node/module',
exports: {},
filename: '/Users/xxx/FeProjects/about-node/learn-node/module/node.js',
loaded: false,
children: [],
paths: [
'/Users/xxx/FeProjects/about-node/learn-node/module/node_modules',
'/Users/xxx/FeProjects/about-node/learn-node/node_modules',
'/Users/xxx/FeProjects/about-node/node_modules',
'/Users/xxxx/FeProjects/node_modules',
'/Users/xxx/node_modules',
'/Users/node_modules',
'/node_modules'
],
[Symbol(kIsMainSymbol)]: true,
[Symbol(kIsCachedByESMLoader)]: false,
[Symbol(kIsExecuting)]: true
}
各模块中大家所使用的 module
对象就是该模块对应的 Module
类实例。它除了包含 exports
对象之外,还包含:
-
children
:该模块通过 require() 加载的子模块数组。 -
filename
:模块文件的绝对路径 -
id
:模块的唯一标识符,通常是文件的完整路径 -
loaded
:表示模块是否已加载完成 -
path
: 模块所在路径 -
paths
:模块的搜索路径数组(由当前文件路径逐级向上查找 node_modules)。 -
exports
: 模块对外暴露的内容,其他模块通过 require() 访问此对象。exports
是module.exports
对应的引用。这是一个用于导出模块内容的通道jsexports = module.exports = {}; exports.name = "exports"; console.info(module.exports); // name 'exports'
-
require()
: 加载模块,访问模块导出的内容。运行时加载,是同步的,返回值是加载模块的module.exports
值,加载失败返回null
。require
是一个函数,这个函数有一个参数代表模块标识,它的返回值就是其所引用的外部模块所暴露的 API。
js
// math.js
exports.add = function () {
var sum = 0,
i = 0,
args = arguments,
l = arguments.length;
while (i < 1) {
sum += args[i++];
}
return sum;
};
// increment.js
var add = require("math").add;
exports.increment = function (val) {
return add(val, 1);
};
// program.js
var inc = require("increment").increment;
var a = 1;
inc(a); // 2
module.id = "program";

Node.js 的 module
对象下还挂载了个 module.exports
对象,其初始值指向 CommonJS 所定义的 exports
对象。而真正导出是 module.exports
,并不是 exports
.
启用 ESM 的两种方式
-
通过文件扩展名
.mjs
:将文件扩展名改为.mjs
,Node.js 会自动识别为ESM模块
js// app.mjs export const hello = () => "Hello ESM";
-
在
package.json
中设置"type":"module"
js{ "name": "ESM", "version": "1.0.0", "type": "module", "scripts": { "serve": "xxx serve", "build": "xxx build" }, }
CommonJS 源码解析
- 给 exports 直接赋值,不能导出
js
// 模块导出 - user.js
const name = "face";
const age = 18;
const printName = () => {
console.info(name);
};
exports = { name, age };
// 模块导入 - index.js
const { name, age, printName } = require("./user.js");
console.info(name, age, printName); // undefined undefined undefined
// 入口文件
node ./index.js
- 同时使用 exports 和 module.exports
js
// 模块导出 - user.js
const name = 'lucy';
const age = 28;
const printName = () => {
console.log(name);
};
module.exports = { name, age };
exports.printName = printName; // 给 exports 的属性赋值,可以导出
// 模块导入 - index.js
const { name, age, printName } = require('./user.js');
console.log(name, age, printName); // lucy 28 undefined
// 入口文件
node ./index.js
- 访问包含随机语句的模块
js
// 模块导出 - user.js
const height = Math.random();
module.exports = { height };
// 模块导入 - index.js
const { height: firstHeight } = require("./user");
console.log("🚀 ~ firstHeight:", firstHeight);
/* 一次使用,内容会被缓存,模块缓存机制,不会重新加载后续取缓存内容,重新 require 时会重新加载 */
const { height: secondHeight } = require("./user");
console.log("🚀 ~ secondHeight:", secondHeight);
// 🚀 ~ firstHeight: 0.227755537794758
// 🚀 ~ secondHeight: 0.227755537794758
- 循环引用
js
// moduleA.js
const name = "moduleA";
const path = "./moduleA.js";
const moduleB = require("./moduleB");
console.log("🚀 ~ moduleB.name:", moduleB.name);
module.exports = { name, path };
// moduleB.js
const name = "moduleB";
const path = "./moduleB.js";
const moduleA = require("./moduleA");
console.log("🚀 ~ moduleA.name:", moduleA.name);
module.exports = { name, path };
// 入口文件 node .\moduleA.js
// 🚀 ~ moduleA.name: undefined
// 🚀 ~ moduleB.name: moduleB
// (node:26784) Warning: Accessing non-existent property 'name' of module exports inside circular dependency
// (Use `node --trace-warnings ...` to show where the warning was created)
- CommonJS 模块输出的是一个值的拷贝, 浅拷贝
js
// user.js
let num = 1;
let user = { name: "face" };
exports.num = num; // 赋值, 是对变量 num 进行了拷贝, 拷贝的是 1 这个值
exports.user = user; // 赋值, 是对变量 user 进行了拷贝, 拷贝的是对象的引用地址
exports.addNum = () => {
num += 1; // 修改变量 num 的值
};
exports.getNum = () => {
console.log("🚀 ~ 模块内部修改 num:", num);
};
exports.setName = () => {
user.name = "exports"; // 修改对象 user 的属性 name
};
exports.getName = () => {
console.log("🚀 ~ 模块内部修改 user.name:", user.name);
};
// index.js
const a = require("./user");
console.log("🚀 ~ a 模块:", a);
console.log("🚀 ~ 初始 a.num :", a.num);
console.log("🚀 ~ 初始 a.user.name:", a.user.name);
a.addNum();
a.setName();
console.log("🚀 ~ 修改后 a.num:", a.num);
console.log("🚀 ~ 修改后 a.user.name:", a.user.name);
a.getNum();
a.getName();
/* 运行 node ./index.js */
// 🚀 ~ a 模块: {
// num: 1,
// user: { name: 'face' },
// addNum: [Function (anonymous)],
// getNum: [Function (anonymous)],
// setName: [Function (anonymous)],
// getName: [Function (anonymous)]
// }
// 🚀 ~ 初始 a.num : 1
// 🚀 ~ 初始 a.user.name: face
// 🚀 ~ 修改后 a.num: 1
// 🚀 ~ 修改后 a.user.name: exports
// 🚀 ~ 模块内部修改 num: 2
// 🚀 ~ 模块内部修改 user.name: exports

为什么直接赋值 exports
会失效
错误用法 ❌
js
// 错误!无法导出内容
exports = {
name: "foo",
method: function () {
/* ... */
},
};
- 上面代码中
exports
被重新赋值,但module.exports
仍然指向原始的空对象,导致导出失败。 exports
的本质:exports
是模块系统在运行时提供的一个引用变量,它默认指向module.exports
的内存地址。- 直接赋值会切断引用 :如果你直接对
exports
赋值(例如exports = ...
),相当于让exports
指向了一个新的对象,而原本的module.exports
不会受到影响。此时模块实际导出的仍然是旧的module.exports
(默认为空对象),就会导致 require 引入的始终是空对象
正确用法:
-
通过
module.exports
导出js// 模块导出 - user.js const name = "lucy"; const age = 28; const printName = () => { console.log(name); };
-
修改
exports
的属性,如果希望保持使用 exports,可以通过添加属性的方式修改它(而不是直接赋值):jsexports.name = "face"; exports.method0 = () => { console.log("method0"); };
此时
exports
仍然指向原始的module.exports
,因此修改其属性是有效的。 -
底层原理
-
模块的导出始终以
module.exports
为准 -
初始化时,Node.js 会执行一下操作
jsvar module = { exports: {}, }; var exports = module.exports; // 初始状态时两者指向同一个对象
- 因此,直接修改
exports = ...
会影响module.exports
- 因此,直接修改
-
- 直接赋值给
exports
:❌ 无效(会切断与 module.exports 的关联)。 - 操作
module.exports
:✅ 正确且可靠。 - 修改
exports
的属性:✅ 正确(前提是保持引用)。