Node.js 与 JavaScript 语言特性详解

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 实例
上一篇
下一篇