跳转到内容

附录 B JS/TS 速查表

这份表给写过别的语言、要快速想起 JavaScript 和 TypeScript 怎么写的人:按节查,不从头读。每个条目是一行说明加一段代码,只讲怎么写,不讲为什么;需要原理的条目在说明末尾注明正文章节。代码以 Node 22 和 TypeScript 5 的 strict 模式为准:js 代码块用 node 运行,ts 代码块用 tsx 运行,每段都能直接运行,注释里写的是运行结果。文件一律按 ES 模块写,所在目录的 package.json 里要有 "type": "module"。只有 TypeScript 才有的条目在说明前标“TS”,其余条目 JS 和 TS 通用;标“第三方”的工具需要另外安装。

官方下载地址:https://nodejs.org/

查看 Node 和 npm 的版本

终端窗口
node --version
npm --version

在代码里查看版本

console.log(process.version); // v22.23.2
console.log(process.versions.v8); // V8 引擎版本

安装 fnm(第三方,管理多个 Node 版本)

终端窗口
# macOS / Linux
curl -fsSL https://fnm.vercel.app/install | bash
# Windows
winget install Schniz.fnm

让 fnm 在当前 shell 里生效,进入目录时自动切换版本(写进 shell 的启动文件)

终端窗口
eval "$(fnm env --use-on-cd --shell bash)" # bash
fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression # PowerShell

用 fnm 安装、切换、列出 Node 版本

终端窗口
fnm install 22
fnm use 22
fnm list

设默认版本

终端窗口
fnm default 22

项目里固定 Node 版本:根目录放 .node-version(也认 .nvmrc),fnm 进入目录时自动切换

终端窗口
node --version > .node-version

TS 查看项目里装的 TypeScript 版本

终端窗口
npx tsc --version

运行 JS 文件

终端窗口
node hello.js

TS 用 tsx(第三方)运行 TS 文件:只擦掉类型、不做检查,所以快

终端窗口
npx tsx hello.ts

TS Node 22.18 起也能直接运行 .ts:同样只擦类型,enum、参数属性这类要生成代码的语法不支持,详见第 2 章

终端窗口
node hello.ts

TS 类型检查要另跑 tsc,--noEmit 表示只查不输出文件

终端窗口
npx tsc --noEmit

TS 要输出 JS 文件时,tsconfig.json 里去掉 noEmit、加上 "outDir": "dist",再运行 tsc

终端窗口
npx tsc
node dist/hello.js

进入交互式环境(REPL),输入 .exit 或按两次 Ctrl+C 退出

终端窗口
node

直接执行一行代码;-p 执行并打印结果

终端窗口
node -e "console.log(2 ** 10)"
node -p "2 ** 10"

文件改动后自动重启

终端窗口
node --watch app.js

打包成单个可执行文件:Node 的单文件可执行程序(SEA)要好几步,见 https://nodejs.org/api/single-executable-applications.html;通常发布的是 JS 文件,由目标机器上的 node 运行

读取命令行参数:process.argv 的前两项是 node 和脚本路径

// 运行:node args.js a b
console.log(process.argv.slice(2)); // [ 'a', 'b' ]

用 util.parseArgs 解析参数

import { parseArgs } from "node:util";
const { values, positionals } = parseArgs({
options: { times: { type: "string", default: "1" } },
allowPositionals: true,
});
for (let i = 0; i < Number(values.times); i++) {
console.log(`你好,${positionals[0]}`);
}
// 运行:node greet.js 小明 --times 2

读环境变量,不存在时给默认值

console.log(process.env.APP_MODE ?? "dev"); // dev

从 .env 文件加载环境变量

终端窗口
node --env-file=.env app.js

以非零退出码结束程序:设 process.exitCode,让程序自然结束

console.error("配置文件不存在");
process.exitCode = 1;

新建项目,生成 package.json

终端窗口
mkdir demo
cd demo
npm init -y

改成 ES 模块:package.json 里加 "type": "module",.js 文件才按 import 解析,详见第 11 章

终端窗口
npm pkg set type=module

添加依赖,同时更新 package.json 和 package-lock.json

终端窗口
npm install zod

添加只在开发时用的依赖:@5、@22 把 TypeScript 和 Node 类型声明固定在对应的大版本(本书示例按 TS 5 写,npm 上的最新版已是 7.x)

终端窗口
npm install --save-dev typescript@5 tsx @types/node@22

移除依赖

终端窗口
npm uninstall zod

package.json:项目名、模块类型、脚本、依赖和开发依赖(节选)

{
"name": "demo",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "tsx src/main.ts",
"check": "tsc --noEmit",
"test": "node --test"
},
"dependencies": {
"zod": "^4.6.5"
},
"devDependencies": {
"@types/node": "^22.20.5",
"tsx": "^4.23.15",
"typescript": "^5.9.3"
}
}

锁文件 package-lock.json 由 npm 维护,提交进版本库,不要手改;拿到别人的项目或在 CI 里,按锁文件原样安装

终端窗口
npm ci

按 package.json 安装全部依赖(会按需更新锁文件)

终端窗口
npm install

运行 scripts 里定义的脚本;start 和 test 可以省掉 run

终端窗口
npm run check
npm test

运行项目里装的命令行工具;没装时 npx 按同名的 npm 包临时下载,tsc 这个包名不是 TypeScript,所以 npx tsc 之前要先装 typescript

终端窗口
npx prettier --version

TS 生成 tsconfig.json

终端窗口
npx tsc --init

TS 用 tsx 运行、tsc 只做检查时的 tsconfig.json(本表的 TS 代码都按这份检查过)

{
"compilerOptions": {
"target": "es2023",
"lib": ["esnext"],
"module": "nodenext",
"types": ["node"],
"strict": true,
"noEmit": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
}
}

const 声明不能重新赋值的变量,let 声明能重新赋值的变量;默认用 const

const name = "Ada";
let count = 0;
count += 1;
console.log(name, count); // Ada 1

TS 类型标注写在名字后面;能推断时不用写,详见第 3 章

const port: number = 8080;
let label = "端口"; // 推断为 string
console.log(label, port); // 端口 8080

多重赋值用解构:数组按位置,对象按名字

const [x, y] = [1, 2];
const { host, port } = { host: "localhost", port: 80 };
console.log(x, y, host, port); // 1 2 localhost 80

交换两个变量

let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1

const 只管变量本身,对象的内容照样能改,详见第 4 章

const user = { name: "小明" };
user.name = "小红";
console.log(user.name); // 小红

Object.freeze 冻结对象:ES 模块是严格模式,写入抛 TypeError;只冻结一层

const config = Object.freeze({ port: 80, db: { host: "a" } });
try {
config.port = 81;
} catch (e) {
console.log(e.constructor.name); // TypeError
}
config.db.host = "b";
console.log(config.db.host); // b

TS as const:字面量变成只读,类型收窄到具体的值

