Node系列 · Node基础:全局变量与全局对象
Node 运行时在 JS 标准全局对象之外,又注入了一批只在 Node 环境存在 的全局变量。它们不在 ECMAScript 规范里,浏览器看不到;记不清就很容易写出让本地能跑、生产报
ReferenceError的代码。
一、Node 的全局对象分两层
| 层次 | 归属 | 在浏览器 | 在 Node |
|---|---|---|---|
| JS 标准全局 | ECMAScript 规范 | globalThis、Promise、Map、setTimeout 等 |
同样存在 |
| Node 专属全局 | Node 运行时注入 | 不存在 | global、process、Buffer、__dirname、__filename、console 等 |
Node 中 globalThis === global(不同于浏览器里 globalThis === window):
javascript:global-check.js
console.log(globalThis === global); // true
::: tip
判断代码运行环境的最小成本方法:typeof process !== 'undefined'。
:::
二、定时器三件套:setTimeout / setInterval / setImmediate
2.1 三者的归属阶段
| API | 触发阶段 | 最小延迟 |
|---|---|---|
setTimeout(fn, 0) |
timers | 1ms(HTML 规范,Node 同样遵守) |
setInterval(fn, ms) |
timers | 同上 |
setImmediate(fn) |
check | 0(事件循环下一轮立即) |
三者触发前都会先清空微任务队列,所以总是比 Promise.then 晚执行。
javascript:setTimer.js
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
bash
$ node setTimer.js
sync
promise
timeout
immediate
执行顺序解释:
sync------ 同步代码立即执行promise------ 微任务队列先于宏任务timeout------ timers 阶段触发immediate------ check 阶段触发,在 timers 之后
::: warning
不要把 setTimeout(fn, 0) 当成"立即执行"。它会被推迟到事件循环的 timers 阶段,且最小间隔是 1ms。要"尽快执行"且不阻塞当前同步流程,用 queueMicrotask 或 Promise.resolve().then()。
:::
2.2 setTimeout / setInterval / setImmediate 的返回值与清除
三个定时器都有配套清除函数,返回值在 Node 里都是对象(Timeout / Timeout / Immediate),浏览器里是数字。返回值类型属于 Node 扩展行为,跨平台代码不要依赖,只用对应的 clearXxx 清除:
javascript:clear-timer.js
const t1 = setTimeout(fn, 1000);
const t2 = setInterval(fn, 1000);
const t3 = setImmediate(fn);
clearTimeout(t1);
clearInterval(t2);
clearImmediate(t3);
清除时机:
- 未触发的回调 :调用
clearXxx后回调不再执行------这是常规用法 - 已触发的回调:清除无效,回调已经跑完或正在跑
- 重复触发(仅
setInterval):清除后停止排队下一次;已经在 poll 队列里的本次执行无法阻止
setInterval 的失真
setInterval(fn, 1000) 不保证每 1 秒精确执行。如果回调本身耗时 800ms,下一次触发会在回调结束后立即排队,而不是和上一次间隔 1 秒:
javascript:setInterval-drift.js
let n = 0;
const t = setInterval(() => {
n++;
console.log(`第 ${n} 次`, new Date().toISOString());
if (n === 3) {
clearInterval(t);
}
}, 1000);
若想"上次结束 + N 毫秒"再触发,用递归 setTimeout:
javascript:setInterval-self-correct.js
function loop() {
console.log(new Date().toISOString());
setTimeout(loop, 1000);
}
loop();
三、console:不只是 log
Node 的 console 模块对外暴露 5 个基础方法 + 2 个格式化方法。按写入目标分组:
| 目标 | 方法 | 典型用途 |
|---|---|---|
| stdout | console.log / info / debug |
普通日志、信息、调试 |
| stderr | console.warn / error |
警告、错误(不会被正常日志吞掉) |
| stdout | console.dir(obj, { depth }) |
深打印对象 |
| stdout | console.table([...]) |
打印数组/对象为表格 |
javascript:console-methods.js
console.dir({ a: { b: { c: 1 } } }, { depth: 4 });
// { a: { b: { c: 1 } } }
console.table([{ name: 'A', age: 1 }, { name: 'B', age: 2 }]);
// ┌─────────┬─────┐
// │ (index) │ name │ age │
// ├─────────┼─────┼─────┤
// │ 0 │ 'A' │ 1 │
// │ 1 │ 'B' │ 2 │
// └─────────┴─────┴─────┘
::: warning
console.log 在高并发下会阻塞事件循环 ------stdout 默认是 TTY 或管道,写入是同步调用。生产环境请用日志库(pino / winston / log4js)替代,必要时切换到异步写入。
:::
四、__dirname 与 __filename
| 变量 | 类型 | 值 |
|---|---|---|
__dirname |
string | 当前执行文件所在目录的绝对路径 |
__filename |
string | 当前执行文件的绝对路径(含文件名) |
javascript:paths.js
console.log(__dirname);
// /Users/you/project/src
console.log(__filename);
// /Users/you/project/src/paths.js
使用场景:拼接配置文件、模板文件等相对路径,避免依赖 process.cwd()。
javascript:read-config.js
const fs = require('fs');
const path = require('path');
const configPath = path.join(__dirname, 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
::: warning
__dirname 与 __filename 仅 CommonJS 模块可用 。Node 加载 JS 文件时,默认按 CommonJS(简称 CJS)规范处理,五个核心参数(exports / require / module / __filename / __dirname)由 Node 注入。如果项目改成 ESM(.mjs 后缀或在 package.json 设 "type": "module"),这两个变量就不再存在,要这样取:
javascript:esm-dirname.mjs
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
CommonJS 与 ESM 的完整对比会在模块化章节展开。
:::
五、Buffer:Node 处理二进制数据的核心
Buffer 是 Node 在 V8 堆之外分配的定长二进制缓冲区 。一句话:Buffer 实例 ≈ Uint8Array 的子类 + Node 专属 API。
5.1 创建方式
javascript:buffer-create.js
// 1. 指定字节数(未初始化,可能包含旧内存数据)
const buf1 = Buffer.alloc(4); // 00000000 00000000 00000000 00000000
// 2. 指定字节数 + 填充值
const buf2 = Buffer.alloc(4, 0x41); // 'AAAA'
// 3. 从字符串创建(默认 UTF-8)
const buf3 = Buffer.from('hello'); // <Buffer 68 65 6c 6c 6f>
// 4. 从数组创建
const buf4 = Buffer.from([0x68, 0x65]); // <Buffer 68 65>
::: warning
永远不要用 new Buffer(...) 。Buffer 构造函数已被弃用,且未初始化内存会泄漏数据。统一用 Buffer.alloc / Buffer.from。
:::
5.2 字节范围与字符编码
- 每个字节取值
0-255(即0x00 - 0xFF) - 常用编码:
utf-8(默认)、hex、base64、latin1、ascii
javascript:buffer-encode.js
const buf = Buffer.from('你好');
console.log(buf);
// <Buffer e4 bd a0 e5 a5 bd>
console.log(buf.toString('hex'));
// e4bda0e5a5bd
console.log(buf.toString('base64'));
// 5L2g5aW9
5.3 与 Uint8Array 的关系
javascript:buffer-uint8array.js
const u8 = new Uint8Array([1, 2, 3]);
const buf = Buffer.from(u8);
console.log(buf instanceof Uint8Array); // true
console.log(Buffer.prototype.__proto__ === Uint8Array.prototype); // true
Buffer 实例可直接传给所有接受 Uint8Array 的 API(如 fs.read、fetch 的 body、WebSocket.send)。
5.4 字符串长度 ≠ Buffer 长度
UTF-8 是变长编码:英文 1 字节、中文 3 字节、emoji 4 字节。Buffer.length 是字节数 ,String.length 是字符数(按 UTF-16 code unit 计),两者不能互换:
javascript:buffer-slice.js
const buf = Buffer.from('你好世界');
console.log(buf.length); // 12 字节(4 × 3)
console.log(buf.toString().length); // 4 字符
// 错误:从中间字节切开会乱码
console.log(buf.slice(0, 5).toString()); // '你?'(后半截被截断)
Node 提供 string_decoder 模块解决这个问题。它知道字符边界,遇到不完整的字节序列会缓存起来,等下次写入再拼接:
javascript:string-decoder.js
const { StringDecoder } = require('string_decoder');
const decoder = new StringDecoder('utf8');
// 第一次写入:拿到一个完整字符,缓存为空
console.log(decoder.write(buf.slice(0, 3))); // '你'
// 第二次写入:跨字节切片(2 + 3 = 5 字节),decoder 自动补完
console.log(decoder.write(buf.slice(3, 8))); // '好世'
// 拿回缓存里残留的不完整字符
console.log(decoder.end()); // '界'
这个模块在处理流式 IO(如 net / fs 的 data 事件)时几乎是必备------单次 data 回调拿到的字节切片不会保证按字符边界 对齐,自己直接 toString() 必然乱码。
六、process:进程级控制中心
process 是 Node 运行时暴露的当前进程对象,属性与方法按"退出、参数、环境、信号、子进程、IO"分组。
6.1 进程退出
javascript:process-exit.js
process.exit(); // 同步退出,code = 0
process.exit(1); // 失败退出,code = 1
::: warning
process.exit() 会立即终止进程 ,所有未完成的异步 IO、未刷新的写流都会被丢弃。HTTP 服务里应通过 server.close() 让现有请求跑完再退出:
javascript:graceful-shutdown.js
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
});
:::
6.2 当前工作目录
process.cwd() 是运行时的工作目录 (启动命令所在的目录),而 __dirname 是当前模块文件所在目录------两者不同:
bash
$ cd /home/you && node /opt/app/src/index.js
process.cwd()→/home/you(启动时所在目录)__dirname→/opt/app/src(当前模块文件所在目录)
6.3 命令行参数
javascript:argv.js
console.log(process.argv);
bash
$ node argv.js --port 3000 config.json
[
'/usr/local/bin/node', // argv[0] node 可执行路径
'/opt/app/src/argv.js', // argv[1] 当前文件
'--port', // 用户参数从 argv[2] 开始
'3000',
'config.json'
]
实际项目里通常用 minimist 或 commander 解析,而不是手写 split。
6.4 平台与架构
javascript:platform.js
console.log(process.platform); // 'darwin' | 'linux' | 'win32' | ...
console.log(process.arch); // 'arm64' | 'x64' | ...
注意:process.platform 是 Node 二进制编译时的目标平台,不一定等于 OS 内核 。在 macOS 上跑 Rosetta 兼容层时 process.platform 仍是 'darwin'。
6.5 进程间通信:发信号
javascript:kill.js
process.kill(pid, 'SIGTERM');
process.kill 名字有歧义------它只是发送信号,不保证对方真的死。pid 不存在或没权限时抛异常。
Node 进程里常见的信号:
| 信号 | 来源 | 默认行为 | 进程该如何响应 |
|---|---|---|---|
SIGTERM |
kill <pid>、K8s/PM2 优雅停机 |
终止进程 | 监听后优雅退出 ------先 server.close() 再 process.exit(0) |
SIGINT |
Ctrl + C |
终止进程 | 通常等同 SIGTERM,开发环境常用 |
SIGHUP |
终端断开 / kill -HUP |
终止进程 | 历史上用于"重新加载配置";现在多用作自定义信号 |
SIGKILL |
kill -9 |
立即终止,不可拦截 | 监听不到------OS 直接杀进程 |
SIGUSR1 / SIGUSR2 |
用户自定义 | 终止进程 | Node 默认不做任何事,可作自定义触发 |
K8s pod 终止流程:
- K8s 发
SIGTERM - pod 进入
terminationGracePeriodSeconds(默认 30s)倒计时 - 超时后 K8s 发
SIGKILL强杀
所以 Node 进程必须监听 SIGTERM 并优雅退出,否则正在处理的请求会被中途切断。
6.6 环境变量
javascript:env.js
console.log(process.env.NODE_ENV); // 'development' | 'production' | undefined
读取自定义变量时,|| 提供本地开发默认值是常见做法:
javascript:env-default.js
const dbUrl = process.env.DATABASE_URL || 'mongodb://localhost:27017/dev';
::: tip
不要把 .env 文件直接 require 进生产 。使用 dotenv 或 node --env-file=.env(Node 20.6+ 已内置)按环境注入。
:::
6.7 标准 IO
| 流 | 用途 |
|---|---|
process.stdin |
标准输入(可读) |
process.stdout |
标准输出(可写) |
process.stderr |
标准错误(可写) |
javascript:stdin-echo.js
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
process.stdout.write(`echo: ${chunk}`);
});
七、对比表:本文涉及的全局对象一览
| 名称 | 类别 | Node 专属 | 典型用途 |
|---|---|---|---|
global / globalThis |
全局对象 | 否(标准) | 跨环境统一访问全局 |
setTimeout / clearTimeout |
定时器 | 否(标准) | 延迟执行 |
setInterval / clearInterval |
定时器 | 否(标准) | 周期性执行 |
setImmediate / clearImmediate |
定时器 | 是 | 事件循环下一轮立即 |
console |
日志 | 否(Web 也有) | 调试输出 |
__dirname / __filename |
路径 | 是 | 当前模块路径(仅 CJS) |
Buffer |
二进制 | 是 | 处理字节流 |
process |
进程 | 是 | 环境、退出、信号、参数 |
表中仅列前文展开过的全局对象。Node 还提供
queueMicrotask、URL、TextEncoder/TextDecoder、fetch(Node 18+)等,本文不展开。
八、小结
- Node 全局对象分 ECMAScript 标准层 + Node 专属层,后者浏览器看不到
- 定时器三件套各属不同阶段,理解阶段顺序就理解执行顺序
__dirname/__filename只在 CJS 存在;ESM 用import.meta.url+fileURLToPathBuffer是 Node 的二进制基石,等价于Uint8Array子类process几乎覆盖了进程的所有可观察状态与可控行为