Node.js全栈基石(壹)

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 的运行时环境

  1. 什么是 JS 运行时环境
  2. 为什么 JS 需要特别的运行时环境
  3. JS 引擎是什么
  4. 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 是有两部分组成:
    1. WebCore:负责 HTML,CSS 解析,布局,渲染等操作;
    2. 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;
}
  1. Parse 模块 会将 JavaScript 代码转换成 AST(抽象语法树),这是因为解释器并不直接认识 JavaScript 代码;
    • 如果函数没有被调用,那么是不会被转换成 AST 的;
    • Parse 的 V8 官方文档:Parser
  2. lgnition 模块 Ignition 是一个解释器,会将 AST 转换成 ByteCode(字节码);
    • 同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
    • 如果函数只调用一次,Ignition 会执行解释执行 ByteCode;
    • Ignition 的 V8 官方文档:Ignition
  3. 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 架构区别

  1. 在 Chrome 浏览器中
    • V8 引擎只是其中一小部分,用来辅助 JavaScript 代码的运行
    • 还有一些浏览器的内核用来负责 HTML 解析、布局、渲染等相关工作
    • 中间层和操作系统( 网卡、硬盘、显卡......)
      • 比如发送网络请求,中间层会调用操作系统中的网卡
      • 读取一些本地文件,中间层会调用操作系统中的硬盘
      • 浏览器页面的渲染工作,中间层会调用操作系统中的显卡
  2. 在 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

  1. JS 代码就是我们平时在使用的那些 JS 模块。比如,fs模块,http模块,path模块等
  2. C++ 代码主要分三个部分。
    • 第一部分主要是封装Libuv和第三方库的C++代码,比如netfs这些模块都会对应一个 C++ 模块,它主要是对底层的一些封装,暴露给开发者使用
    • 第二部分是不依赖Libuv和第三方库的C++代码,比如 Buffer模块的实现
    • 第三部分 C++ 代码是 V8 本身的代码
  3. 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 开发。以下是其关键阶段的梳理:

  1. 2009 年:Node.js 的诞生
    • 创始人:Ryan Dahl,一位对服务器性能瓶颈不满的开发者。
    • 灵感来源:受 **V8 引擎(Chrome 的 JavaScript 引擎)**启发,Dahl 希望用 JavaScript 实现高性能、事件驱动的服务器端开发。
    • 首次发布:2009 年 11 月,Node.js 在 JSConf EU 大会亮相,核心思想是非阻塞 I/O事件循环,解决了传统服务器(如 Apache)的并发处理瓶颈。
  2. 早期发展与争议(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 能做什么

  1. 服务端开发:用于做服务器端开发 web 服务器
  2. 工具:可以基于 node 构建一些工具,构建工作流,比如 npm,webpack,gulp,less,sass 等 vue-cli
  3. 开发工具或者客户端应用:开发桌面应用程序(借助 node-webkit、electron 等框架实现),比如:vscode、语雀等
  4. 同构:SSR,借助 Node.js 完成服务端渲染+前后端同构
  5. serverless
  6. 流式 SSR
  7. 游戏
  8. npm、yarn,pnpm 工具成为前端开发使用最多的工具;
  9. 爬虫

Node.js 环境搭建

如何选择 Node.js 版本

下载地址:Download | Node.js LTS VS Current Current 版本:当前最新的版本。 LTS 版本 :Long Term Support,即长期维护版本。 说明参照:Release

Node.js 版本可以分为三个阶段:CurrentActive LTSMaintenance

当一个奇数版本发布后,最近的一个偶数版本会立即进入 LTS 维护阶段,一直持续 18 个月。LTS 维护阶段结束以后,进入 12 个月的 Maintenance 阶段。

注意:奇数版本不包含 Active LTSMaintenance 这两个阶段。

三个阶段主要做的事情

  • 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 的缩写。

  • AMDAsynchronous Module Definition,即异步模块定义
  • CMDCommon Module Definition,即一般模块定义,虽然 Common 也含通用意思,但这里将其译为"一般"是为了不与后面 UMD 冲突
  • UMD 则是 Universal Module Definition,即通用模块定义。

AMD 最开始在 require.js 中被使用,其首个提交是在 2009 年发出的。CMDAMD 很类似,不同点在于: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() 访问此对象。exportsmodule.exports对应的引用。这是一个用于导出模块内容的通道

    js 复制代码
    exports = module.exports = {};
    exports.name = "exports";
    console.info(module.exports); // name 'exports'
  • require(): 加载模块,访问模块导出的内容。运行时加载,是同步的,返回值是加载模块的module.exports值,加载失败返回nullrequire 是一个函数,这个函数有一个参数代表模块标识,它的返回值就是其所引用的外部模块所暴露的 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 的两种方式

  1. 通过文件扩展名.mjs:将文件扩展名改为.mjs,Node.js 会自动识别为 ESM模块

    js 复制代码
    // app.mjs
    export const hello = () => "Hello ESM";
  2. package.json中设置"type":"module"

    js 复制代码
    {
    	"name": "ESM",
    	"version": "1.0.0",
    	"type": "module",
    	"scripts": {
    		"serve": "xxx serve",
    		"build": "xxx build"
    	},
    }

CommonJS 源码解析

  1. 给 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
  1. 同时使用 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
  1. 访问包含随机语句的模块
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
  1. 循环引用
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)
  1. 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 引入的始终是空对象

正确用法

  1. 通过 module.exports 导出

    js 复制代码
    // 模块导出 - user.js
    const name = "lucy";
    const age = 28;
    const printName = () => {
    	console.log(name);
    };
  2. 修改exports的属性,如果希望保持使用 exports,可以通过添加属性的方式修改它(而不是直接赋值):

    js 复制代码
    exports.name = "face";
    exports.method0 = () => {
    	console.log("method0");
    };

    此时 exports 仍然指向原始的 module.exports,因此修改其属性是有效的。

  3. 底层原理

    • 模块的导出始终以module.exports为准

    • 初始化时,Node.js 会执行一下操作

      js 复制代码
      var module = {
      	exports: {},
      };
      var exports = module.exports; // 初始状态时两者指向同一个对象
      • 因此,直接修改exports = ... 会影响module.exports
  • 直接赋值给 exports:❌ 无效(会切断与 module.exports 的关联)。
  • 操作 module.exports:✅ 正确且可靠。
  • 修改 exports 的属性:✅ 正确(前提是保持引用)。
相关推荐
折果1 小时前
如何在vue项目中封装自己的全局message组件?一步教会你!
前端·面试
不死鸟.亚历山大.狼崽子1 小时前
Syntax Error: Error: PostCSS received undefined instead of CSS string
前端·css·postcss
汪子熙1 小时前
Vite 极速时代的构建范式
前端·javascript
跟橙姐学代码1 小时前
一文读懂 Python 的 JSON 模块:从零到高手的进阶之路
前端·python
前端小巷子1 小时前
Vue3的渲染秘密:从同步批处理到异步微任务
前端·vue.js·面试
nightunderblackcat2 小时前
新手向:用FastAPI快速构建高性能Web服务
前端·fastapi
小码编匠2 小时前
物联网数据大屏开发效率翻倍:Vue + DataV + ECharts 的标准化模板库
前端·vue.js·echarts
爱心发电丶3 小时前
NodeSSh 实现前端自动部署:服务端编译和本地编译
node.js
欧阳天风3 小时前
分段渲染加载页面
前端·fcp
艾小码3 小时前
TypeScript在前端的实践:类型系统助力大型应用开发
前端·typescript