附录 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 --versionnpm --version在代码里查看版本
console.log(process.version); // v22.23.2console.log(process.versions.v8); // V8 引擎版本安装 fnm(第三方,管理多个 Node 版本)
# macOS / Linuxcurl -fsSL https://fnm.vercel.app/install | bash# Windowswinget install Schniz.fnm让 fnm 在当前 shell 里生效,进入目录时自动切换版本(写进 shell 的启动文件)
eval "$(fnm env --use-on-cd --shell bash)" # bashfnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression # PowerShell用 fnm 安装、切换、列出 Node 版本
fnm install 22fnm use 22fnm list设默认版本
fnm default 22项目里固定 Node 版本:根目录放 .node-version(也认 .nvmrc),fnm 进入目录时自动切换
node --version > .node-versionTS 查看项目里装的 TypeScript 版本
npx tsc --version运行 JS 文件
node hello.jsTS 用 tsx(第三方)运行 TS 文件:只擦掉类型、不做检查,所以快
npx tsx hello.tsTS Node 22.18 起也能直接运行 .ts:同样只擦类型,enum、参数属性这类要生成代码的语法不支持,详见第 2 章
node hello.tsTS 类型检查要另跑 tsc,--noEmit 表示只查不输出文件
npx tsc --noEmitTS 要输出 JS 文件时,tsconfig.json 里去掉 noEmit、加上 "outDir": "dist",再运行 tsc
npx tscnode 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 bconsole.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;项目初始化与依赖管理
Section titled “项目初始化与依赖管理”新建项目,生成 package.json
mkdir democd demonpm 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 zodpackage.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 checknpm test运行项目里装的命令行工具;没装时 npx 按同名的 npm 包临时下载,tsc 这个包名不是 TypeScript,所以 npx tsc 之前要先装 typescript
npx prettier --versionTS 生成 tsconfig.json
npx tsc --initTS 用 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 1TS 类型标注写在名字后面;能推断时不用写,详见第 3 章
const port: number = 8080;let label = "端口"; // 推断为 stringconsole.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 1const 只管变量本身,对象的内容照样能改,详见第 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); // bTS 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基本类型与转换
Section titled “基本类型与转换”字符串转数字: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 9007199254740992console.log(2n ** 100n); // 1267650600228229401496703205376n浮点数有误差:比较时看差值,显示时用 toFixed
console.log(0.1 + 0.2); // 0.30000000000000004console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // trueconsole.log((0.1 + 0.2).toFixed(2)); // 0.30Math.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 10TS 用 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\tablelength 数的是 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 2for (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")); // 4Set:去重、并、交、差(集合运算方法 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 truefor...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,0TS 可辨识联合加 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 })); // 9do...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 3TS 函数类型的写法
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)",再打印 3TS 方法装饰器:标准装饰器只能用在类和类成员上,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); // 小明 100TS 字段要声明类型
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); // 小明 1TS 访问修饰符和 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)); // 50TS 参数属性:构造参数前加修饰符,自动声明并赋值字段(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()); // 小明 trueTS 只装数据不需要类:用对象字面量,配一个类型
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 trueTS 枚举:用 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 }])); // 14TS 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 falseTS 联合类型用 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 aTS 泛型类
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()); // 2TS 多个类型参数
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); // 18TS 类型参数的默认值
class Box<T = string> { constructor(public item: T) {}}const b: Box = new Box("默认是 string");console.log(b.item); // 默认是 stringTS 泛型类型别名
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)); // 3try / 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); // 0TS 可选属性 ?:类型自动带上 undefined
interface Config { host: string; port?: number;}const c: Config = { host: "localhost" };const port: number = c.port ?? 8080;console.log(port); // 8080TS 非空断言 !:告诉编译器“这里一定不是空”,运行时不检查,错了照样崩
const map = new Map([["a", 1]]);const n: number = map.get("a")!;console.log(n); // 1Map.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 章
export function add(a, b) { return a + b;}function helper() {}
// main.jsimport { add } from "./utils.js";console.log(add(1, 2)); // 3TS 导入 TS 文件时路径也写 .js,tsc 和 tsx 都会对应到 .ts 文件;用 node 直接运行 .ts(类型剥离)时不做这层对应,要写 .ts
export function add(a: number, b: number): number { return a + b;}
// main.tsimport { add } from "./utils.js";console.log(add(1, 2)); // 3TS 只导入类型用 import type,编译后整行消失
export interface User { name: string;}
// main.tsimport 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默认导出:一个模块一个;具名导出更便于重构和自动导入,优先用具名导出
export default function greet(name) { return `你好,${name}`;}
// main.jsimport 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" }
// config.json{ "port": 8080 }
import config from "./config.json" with { type: "json" };console.log(config.port); // 8080迭代与生成器
Section titled “迭代与生成器”可迭代对象有 [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("同步");// 依次打印 同步、微任务、定时器文件与 JSON
Section titled “文件与 JSON”整个读写文本文件: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")); // trueawait 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); // 8080TS 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)); // trueawait rm(dir, { recursive: true });console.log(existsSync(dir)); // false标准库 node:test 的最小测试文件:test 定义测试,断言用 node:assert/strict
export function add(a, b) { return a + b;}
// calc.test.jsimport { 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.jsnode --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格式化与 lint
Section titled “格式化与 lint”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 globalsESLint 的配置文件 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 falsesort() 不传比较函数 → 按字符串排序,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); // 0for (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 0JSON.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