const SIZES = ["S", "M", "L"] as const;
type Size = (typeof SIZES)[number]; // "S" | "M" | "L"
const s: Size = "M";
console.log(s, SIZES.length); // M 3

作用域规则:let、const 是块级作用域,只在所在的花括号里可见

if (true) {
const inner = 1;
}
console.log(typeof inner); // undefined

在声明之前读 let、const 变量抛 ReferenceError(暂时性死区)

try {
console.log(value);
} catch (e) {
console.log(e.constructor.name); // ReferenceError
}
const value = 1;

内层函数能读写外层的变量,不需要额外声明

let counter = 0;
function bump() {
counter += 1;
}
bump();
console.log(counter); // 1

字符串转数字:Number 要求整个字符串是数字,Number.parseInt 可指定进制

console.log(Number("42"), Number("42px"), Number.parseInt("ff", 16)); // 42 NaN 255

数字只有一种(64 位浮点);转整数用 Math.trunc(向零截断)或 Math.floor(向下)

console.log(Math.trunc(-3.9), Math.floor(-3.9)); // -3 -4

任意值转字符串

console.log(String(42) + String(3.5) + String(true)); // 423.5true

转布尔值:false、0、NaN、空串、null、undefined、0n 为假,其余为真(包括空数组)

console.log(Boolean(0), Boolean(""), Boolean([]), Boolean("0")); // false false true true

/ 总是得小数;整数除法用 Math.trunc;% 的符号跟被除数;** 乘方

console.log(7 / 2, Math.trunc(7 / 2), Math.floor(-7 / 2), -7 % 3, 2 ** 10); // 3.5 3 -4 -1 1024

超过 2 ** 53 的整数会丢精度;大整数用 BigInt(字面量加 n)

console.log(Number.MAX_SAFE_INTEGER, 2 ** 53 + 1); // 9007199254740991 9007199254740992
console.log(2n ** 100n); // 1267650600228229401496703205376n

浮点数有误差:比较时看差值,显示时用 toFixed

console.log(0.1 + 0.2); // 0.30000000000000004
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // true
console.log((0.1 + 0.2).toFixed(2)); // 0.30

Math.round 遇到 .5 向正无穷取整

console.log(Math.round(2.5), Math.round(-2.5), Math.round(3.14159 * 100) / 100); // 3 -2 3.14

判断类型:基本类型用 typeof,数组用 Array.isArray,类的实例用 instanceof

console.log(typeof 3, typeof "a", typeof null, Array.isArray([]), new Date() instanceof Date);
// number string object true true

非法的数字运算得 NaN,NaN 不等于自己,判断用 Number.isNaN

const n = Number("abc");
console.log(n === n, Number.isNaN(n)); // false true

数字字面量:下划线分隔、十六进制、二进制

console.log(1_000_000, 0xff, 0b1010); // 1000000 255 10

TS 用 typeof 判断后,分支里的类型自动收窄

function double(x: number | string): number {
if (typeof x === "string") {
return Number(x) * 2; // 这里 x 是 string
}
return x * 2; // 这里 x 是 number
}
console.log(double("21"), double(4)); // 42 8

插值用模板字符串(反引号)

const name = "小明", age = 18;
console.log(`${name} 今年 ${age} 岁`); // 小明 今年 18 岁

数字格式:小数位、千分位、百分比、补零、宽度

console.log(
(3.14159).toFixed(2),
(1234567).toLocaleString("en-US"),
new Intl.NumberFormat("en-US", { style: "percent" }).format(0.25),
String(42).padStart(5, "0"),
"右".padStart(4),
); // 3.14 1,234,567 25% 00042 右

拼接少量用 + 或模板字符串,拼接一列用 join

console.log("a" + "b", ["2026", "10", "08"].join("-")); // ab 2026-10-08

取字符和子串:at 支持负数,slice 不含结束位置

const s = "JavaScript";
console.log(s[0], s.at(-1), s.slice(0, 4), s.slice(-6)); // J t Java Script

查找:includes 判断包含,indexOf 找不到返回 -1

const s = "hello world";
console.log(s.includes("wor"), s.indexOf("o"), s.indexOf("z"), s.startsWith("he")); // true 4 -1 true

替换:replace 只换第一个,replaceAll 全换

console.log("a-b-c".replace("-", "/"), "a-b-c".replaceAll("-", "/")); // a/b-c a/b/c

用正则替换和提取

const s = "订单 12 和 345";
console.log(s.replace(/\d+/g, "#"), s.match(/\d+/g)); // 订单 # 和 # [ '12', '345' ]

分割:按任意空白分割要用正则

console.log("x,y,,z".split(","), "a b\tc".trim().split(/\s+/));
// [ 'x', 'y', '', 'z' ] [ 'a', 'b', 'c' ]

大小写

const s = "hello World";
console.log(s.toUpperCase(), s.toLowerCase()); // HELLO WORLD hello world

去掉两端空白,或只去开头、结尾

const s = " hi \n";
console.log(JSON.stringify([s.trim(), s.trimStart(), s.trimEnd()])); // ["hi","hi \n"," hi"]

多行字符串:模板字符串里直接换行

const sql = `
SELECT *
FROM users
`.trim();
console.log(sql.split("\n")); // [ 'SELECT *', 'FROM users' ]

原始字符串:String.raw,反斜杠不转义

console.log(String.raw`C:\new\table`); // C:\new\table

length 数的是 UTF-16 码元,不是字符;按码点数用展开,详见第 14 章

const s = "中😀";
console.log(s.length, [...s].length, new TextEncoder().encode(s).length); // 3 2 7

字符串和字节互转

const bytes = new TextEncoder().encode("你好");
console.log(bytes.length, new TextDecoder().decode(bytes)); // 6 你好

按中文拼音排序用 localeCompare

console.log(["香蕉", "苹果", "橙子"].sort((a, b) => a.localeCompare(b, "zh-CN")));
// [ '橙子', '苹果', '香蕉' ]

数组:末尾增删用 push、pop,开头增删用 unshift、shift,中间用 splice

const nums = [3, 1, 2];
nums.push(4);
nums.unshift(0);
nums.splice(1, 1); // 从下标 1 起删 1 个
const last = nums.pop();
console.log(nums, last); // [ 0, 1, 2 ] 4

数组:索引、修改、长度、包含、切片

const nums = [10, 20, 30, 40];
nums[0] = 11;
console.log(nums.at(-1), nums.length, nums.includes(20), nums.slice(1, 3)); // 40 4 true [ 20, 30 ]

排序:toSorted 返回新数组,sort 原地排;数字一定要传比较函数

const nums = [10, 9, 1];
console.log(nums.toSorted((a, b) => a - b)); // [ 1, 9, 10 ]
const words = ["banana", "Apple", "cherry"];
words.sort((a, b) => b.length - a.length);
console.log(words); // [ 'banana', 'cherry', 'Apple' ]

