Node.js 的「关键字层」= JavaScript 语言机制。每条给语义、多场景示例、边界与坑。只覆盖 Node 工程相关(不含浏览器 API)。
1. let / const / var – 声明与作用域
描述
const 声明不可重赋值的绑定(对象内容仍可改);let 是块级作用域;var 是函数级且会提升(内部实现有坑)。新代码只用 const/let。
语法和示例
const list = [1, 2];
list.push(3); // 合法:改内容
// list = []; // TypeError:改绑定
let n = 1;
if (true) { let n = 2; } // 块内独立
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0 1 2(每轮独立 i)
}
for (var j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0); // 3 3 3(var 函数级共享)
}注意事项
- var 的循环闭包陷阱是经典面试题——用 let 自动解决
- const 只锁绑定不锁内容:const obj = {} 仍可 obj.a = 1
- TDZ(暂时性死区):let/const 在声明前的区域访问会抛 ReferenceError(var 是 undefined)
- 块级作用域包括 for/if/try 的 {}
- 顶层 this:ESM 是 undefined,CJS 是 module.exports;用 globalThis 统一
2. 箭头函数与 this 绑定
描述
箭头函数没有自己的 this,继承定义处的外层 this;没有 arguments,不能作构造器,不能 yield(不能当生成器)。
语法和示例
const obj = {
name: 'A',
regular() { return this.name; }, // this = obj(调用者)
arrow: () => this?.name, // this = 外层(模块里是 undefined)
delayed() {
setTimeout(() => this.name, 100); // 箭头继承 delayed 的 this ✓
setTimeout(function () { this.name; }, 100); // 普通函数 this=全局 ✗
},
};
const add = (a, b) => a + b;
const obj2 = () => ({ x: 1 }); // 返回对象要加括号注意事项
- 对象方法不要用箭头函数——this 不会指向对象
- 箭头函数不能 new,也没有 prototype
- 回调里需要 arguments 或需要动态 this 时用普通函数
- 类字段用箭头函数可绑定 this(比 bind 简洁):class A { run = () => this.x }
- 箭头函数无法用 apply/call 改变 this
3. 解构与展开
描述
解构从数组/对象取值到变量(可重命名、默认值、剩余);展开运算符 … 复制浅层或拼接。
语法和示例
const { a, b: renamed, c = 1, ...rest } = obj;
const [first, , third] = arr;
const [x, ...others] = arr;
function f({ name, port = 3000 }) {}
// 展开
const merged = { ...a, ...b }; // 浅合并(后者覆盖)
const copy = [...list];
const newObj = { ...obj, k: 2 }; // 改单个键
Math.max(...nums);
// 交换
let p = 1, q = 2;
[p, q] = [q, p];注意事项
- 展开是浅拷贝——嵌套对象仍共享引用(深拷贝用 structuredClone)
- 默认值只对 undefined 生效(null 不触发默认值)
- 对象展开的键顺序会影响结果(后者覆盖前者)
- 数组解构的剩余必须是最后一个元素
- 函数参数解构时传 undefined 会触发默认值;传 null 会报错
4. 可选链 ?. 与空值合并 ??
描述
?. 深取时遇到 null/undefined 直接返回 undefined(不崩);?? 只在左侧为 null/undefined 时取右(0/”/false 不触发)。
语法和示例
user?.profile?.name ?? '匿名';
user?.comments?.[0]?.text;
fn?.(); // 函数存在才调
obj?.method?.();
// ?? 与 || 的区别
const port = env.PORT ?? 3000; // PORT=0 时保留 0
const port2 = env.PORT || 3000; // PORT=0 时错误地用 3000
// 混合使用时必须加括号
(a ?? b) || c;注意事项
- ?. 不能用于赋值左侧(a?.b = 1 是语法错误)
- ?? 与 || 混用要加括号,否则语法错误
- ?. 只对 null/undefined 短路,对 0/”/false 不短路
- 深层可选链返回 undefined,不是 null
- 可选调用 ?.() 与可选访问 ?.[] 是两个不同运算符
5. async / await 与错误处理
描述
async 函数总返回 Promise;await 只能在其内部(或 ESM 顶层);try/catch 可捕获 await 的拒绝。
语法和示例
async function load() {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (e) {
if (e.name === 'AbortError') return null; // 分类处理
throw e; // 其他向上传播
}
}
const v = await load(); // ESM 顶层 await
// 并行(不是串行)
const [a, b] = await Promise.all([f1(), f2()]);
// 不要这样(串行)
// const a = await f1(); const b = await f2();注意事项
- 忘记 await 会拿到 Promise 而不是值(静默 bug,类型检查能抓)
- await 在循环里逐个等待 = 串行,性能差——用 Promise.all
- async 函数抛出的异常返回 rejected Promise,不会同步抛出
- try/catch 只能捕获 await 表达式的拒绝,捕获不到未 await 的 Promise
- 顶层 await 仅 ESM 可用;CJS 需包在 async IIFE 里
6. Promise 组合四式
描述
all(全成才成)、allSettled(各自结果)、race(先完成者)、any(先成功者)。
语法和示例
const [a, b] = await Promise.all([f1(), f2()]);
const settled = await Promise.allSettled([risky(), safe()]);
settled.filter(s => s.status === 'fulfilled');
// race:超时控制
await Promise.race([task(), sleep(5000).then(() => { throw new Error('timeout'); })]);
// any:多镜像取最快成功的
await Promise.any([mirror1(), mirror2()]);
// 全部完成(不关心结果)
await Promise.allSettled(jobs);注意事项
- Promise.all 遇第一个 reject 立即 reject,其余仍在跑(无法取消)
- race 的「先完成」包含 reject,常用于超时
- any 全部失败时抛 AggregateError(不是第一个错误)
- allSettled 永不 reject,结果需逐个检查 status
- 没有 Promise 取消机制——用 AbortController 传递信号
7. class 与私有字段
描述
class 是原型继承的语法糖。支持 static、getter/setter、私有 # 字段(真正不可外部访问)、私有方法。
语法和示例
class Api {
#key; // 真私有字段
static VERSION = '1';
static #instances = 0; // 私有静态
constructor(key) {
this.#key = key;
Api.#instances++;
}
get auth() { return `Bearer ${this.#key}`; }
set auth(v) { this.#key = v.replace('Bearer ', ''); }
async call(path) {
return fetch(path, { headers: { authorization: this.auth } });
}
#helper() {} // 私有方法
}
class V2 extends Api { constructor(k) { super(k); } }
// 静态块(ES2022)
class Cfg { static x; static { Cfg.x = load(); } }注意事项
- # 私有是真私有(反射/外部都拿不到),而 _下划线 只是约定
- 私有字段必须先声明(不能用 this.#x 隐式创建)
- 字段声明顺序影响初始化:父类字段先于子类
- class 声明不提升(TDZ),与函数声明不同
- 构造函数返回对象会覆盖 this(返回原始值则忽略)
8. 模块:ESM 与 CJS
描述
ESM:静态导入导出、顶层 await、导入必须写扩展名;CJS:require 同步、运行时求值。Node 通过 type 或扩展名区分。
语法和示例
// ESM(type: module 或 .mjs)
import fs from 'node:fs/promises';
import { readFile } from 'node:fs/promises';
export const A = 1;
export default class {}
const mod = await import('./heavy.js'); // 动态导入(按需)
// ESM 没有 __dirname
import { fileURLToPath } from 'node:url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
// CJS(.cjs 或默认)
const fs = require('node:fs');
module.exports = { A: 1 };注意事项
- ESM 导入路径必须含扩展名(’./a.js’ 而非 ‘./a’)
- node: 前缀强制内置模块,避免被同名包劫持
- CJS 的 require 可出现在任意位置(同步阻塞);ESM 的 import 必须顶层
- 混用时:ESM 可 import CJS(拿到 default),CJS 不能 require ESM(需动态 import)
- 模块只求值一次并缓存——跨模块共享状态靠这点
9. 迭代器与生成器
描述
实现 Symbol.iterator 即可被 for-of / 展开消费;function* 生成器用 yield 惰性产出,可双向传值。
语法和示例
function* range(n) {
for (let i = 0; i < n; i++) yield i;
}
[...range(3)]; // [0,1,2]
for (const x of range(5)) {}
// 生成器接收值
function* echo() {
const v = yield 'first'; // 暂停并返回值
yield `got ${v}`;
}
const it = echo(); it.next(); it.next('hi');
// 自定义可迭代
const obj = { *[Symbol.iterator]() { yield 1; yield 2; } };
// 异步生成器
async function* pages() { yield await fetchPage(0); }
for await (const p of pages()) {}注意事项
- 生成器函数调用后不执行,首次 next() 才运行到第一个 yield
- for-of 只接受同步迭代器;异步用 for await-of
- 展开/解构会消耗迭代器(一次性)
- 生成器的 return() 会触发 finally 块
- 无限生成器必须配 break/limit 否则死循环
10. Symbol / Proxy / Reflect
描述
Symbol 创建唯一键(防冲突);Proxy 拦截对象操作(校验/日志/响应式);Reflect 是 Proxy 陷阱的默认实现集合。
语法和示例
const KEY = Symbol('k');
const obj = { [KEY]: 'private-ish' };
// Proxy 校验
const handler = {
get(t, k) { return Reflect.get(t, k); },
set(t, k, v) {
if (k === 'age' && v < 0) throw new RangeError('age');
return Reflect.set(t, k, v);
},
has(t, k) { return Reflect.has(t, k); },
};
const safe = new Proxy({ age: 0 }, handler);
safe.age = 20;
// 常见用途:Vue3 响应式、不可变包装、日志埋点注意事项
- Symbol 键不会出现在 Object.keys / JSON.stringify 里
- Proxy 的性能开销明显,热点路径慎用
- Proxy 无法拦截「已存在的非配置属性」的部分操作
- Reflect 方法返回布尔值(操作是否成功),比直接操作更可控
- Symbol.for(‘x’) 是全局注册表,与 Symbol(‘x’) 不同(可跨模块共享)
11. 错误类型与异常链
描述
标准错误:Error/TypeError/RangeError/SyntaxError/ReferenceError。自定义错误继承 Error 并用 cause 保留根因(ES2022)。
语法和示例
class HttpError extends Error {
constructor(status, msg, options) {
super(`HTTP ${status}: ${msg}`, options); // options.cause
this.name = 'HttpError';
this.status = status;
}
}
try { await api(); }
catch (e) {
throw new HttpError(500, '内部错误', { cause: e });
}
// 判断类型
if (e instanceof TypeError) {}
if (e?.name === 'AbortError') {}
// 解包原因链
while (e) { console.error(e.message); e = e.cause; }注意事项
- 继承 Error 后必须设 name,否则栈显示 ‘Error’
- 无需 catch 里 throw e 重新抛(会丢栈)——直接 throw 或包装 cause
- 异步回调里的抛错可能变成 unhandledRejection(挂 process 事件兜底)
- e.stack 是字符串,结构化日志要另存 name/message
- 不要 throw 字符串/对象(拿不到栈)——永远 throw Error 实例