变换和筛选:map、filter(相当于推导式)

const nums = [1, 2, 3, 4];
console.log(nums.filter((n) => n % 2 === 0).map((n) => n * n)); // [ 4, 16 ]

查找:find 找第一个,some、every 判断有没有、是不是全部

const nums = [3, 8, 12];
console.log(nums.find((n) => n > 5), nums.findIndex((n) => n > 5), nums.some((n) => n > 10), nums.every((n) => n > 0));
// 8 1 true true

汇总:reduce;最大最小用 Math.max(...数组)

const nums = [3, 1, 4];
console.log(nums.reduce((sum, n) => sum + n, 0), Math.max(...nums), Math.min(...nums)); // 8 4 1

按规则生成数组

console.log(Array.from({ length: 4 }, (_, i) => i * i)); // [ 0, 1, 4, 9 ]

字典用 Map:键可以是任意类型,按插入顺序遍历

const ages = new Map([["小明", 18], ["小红", 20]]);
ages.set("小刚", 19);
ages.delete("小红");
console.log(ages.get("小明"), ages.get("小李"), ages.has("小刚"), ages.size); // 18 undefined true 2
for (const [name, age] of ages) {
console.log(name, age);
}

键是固定字符串的小字典也常用普通对象;遍历用 Object.entries

const prices = { 苹果: 3, 梨: 4 };
prices.橙子 = 5;
for (const [name, price] of Object.entries(prices)) {
console.log(name, price);
}

分组:Map.groupBy 和 Object.groupBy(Node 21+)

const words = ["a", "bb", "cc", "d"];
console.log(Map.groupBy(words, (w) => w.length)); // Map(2) { 1 => [ 'a', 'd' ], 2 => [ 'bb', 'cc' ] }

计数:用 Map 累加

const counts = new Map();
for (const ch of "mississippi") {
counts.set(ch, (counts.get(ch) ?? 0) + 1);
}
console.log(counts.get("s")); // 4

Set:去重、并、交、差(集合运算方法 Node 22 起有)

const a = new Set([1, 2, 3]), b = new Set([2, 3, 4]);
a.add(5);
console.log(a.union(b), a.intersection(b), a.difference(b));
// Set(5) { 1, 2, 3, 5, 4 } Set(2) { 2, 3 } Set(2) { 1, 5 }
console.log([...new Set([1, 1, 2])]); // [ 1, 2 ]

展开和剩余:... 展开数组和对象,解构时收集剩余元素

const defaults = { debug: false, port: 80 }, overrides = { port: 8080 };
const [first, ...rest] = [...[1, 2], ...[3]];
console.log(first, rest, { ...defaults, ...overrides }); // 1 [ 2, 3 ] { debug: false, port: 8080 }

不改原数组的版本:toSorted、toReversed、with

const nums = [3, 1, 2];
console.log(nums.toReversed(), nums.with(0, 9), nums); // [ 2, 1, 3 ] [ 9, 1, 2 ] [ 3, 1, 2 ]

TS 集合的类型:数组、只读数组、元组、Record

const ids: number[] = [1, 2];
const frozen: readonly string[] = ["a"];
const pair: [string, number] = ["小明", 18];
const scores: Record<string, number> = { 小明: 90 };
console.log(ids, frozen, pair, scores); // [ 1, 2 ] [ 'a' ] [ '小明', 18 ] { '小明': 90 }

if / else if / else,条件要加括号

const score = 85;
let grade;
if (score >= 90) {
grade = "A";
} else if (score >= 80) {
grade = "B";
} else {
grade = "C";
}
console.log(grade); // B

三元表达式

const n = 7;
console.log(n % 2 ? "奇数" : "偶数"); // 奇数

比较一律用 === 和 !==,不做类型转换

console.log(1 === 1, "1" === 1, "1" == 1); // true false true

for...of 遍历数组、字符串、Map、Set 等可迭代对象

for (const ch of "ab") {
console.log(ch); // 依次打印 a、b
}

计数循环

for (let i = 0; i < 10; i += 3) {
console.log(i); // 依次打印 0 3 6 9
}

同时要下标和元素:entries()

for (const [i, ch] of ["a", "b"].entries()) {
console.log(i, ch); // 依次打印 0 a、1 b
}

while、break、continue

let n = 0;
while (true) {
n += 1;
if (n % 2 === 0) continue;
if (n > 5) break;
console.log(n); // 依次打印 1 3 5
}

带标签的 break 跳出外层循环

outer: for (const row of [[1, 2], [3, 4]]) {
for (const x of row) {
if (x === 3) break outer;
console.log(x); // 依次打印 1 2
}
}

switch 用 === 比较;每个 case 结尾要 break,否则往下贯穿

const code = 404;
switch (code) {
case 200:
case 201:
console.log("成功");
break;
case 404:
console.log("找不到"); // 找不到
break;
default:
console.log("其他");
}

没有 match 语法:按结构分支用解构加 if,详见第 15 章

function handle(event) {
if (Array.isArray(event)) {
const [kind, dx, dy] = event;
if (kind === "move") return `移动 ${dx},${dy}`;
} else if (event?.type === "click") {
const { x, y } = event;
return `点击 (${x}, ${y})`;
}
return "未知";
}
console.log(handle({ type: "click", x: 1, y: 2 }), handle(["move", 3, 0])); // 点击 (1, 2) 移动 3,0

TS 可辨识联合加 switch:每个分支里类型自动收窄,漏写分支时 never 赋值报错,详见第 15 章

type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.PI * s.r ** 2;
case "square":
return s.side ** 2;
default: {
const unreachable: never = s;
return unreachable;
}
}
}
console.log(area({ kind: "square", side: 3 })); // 9

do...while:先执行一次再判断

let tries = 0;
do {
tries += 1;
} while (tries < 3);
console.log(tries); // 3

定义函数,参数可以有默认值

function greet(name, greeting = "你好") {
return `${greeting},${name}`;
}
console.log(greet("小明"), greet("小明", "早上好")); // 你好,小明 早上好,小明

TS 参数和返回值的类型标注;? 表示可选参数

function greet(name: string, greeting?: string): string {
return `${greeting ?? "你好"},${name}`;
}
console.log(greet("小明")); // 你好,小明

默认值每次调用都重新求值,可以放 []、{},详见第 7 章

function addItem(item, items = []) {
items.push(item);
return items;
}
console.log(addItem("a"), addItem("b")); // [ 'a' ] [ 'b' ]

剩余参数:... 收集任意多个参数(得到数组)

function total(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
console.log(total(1, 2, 3)); // 6

没有关键字参数,用对象参数代替:传一个对象,在参数里解构并给默认值,详见第 7 章

function connect(host, { timeout = 5, retries = 0 } = {}) {
return `${host} 超时 ${timeout} 重试 ${retries}`;
}
console.log(connect("example.com", { timeout: 2 })); // example.com 超时 2 重试 0

调用时用 ... 把数组展开成参数

function point(x, y) {
return `(${x}, ${y})`;
}
console.log(point(...[1, 2])); // (1, 2)

返回多个值:返回数组或对象,调用方解构

function minMax(nums) {
return [Math.min(...nums), Math.max(...nums)];
}
const [lo, hi] = minMax([3, 1, 4]);
console.log(lo, hi); // 1 4

箭头函数:只有一个表达式时省掉花括号和 return

const pairs = [["b", 2], ["a", 3], ["c", 1]];
console.log(pairs.toSorted((p, q) => p[1] - q[1])); // [ [ 'c', 1 ], [ 'b', 2 ], [ 'a', 3 ] ]

闭包:内层函数记住并能修改外层变量,详见第 7 章

function makeCounter() {
let count = 0;
return () => ++count;
}
const c = makeCounter();
c();
console.log(c()); // 2

函数是普通的值,可以传入、返回、放进容器

const ops = { double: (x) => x * 2, neg: (x) => -x };
const apply = (f, x) => f(x);
console.log(apply(ops.double, 5), apply(Math.abs, -3)); // 10 3

TS 函数类型的写法

type BinaryOp = (a: number, b: number) => number;
const add: BinaryOp = (a, b) => a + b; // 参数类型由 BinaryOp 推出
console.log(add(1, 2)); // 3

包装函数:用高阶函数给函数加功能

function logged(fn) {
return (...args) => {
console.log(`调用 ${fn.name}(${args.join(", ")})`);
return fn(...args);
};
}
const add = logged(function add(a, b) {
return a + b;
});
console.log(add(1, 2)); // 先打印"调用 add(1, 2)",再打印 3

TS 方法装饰器:标准装饰器只能用在类和类成员上,Node 22 不认,靠 tsx 转译,详见第 22 章

function logged<This, Args extends unknown[], R>(
fn: (this: This, ...args: Args) => R,
context: ClassMethodDecoratorContext<This>,
) {
return function (this: This, ...args: Args): R {
console.log(`调用 ${String(context.name)}(${args.join(", ")})`);
return fn.call(this, ...args);
};
}
class Calc {
@logged
add(a: number, b: number): number {
return a + b;
}
}
console.log(new Calc().add(1, 2)); // 先打印"调用 add(1, 2)",再打印 3

定义类:字段声明在类体里,constructor 是构造方法,方法里用 this

class Account {
balance = 0;
constructor(owner) {
this.owner = owner;
}
deposit(amount) {
this.balance += amount;
}
}
const acc = new Account("小明");
acc.deposit(100);
console.log(acc.owner, acc.balance); // 小明 100

TS 字段要声明类型

class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
console.log(new Point(1, 2)); // Point { x: 1, y: 2 }

字符串表示:toString 用于字符串拼接和插值;console.log 直接打印对象时显示字段

class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x}, ${this.y})`;
}
}
const p = new Point(1, 2);
console.log(`${p}`, p); // (1, 2) Point { x: 1, y: 2 }

私有字段用 # 开头,类外访问是语法错误

class Counter {
#count = 0;
inc() {
return ++this.#count;
}
}
const c = new Counter();
c.inc();
console.log(c.inc(), Object.keys(c)); // 2 []

getter、setter:像属性一样读写,背后是方法(可做校验)

class Temperature {
#celsius = 0;
constructor(celsius) {
this.celsius = celsius;
}
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (value < -273.15) throw new RangeError("低于绝对零度");
this.#celsius = value;
}
get fahrenheit() {
return (this.#celsius * 9) / 5 + 32;
}
}
console.log(new Temperature(100).fahrenheit); // 212

继承用 extends,super 调父类构造方法和方法

class Animal {
constructor(name) {
this.name = name;
}
speak() {
return "...";
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
return `${this.name}:汪`;
}
}
const d = new Dog("旺财", "柴犬");
console.log(d.speak(), d instanceof Animal); // 旺财:汪 true

静态方法和静态字段:属于类本身,常用作备用构造方法

class Person {
static count = 0;
constructor(name, age) {
this.name = name;
this.age = age;
Person.count++;
}
static fromBirthYear(name, year) {
return new Person(name, new Date().getFullYear() - year);
}
}
const p = Person.fromBirthYear("小明", 2000);
console.log(p.name, Person.count); // 小明 1

TS 访问修饰符和 readonly:只在编译时检查;运行时真正私有要用 #

class Account {
readonly id: string;
private balance = 0;
constructor(id: string) {
this.id = id;
}
deposit(amount: number): number {
this.balance += amount;
return this.balance;
}
}
const acc = new Account("A1");
// @ts-expect-error id 是只读的
acc.id = "A2";
console.log(acc.deposit(50)); // 50

TS 参数属性:构造参数前加修饰符,自动声明并赋值字段(tsx 支持,Node 直接运行 .ts 不支持)

class User {
constructor(public readonly name: string, private age: number) {}
isAdult(): boolean {
return this.age >= 18;
}
}
const u = new User("小明", 18);
console.log(u.name, u.isAdult()); // 小明 true

TS 只装数据不需要类:用对象字面量,配一个类型

type User = { name: string; age: number };
const u: User = { name: "小明", age: 18 };
const older = { ...u, age: 19 }; // 复制一份改一处
console.log(u.age, older.age); // 18 19

=== 比较对象比的是引用;按值比较要自己写方法,详见第 5 章

class Money {
constructor(cents) {
this.cents = cents;
}
equals(other) {
return other instanceof Money && other.cents === this.cents;
}
}
console.log(new Money(100) === new Money(100), new Money(100).equals(new Money(100))); // false true

TS 枚举:用 as const 对象加联合类型(不用 enum,Node 直接运行 .ts 不支持它),详见第 15 章

const Color = { Red: "red", Green: "green" } as const;
type Color = (typeof Color)[keyof typeof Color]; // "red" | "green"
function paint(c: Color): string {
return `涂成 ${c}`;
}
console.log(paint(Color.Red), Object.keys(Color)); // 涂成 red [ 'Red', 'Green' ]

TS interface 是结构化的:有这些成员就算实现,不用声明,详见第 8 章

interface Shape {
area(): number;
}
class Square { // 没有写 implements Shape
constructor(public side: number) {}
area(): number {
return this.side ** 2;
}
}
function totalArea(shapes: Shape[]): number {
return shapes.reduce((sum, s) => sum + s.area(), 0);
}
console.log(totalArea([new Square(2), new Square(3), { area: () => 1 }])); // 14

TS implements:让编译器检查类是不是真的实现了接口

interface Closable {
close(): void;
}
class Conn implements Closable {
close(): void {
console.log("关闭");
}
}
new Conn().close(); // 关闭

TS 可选成员、只读成员、接口继承

interface Named {
readonly name: string;
}
interface Plugin extends Named {
version?: string;
}
const p: Plugin = { name: "hello" };
console.log(p.name, p.version ?? "未指定"); // hello 未指定

TS 抽象类:可以带普通方法,子类必须实现抽象方法

abstract class Storage {
abstract save(key: string, value: string): void;
saveAll(items: Record<string, string>): void {
for (const [k, v] of Object.entries(items)) this.save(k, v);
}
}
class MemoryStorage extends Storage {
data = new Map<string, string>();
save(key: string, value: string): void {
this.data.set(key, value);
}
}
const s = new MemoryStorage();
s.saveAll({ a: "1" });
console.log(s.data); // Map(1) { 'a' => '1' }

TS 抽象类不能 new,没实现完抽象方法的子类编译不过

abstract class Storage {
abstract save(key: string): void;
}
// @ts-expect-error 不能创建抽象类的实例
new Storage();

TS 接口在运行时不存在:要在运行时判断,写类型守卫函数

interface Closable {
close(): void;
}
function isClosable(x: unknown): x is Closable {
return typeof x === "object" && x !== null && typeof (x as Closable).close === "function";
}
console.log(isClosable({ close() {} }), isClosable(42)); // true false

TS 联合类型用 type 写;对象形状用 interface 或 type 都可以

type Result = { ok: true; value: number } | { ok: false; error: string };
function show(r: Result): string {
return r.ok ? `值 ${r.value}` : `错误 ${r.error}`;
}
console.log(show({ ok: false, error: "超时" })); // 错误 超时

JS 没有接口,用鸭子类型代替:不声明接口,直接调用方法,详见第 8 章

const things = [{ speak: () => "嘎" }, { speak: () => "滴" }];
for (const t of things) {
console.log(t.speak()); // 依次打印 嘎、滴
}

JS 没有泛型,用 TS 代替:本节条目都只适用于 TS,详见第 9 章。

TS 泛型函数:类型参数写在函数名后的尖括号里,调用时一般能推断,详见第 9 章

function first<T>(items: T[]): T | undefined {
return items[0];
}
console.log(first([3, 1, 2]), first(["a", "b"])); // 3 a

TS 泛型类

class Stack<T> {
#items: T[] = [];
push(item: T): void {
this.#items.push(item);
}
pop(): T | undefined {
return this.#items.pop();
}
}
const s = new Stack<number>();
s.push(1);
s.push(2);
console.log(s.pop()); // 2

TS 多个类型参数

function invert<K extends PropertyKey, V extends PropertyKey>(obj: Record<K, V>): Record<V, K> {
const out = {} as Record<V, K>;
for (const k in obj) out[obj[k]] = k;
return out;
}
console.log(invert({ a: 1, b: 2 })); // { '1': 'a', '2': 'b' }

TS 约束:extends 限定类型参数必须有某些成员

function longer<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(longer("abc", "de"), longer([1], [1, 2])); // abc [ 1, 2 ]

TS keyof 约束:只能传对象上存在的键,返回值类型跟着键走

function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "小明", age: 18 };
const age: number = pick(user, "age");
console.log(age); // 18

TS 类型参数的默认值

class Box<T = string> {
constructor(public item: T) {}
}
const b: Box = new Box("默认是 string");
console.log(b.item); // 默认是 string

TS 泛型类型别名

type Pair<T> = [T, T];
function swap<T>(p: Pair<T>): Pair<T> {
return [p[1], p[0]];
}
console.log(swap([1, 2])); // [ 2, 1 ]

TS 常用的内置泛型工具类型:Partial、Pick、Omit、Readonly

interface User {
name: string;
age: number;
email: string;
}
const patch: Partial<User> = { age: 19 };
const card: Pick<User, "name"> = { name: "小明" };
const publicUser: Omit<User, "email"> = { name: "小明", age: 18 };
console.log(patch, card, publicUser); // { age: 19 } { name: '小明' } { name: '小明', age: 18 }

TS 类型参数只在编译时存在,运行时被擦掉,不能 instanceof T

class Box<T> {
constructor(public item: T) {}
}
const b = new Box<number>(JSON.parse('"不是数字"')); // JSON.parse 返回 any,编译通过
console.log(typeof b.item); // string

抛出异常:抛 Error 或它的子类,不要抛字符串

function sqrt(x) {
if (x < 0) throw new RangeError(`负数不能开平方:${x}`);
return Math.sqrt(x);
}
console.log(sqrt(9)); // 3

try / catch / finally:finally 总会执行,详见第 10 章

try {
JSON.parse("{坏的");
} catch (e) {
console.log(e.name); // SyntaxError
} finally {
console.log("结束"); // 结束
}

catch 不能按类型分开写,在里面用 instanceof 区分,不认识的再抛出去

try {
null.length;
} catch (e) {
if (e instanceof TypeError) {
console.log("类型错误"); // 类型错误
} else {
throw e;
}
}

TS catch 的变量类型是 unknown,用之前先收窄

try {
JSON.parse("{坏的");
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
console.log(message.length > 0); // true
}

不需要异常对象时省掉 catch 的括号

let port;
try {
port = JSON.parse("abc");
} catch {
port = 8080;
}
console.log(port); // 8080

自定义错误:继承 Error,设置 name

class ConfigError extends Error {
name = "ConfigError";
constructor(path, reason) {
super(`${path}:${reason}`);
this.path = path;
}
}
try {
throw new ConfigError("app.json", "端口超出范围");
} catch (e) {
console.log(`${e}`, e.path, e instanceof ConfigError); // ConfigError: app.json:端口超出范围 app.json true
}

包一层再抛:用 cause 保留原始错误

function loadPort(text) {
try {
return JSON.parse(text);
} catch (e) {
throw new Error(`端口配置无效:${text}`, { cause: e });
}
}
try {
loadPort("abc");
} catch (e) {
console.log(e.message, "原因:", e.cause.name); // 端口配置无效:abc 原因: SyntaxError
}

异步函数里的错误:await 被拒绝的 Promise 时在原地抛出,照样用 try / catch

async function fetchUser() {
throw new Error("网络断了");
}
try {
await fetchUser();
} catch (e) {
console.log(e.message); // 网络断了
}

几个任务各自出错时不中断其余的:Promise.allSettled 收齐结果

const results = await Promise.allSettled([Promise.resolve(1), Promise.reject(new Error("坏"))]);
console.log(results.map((r) => r.status)); // [ 'fulfilled', 'rejected' ]

用 finally 清理资源:出错也一样会关闭

import { open } from "node:fs/promises";
const file = await open("demo.txt", "w");
try {
await file.write("hi");
} finally {
await file.close();
}

using:离开作用域时自动调用 [Symbol.dispose]();JS 已标准化(ES2027,TC39 stage 4),Node 22 原生不支持,这里靠 tsx 转译,详见第 10 章

class Timer {
#start = performance.now();
constructor(private label: string) {}
[Symbol.dispose](): void {
console.log(`${this.label}结束`);
}
}
{
using t = new Timer("求和");
console.log("计算中");
}
// 先打印"计算中",再打印"求和结束"

TS 异步资源用 await using 和 [Symbol.asyncDispose]()

class Conn {
async [Symbol.asyncDispose](): Promise<void> {
console.log("连接已关闭");
}
}
{
await using conn = new Conn();
console.log("查询中");
}
// 先打印"查询中",再打印"连接已关闭"

有两种空值:undefined 表示“没有值”(变量没赋值、属性不存在、函数没返回),null 表示“有意为空”,详见第 6 章

let x;
const obj = {};
function f() {}
console.log(x, obj.missing, f(), null); // undefined undefined undefined null

同时判断 null 和 undefined:== null(唯一常用 == 的地方)

for (const v of [null, undefined, 0, ""]) {
console.log(v == null); // 依次打印 true true false false
}

可选链 ?.:左边是 null 或 undefined 就整体得 undefined,不报错

const user = { profile: null };
console.log(user.profile?.name, user.getName?.(), user.tags?.[0]); // undefined undefined undefined

?? 只在 null 或 undefined 时用默认值;|| 会把 0、空串也换掉

const count = 0;
console.log(count ?? 10, count || 10); // 0 10

??=:变量为空时才赋值

const options = { retries: 0 };
options.retries ??= 3;
options.timeout ??= 5;
console.log(options); // { retries: 0, timeout: 5 }

TS 可空类型写 T | null;strict 模式下必须先排除空值才能用

function findUser(id: number): string | null {
return id === 1 ? "小明" : null;
}
const name = findUser(2);
console.log(name?.length ?? 0); // 0

TS 可选属性 ?:类型自动带上 undefined

interface Config {
host: string;
port?: number;
}
const c: Config = { host: "localhost" };
const port: number = c.port ?? 8080;
console.log(port); // 8080

TS 非空断言 !:告诉编译器“这里一定不是空”,运行时不检查,错了照样崩

const map = new Map([["a", 1]]);
const n: number = map.get("a")!;
console.log(n); // 1

Map.get、find、数组越界取到的都是 undefined,不抛异常

console.log(new Map().get("k"), [1, 2].find((n) => n > 5), [1, 2][5]); // undefined undefined undefined

导入 Node 内置模块:加 node: 前缀,按名字导入需要的部分

import { readFileSync } from "node:fs";
import path from "node:path";
console.log(typeof readFileSync, path.join("a", "b").length); // function 3

导入时起别名

import { join as joinPath } from "node:path";
console.log(joinPath("a", "b").length); // 3

导出和导入自己的文件:相对路径要写扩展名;没有 export 的名字在模块外不可见,详见第 11 章

utils.js
export function add(a, b) {
return a + b;
}
function helper() {}
// main.js
import { add } from "./utils.js";
console.log(add(1, 2)); // 3

TS 导入 TS 文件时路径也写 .js,tsc 和 tsx 都会对应到 .ts 文件;用 node 直接运行 .ts(类型剥离)时不做这层对应,要写 .ts

utils.ts
export function add(a: number, b: number): number {
return a + b;
}
// main.ts
import { add } from "./utils.js";
console.log(add(1, 2)); // 3

TS 只导入类型用 import type,编译后整行消失

types.ts
export interface User {
name: string;
}
// main.ts
import type { User } from "./types.js";
const u: User = { name: "小明" };
console.log(u.name); // 小明

把整个模块导入成一个对象

import * as path from "node:path";
console.log(path.extname("a.txt")); // .txt

默认导出:一个模块一个;具名导出更便于重构和自动导入,优先用具名导出

greet.js
export default function greet(name) {
return `你好,${name}`;
}
// main.js
import greet from "./greet.js";
console.log(greet("小明")); // 你好,小明

ES 模块的顶层可以直接 await

import { setTimeout as sleep } from "node:timers/promises";
await sleep(10);
console.log("等完了"); // 等完了

动态导入:运行时按需加载,返回 Promise

const { platform } = await import("node:os");
console.log(typeof platform()); // string

当前文件的路径:import.meta.filename、import.meta.dirname(ES 模块里没有 __dirname)

import path from "node:path";
console.log(path.basename(import.meta.filename), path.isAbsolute(import.meta.dirname)); // main.js true

判断文件是被直接运行还是被导入

import { pathToFileURL } from "node:url";
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
console.log("直接运行");
}

导入 JSON 文件要写 with { type: "json" }

main.js
// config.json
{ "port": 8080 }
import config from "./config.json" with { type: "json" };
console.log(config.port); // 8080

可迭代对象有 [Symbol.iterator]() 方法,返回的迭代器每次 next() 给出 { value, done },详见第 13 章

const it = [1, 2][Symbol.iterator]();
console.log(it.next(), it.next(), it.next());
// { value: 1, done: false } { value: 2, done: false } { value: undefined, done: true }

生成器函数:function* 加 yield,调用后得到一个惰性的迭代器

function* fib() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const gen = fib();
console.log(Array.from({ length: 8 }, () => gen.next().value).join(" ")); // 0 1 1 2 3 5 8 13

自定义可迭代类:把 [Symbol.iterator] 写成生成器方法

class Countdown {
constructor(start) {
this.start = start;
}
*[Symbol.iterator]() {
for (let n = this.start; n > 0; n--) yield n;
}
}
console.log([...new Countdown(3)]); // [ 3, 2, 1 ]

不用生成器手写迭代器:返回带 next() 的对象

function range(end) {
let i = 0;
return {
[Symbol.iterator]() {
return this;
},
next() {
return i < end ? { value: i++, done: false } : { value: undefined, done: true };
},
};
}
console.log([...range(3)]); // [ 0, 1, 2 ]

TS 生成器的类型写 Generator<产出类型>

function* evens(limit: number): Generator<number> {
for (let n = 0; n < limit; n += 2) yield n;
}
console.log([...evens(7)]); // [ 0, 2, 4, 6 ]

yield*:把另一个可迭代对象的元素逐个交出去

function* flatten(nested) {
for (const item of nested) {
if (Array.isArray(item)) yield* flatten(item);
else yield item;
}
}
console.log([...flatten([1, [2, [3, 4]], 5])]); // [ 1, 2, 3, 4, 5 ]

迭代器辅助方法:filter、map、take、drop 惰性执行,toArray 收集(Node 22 起有)

function* naturals() {
for (let n = 0; ; n++) yield n;
}
console.log(naturals().filter((n) => n % 2 === 0).map((n) => n * n).take(5).toArray()); // [ 0, 4, 16, 36, 64 ]

Iterator.from:把任何可迭代对象包成带辅助方法的迭代器

const set = new Set(["a", "b", "c"]);
console.log(Iterator.from(set).drop(1).toArray()); // [ 'b', 'c' ]

Map 的 keys()、values()、entries() 返回迭代器,要数组就展开

const m = new Map([["a", 1], ["b", 2]]);
console.log([...m.keys()], [...m.values()]); // [ 'a', 'b' ] [ 1, 2 ]

生成器只能遍历一次

function* three() {
yield* [0, 1, 4];
}
const squares = three();
console.log([...squares], [...squares]); // [ 0, 1, 4 ] []

异步生成器:async function* 产出,for await...of 消费

import { setTimeout as sleep } from "node:timers/promises";
async function* ticks(n) {
for (let i = 1; i <= n; i++) {
await sleep(10);
yield i;
}
}
for await (const t of ticks(3)) {
console.log(t); // 依次打印 1 2 3
}

async 函数返回 Promise,await 等它完成;等待用 node:timers/promises 的 setTimeout,详见第 19 章

import { setTimeout as sleep } from "node:timers/promises";
async function hello() {
await sleep(100);
return "你好";
}
console.log(await hello()); // 你好

Promise.all:同时等待多个任务,结果按传入顺序;一个失败就整体失败

import { setTimeout as sleep } from "node:timers/promises";
async function fetchN(n) {
await sleep(100);
return n * 10;
}
console.log(await Promise.all([fetchN(1), fetchN(2), fetchN(3)])); // [ 10, 20, 30 ]

Promise.race:取最先完成的那个

import { setTimeout as sleep } from "node:timers/promises";
console.log(await Promise.race([sleep(50, "快"), sleep(200, "慢")])); // 快

TS 异步函数的返回类型写 Promise<T>

async function getPort(): Promise<number> {
return 8080;
}
const port: number = await getPort();
console.log(port); // 8080

超时:AbortSignal.timeout 生成一个到时就取消的信号,详见第 21 章

import { setTimeout as sleep } from "node:timers/promises";
try {
await sleep(1000, null, { signal: AbortSignal.timeout(100) });
} catch (e) {
console.log(e.name); // AbortError
}

主动取消:AbortController,把它的 signal 传给支持取消的函数

import { setTimeout as sleep } from "node:timers/promises";
const controller = new AbortController();
setTimeout(() => controller.abort(), 50);
try {
await sleep(1000, null, { signal: controller.signal });
} catch (e) {
console.log("已取消", e.name); // 已取消 AbortError
}

把回调式 API 包成 Promise:Promise.withResolvers

import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
const { promise, resolve } = Promise.withResolvers();
emitter.once("ready", resolve);
setTimeout(() => emitter.emit("ready", "就绪"), 10);
console.log(await promise); // 就绪

CPU 密集的任务放进 Worker 线程:同一个文件既当主线程又当 Worker,详见第 17 章

import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
if (isMainThread) {
const worker = new Worker(new URL(import.meta.url), { workerData: 30 });
worker.on("message", (result) => console.log(result)); // 832040
} else {
const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2));
parentPort.postMessage(fib(workerData));
}

普通的 JS 代码只在一个线程上跑,不需要线程锁,但两次 await 之间可能插进别的代码(第 19 章);Worker 之间共享内存时用 Atomics

const shared = new Int32Array(new SharedArrayBuffer(4));
Atomics.add(shared, 0, 5);
console.log(Atomics.load(shared, 0)); // 5

运行外部程序:node:child_process 的 execFile,详见第 18 章

import { execFile } from "node:child_process";
import { promisify } from "node:util";
const { stdout } = await promisify(execFile)("node", ["--version"]);
console.log(stdout.trim().startsWith("v")); // true

执行顺序:同步代码 → 微任务(Promise 回调)→ 定时器等宏任务

setTimeout(() => console.log("定时器"), 0);
Promise.resolve().then(() => console.log("微任务"));
console.log("同步");
// 依次打印 同步、微任务、定时器

整个读写文本文件:node:fs/promises 的 readFile、writeFile,写明 "utf8"

import { readFile, writeFile } from "node:fs/promises";
await writeFile("note.txt", "第一行\n第二行\n", "utf8");
const text = await readFile("note.txt", "utf8");
console.log(text.split("\n").filter(Boolean)); // [ '第一行', '第二行' ]

逐行读大文件:readline 配 for await

import { createReadStream } from "node:fs";
import { writeFile } from "node:fs/promises";
import { createInterface } from "node:readline";
await writeFile("log.txt", "a\nb\n", "utf8");
for await (const line of createInterface({ input: createReadStream("log.txt", "utf8") })) {
console.log(line); // 依次打印 a、b
}

追加写入

import { appendFile } from "node:fs/promises";
await appendFile("log.txt", "新的一行\n", "utf8");

路径拼接用 path.join,拆出文件名、扩展名、父目录

import path from "node:path";
const p = path.join("data", "2026", "report.csv");
console.log(path.basename(p), path.parse(p).name, path.extname(p), path.dirname(p).split(path.sep));
// report.csv report .csv [ 'data', '2026' ]

判断存在、建多级目录、删文件

import { existsSync } from "node:fs";
import { mkdir, rm, writeFile } from "node:fs/promises";
await mkdir("out/logs", { recursive: true });
await writeFile("out/logs/x.txt", "x", "utf8");
console.log(existsSync("out/logs/x.txt")); // true
await rm("out", { recursive: true, force: true });

列出目录下的直接子项

import { readdir } from "node:fs/promises";
for (const entry of await readdir(".", { withFileTypes: true })) {
console.log(entry.name, entry.isDirectory() ? "目录" : "文件");
}

递归列出所有文件,再按扩展名筛选

import { readdir } from "node:fs/promises";
const all = await readdir(".", { recursive: true });
console.log(all.filter((f) => f.endsWith(".txt")));

对象转 JSON 字符串,缩进两格(中文原样输出)

const data = { name: "小明", tags: ["a", "b"], ok: true };
console.log(JSON.stringify(data, null, 2));

JSON 字符串转对象

const obj = JSON.parse('{"n": 1, "items": [1, 2], "x": null}');
console.log(obj, obj.items[1]); // { n: 1, items: [ 1, 2 ], x: null } 2

读写 JSON 文件

import { readFile, writeFile } from "node:fs/promises";
await writeFile("config.json", JSON.stringify({ port: 8080 }, null, 2), "utf8");
const config = JSON.parse(await readFile("config.json", "utf8"));
console.log(config.port); // 8080

TS JSON.parse 返回 any:标注成 unknown 再检查,详见第 3 章

interface Config {
port: number;
}
function isConfig(x: unknown): x is Config {
return typeof x === "object" && x !== null && typeof (x as Config).port === "number";
}
const raw: unknown = JSON.parse('{"port": "8080"}');
console.log(isConfig(raw) ? raw.port : "格式不对"); // 格式不对

临时目录:mkdtemp 建,用完自己删

import { existsSync } from "node:fs";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
const dir = await mkdtemp(path.join(tmpdir(), "demo-"));
await writeFile(path.join(dir, "t.txt"), "临时", "utf8");
console.log(existsSync(dir)); // true
await rm(dir, { recursive: true });
console.log(existsSync(dir)); // false

标准库 node:test 的最小测试文件:test 定义测试,断言用 node:assert/strict

calc.js
export function add(a, b) {
return a + b;
}
// calc.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { add } from "./calc.js";
test("add 把两个数相加", () => {
assert.equal(add(1, 2), 3);
});

运行当前目录下所有测试文件(文件名形如 *.test.js、*.test.mjs 等)

终端窗口
node --test

只跑一个文件,或只跑名字匹配的测试

终端窗口
node --test calc.test.js
node --test --test-name-pattern="add"

TS 测试文件用 tsx 跑

终端窗口
npx tsx --test calc.test.ts

比较对象和数组用 deepEqual

import { test } from "node:test";
import assert from "node:assert/strict";
test("深比较", () => {
assert.deepEqual({ a: [1, 2] }, { a: [1, 2] });
});

断言会抛出某种异常;异步函数用 rejects

import { test } from "node:test";
import assert from "node:assert/strict";
test("抛出 SyntaxError", () => {
assert.throws(() => JSON.parse("{"), SyntaxError);
});
test("异步拒绝", async () => {
await assert.rejects(Promise.reject(new TypeError("坏")), TypeError);
});

分组和每个测试前准备数据:describe、it、beforeEach

import { beforeEach, describe, it } from "node:test";
import assert from "node:assert/strict";
describe("栈", () => {
let items;
beforeEach(() => {
items = [1, 2, 3];
});
it("pop 取出最后一个", () => {
assert.equal(items.pop(), 3);
});
it("长度不受上一个测试影响", () => {
assert.equal(items.length, 3);
});
});

一个测试里跑多组数据:循环里用 t.test 建子测试,某组失败不影响其余组

import { test } from "node:test";
import assert from "node:assert/strict";
test("平方", async (t) => {
for (const [n, expected] of [[2, 4], [3, 9], [-1, 1]]) {
await t.test(`n=${n}`, () => assert.equal(n * n, expected));
}
});

模拟函数:mock.fn 记录调用次数和参数

import { mock, test } from "node:test";
import assert from "node:assert/strict";
test("回调被调用一次", () => {
const callback = mock.fn();
[1].forEach(callback);
assert.equal(callback.mock.callCount(), 1);
});

改了代码自动重跑测试

终端窗口
node --test --watch

Prettier(第三方,格式化的事实标准)加为开发依赖

终端窗口
npm install --save-dev prettier

格式化整个项目;--check 只检查不改

终端窗口
npx prettier --write .
npx prettier --check .

ESLint(第三方,lint 的事实标准)加 TS 支持

终端窗口
npm install --save-dev eslint @eslint/js typescript-eslint globals

ESLint 的配置文件 eslint.config.js:推荐规则加 TS 规则

import eslint from "@eslint/js";
import { defineConfig } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";
export default defineConfig(
eslint.configs.recommended,
tseslint.configs.recommended,
{ languageOptions: { globals: globals.node } }, // 认识 process、console 等 Node 全局变量
);

检查代码问题;--fix 自动修复能修的

终端窗口
npx eslint .
npx eslint --fix .

TS 类型检查本身就是最重要的检查

终端窗口
npx tsc --noEmit

把它们写进 package.json 的 scripts,用 npm run lint 一起跑

{
"scripts": {
"format": "prettier --write .",
"lint": "eslint . && tsc --noEmit"
}
}

只检查语法、不运行

终端窗口
node --check app.js

== 会先转换类型:"0" == 0、[] == false 都为真 → 结果难以预料 → 一律用 ===,判断空值时 == null 例外

console.log("0" == 0, [] == false, "0" === 0); // true true false

sort() 不传比较函数 → 按字符串排序,10 排在 9 前面 → 数字排序写 (a, b) => a - b

console.log([10, 9, 1].sort(), [10, 9, 1].sort((a, b) => a - b)); // [ 1, 10, 9 ] [ 1, 9, 10 ]

把方法当回调传出去 → 调用时 this 丢了,报 TypeError → 用箭头函数包一层,或 bind,详见第 7 章

class Counter {
count = 0;
inc() {
this.count++;
}
}
const c = new Counter();
const inc = c.inc;
try {
inc();
} catch (e) {
console.log(e.constructor.name); // TypeError
}
[1, 2].forEach(() => c.inc());
console.log(c.count); // 2

在 forEach 里 await → forEach 不等回调的 Promise,后面的代码先执行 → 顺序执行用 for...of,并发用 Promise.all

import { setTimeout as sleep } from "node:timers/promises";
const done = [];
[1, 2].forEach(async (n) => {
await sleep(10);
done.push(n);
});
console.log(done.length); // 0
for (const n of [1, 2]) {
await sleep(10);
done.push(n);
}

忘了 await 或 catch → 被拒绝的 Promise 没人处理,Node 15 起整个进程退出 → 每个 Promise 都要 await 或接 .catch,详见第 10 章

async function save() {
throw new Error("写入失败");
}
save().catch((e) => console.log("已处理:", e.message)); // 已处理: 写入失败

用 var 声明循环变量 → 所有闭包共用同一个变量,全都拿到最后的值 → 用 let,每轮一个新变量

const fs1 = [];
for (var i = 0; i < 3; i++) fs1.push(() => i);
const fs2 = [];
for (let j = 0; j < 3; j++) fs2.push(() => j);
console.log(fs1.map((f) => f()), fs2.map((f) => f())); // [ 3, 3, 3 ] [ 0, 1, 2 ]

展开 { ...obj } 只复制一层 → 嵌套对象仍是同一个,改一处两边都变 → 嵌套结构用 structuredClone,详见第 5 章

const a = { pos: { x: 0 } };
const shallow = { ...a }, deep = structuredClone(a);
a.pos.x = 9;
console.log(shallow.pos.x, deep.pos.x); // 9 0

JSON.stringify 遇到 undefined 字段直接丢掉,Map、Set 变成 {} → 存取前转成普通对象或数组

const m = new Map([["a", 1]]);
console.log(JSON.stringify({ x: undefined, m }), JSON.stringify(Object.fromEntries(m))); // {"m":{}} {"a":1}

["1", "2", "3"].map(parseInt) → map 把下标当第二个参数(进制)传进去 → 得 [1, NaN, NaN] → 写成箭头函数或用 Number

console.log(["1", "2", "3"].map(parseInt), ["1", "2", "3"].map(Number)); // [ 1, NaN, NaN ] [ 1, 2, 3 ]

TS 的类型在运行时不存在:JSON.parse(...) as User → 编译通过,数据格式不对时程序在别处崩 → 外部数据先在运行时校验,详见第 3 章

interface User {
name: string;
}
const u = JSON.parse('{"nmae": "小明"}') as User; // 字段名拼错了,编译照样通过
console.log(u.name); // undefined