JavaScript 核心语义:值、对象、函数与模块 / JavaScript Core Semantics: Values, Objects, Functions, and Modules
📅 创建时间:2026-07-28 🏷️ 标签:#JavaScript #CoreSemantics #Runtime #Prototype #Closure #Proxy 📚 前置知识:[[00-overview]]
📋 本章目标
- 理解 JS 中值与引用的精确行为,掌握浅拷贝、深拷贝、structuredClone 的边界
- 掌握
null/undefined的语义差异,正确使用??、?.、默认值模式 - 理解作用域链与闭包的底层机制,能运用闭包实现封装、模块模式与 once 函数
- 掌握
this的四种绑定规则,能诊断并修复常见的this丢失场景 - 深入理解原型链:
__proto__与prototype的关系、class 语法糖本质、继承模式 - 掌握 Map / Set / WeakMap / WeakSet 的特性与适用场景
- 理解 Proxy 与 Reflect 的拦截机制及其在响应式系统中的角色
- 掌握迭代器协议、生成器函数、异步迭代的实际用法
- 理解 ESM 模块系统的各种导入导出变体及其与 CJS 的互操作
- 掌握 try/catch/finally、自定义错误类、AggregateError 与全局错误处理
第1部分:值与引用——JS 世界的"复制"真相
1.1 基本类型与引用类型的分界线
JS 的值世界有一条清晰的分界线:基本类型按值传递,对象按引用传递。
┌─────────────────────────────────────────────────────────────┐
│ 值的两大阵营 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 基本类型 (Primitives) — 7 种 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ string number bigint boolean symbol │ │
│ │ undefined null │ │
│ │ │ │
│ │ 特点:不可变、按值比较、赋值时复制 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 引用类型 (Reference Types) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Object Array Function Date RegExp │ │
│ │ Map Set WeakMap WeakSet Promise Error │ │
│ │ │ │
│ │ 特点:可变、按引用比较、赋值时共享同一内存 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘基本类型赋值时,值本身被复制;对象赋值时,只有引用被复制。
// 基本类型:各自独立
let a = 42;
let b = a;
b = 100;
console.log(a); // 42 — a 不受影响
// 引用类型:共享同一对象
const original = { count: 1 };
const alias = original;
alias.count += 1;
console.log(original.count); // 2 — 它们指向同一个对象1.2 浅拷贝的陷阱
展开语法 ... 和 Object.assign() 只做浅层复制——第一层属性是新的,但嵌套对象仍然是共享引用。
┌─────────────────────────────────────────────────────────────┐
│ 浅拷贝示意 │
├─────────────────────────────────────────────────────────────┤
│ │
│ source = { user: { name: 'Ada' }, score: 10 } │
│ │
│ ┌──────────────────┐ ┌─────────────┐ │
│ │ source │ │ { name } │ │
│ │ user ──────────┼────→│ │ │
│ │ score: 10 │ └─────────────┘ │
│ └──────────────────┘ │
│ │ │
│ │ 浅拷贝 │
│ ▼ │
│ ┌──────────────────┐ │
│ │ copy │ │
│ │ user ──────────┼──→ 同一个 { name } 对象! │
│ │ score: 10 │ │
│ └──────────────────┘ │
│ │
│ copy.user.name = 'Grace' → source.user.name 也变成 Grace │
│ │
└─────────────────────────────────────────────────────────────┘const source = { user: { name: 'Ada' } };
const copy = { ...source };
copy.user.name = 'Grace';
console.log(source.user.name); // 'Grace' — 嵌套对象被共享
// Array 同理
const arr = [[1, 2], [3, 4]];
const shallowArr = [...arr];
shallowArr[0].push(99);
console.log(arr[0]); // [1, 2, 99]1.3 深拷贝方案对比
不可变更新必须复制所有被修改路径上的对象。
// 方案1:手动逐层展开——适合已知结构的浅层对象
const deepCopy = {
...source,
user: { ...source.user, name: 'Grace' }
};
// 方案2:JSON 序列化——仅限纯数据,丢失函数/Date/undefined/BigInt
const jsonCopy: typeof source = JSON.parse(JSON.stringify(source));
// ⚠️ Date → string, undefined → 消失, BigInt → TypeError
// 方案3:structuredClone——现代深拷贝标准方案
const cloned = structuredClone(source);
// ✅ 支持 Date, RegExp, Map, Set, ArrayBuffer, Blob, 循环引用
// ❌ 不支持 Function, Symbol, DOM 节点
// 方案4:Lodash.cloneDeep / Immer produce——复杂场景的工业选择┌─────────────────────────────────────────────────────────────┐
│ 深拷贝方案对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 方案 │ Date │ Fn │ BigInt │ 循环引用 │ 性能 │
│ ├────────────────┼──────┼────┼───────┼─────────┼──────┤
│ │ ... 展开 │ 保留 │保留│ 保留 │ ✗ │ 最快 │
│ │ JSON 序列化 │ 丢失 │丢失│ ✗ │ ✗ │ 中等 │
│ │ structuredClone│ 保留 │✗ │ ✅ │ ✅ │ 快 │
│ │ Lodash cloneDeep│ 保留│保留│ ✅ │ ✅ │ 较慢 │
│ │
│ 通用最佳实践: │
│ • 纯数据对象 → structuredClone │
│ • 不可变状态树 → Immer produce │
│ • 已知浅结构 → 手动展开(最明确、最快) │
│ │
└─────────────────────────────────────────────────────────────┘第2部分:null、undefined 与空值世界
2.1 两种"无"的语义
┌─────────────────────────────────────────────────────────────┐
│ null vs undefined │
├─────────────────────────────────────────────────────────────┤
│ │
│ undefined — "不曾存在" │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 变量已声明但未赋值 │ │
│ │ • 函数没有 return 语句时的返回值 │ │
│ │ • 访问对象上不存在的属性 │ │
│ │ • 函数调用时未传入的参数 │ │
│ │ • typeof undefined → "undefined" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ null — "刻意空缺" │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 程序显式表达"此处没有对象" │ │
│ │ • 通常表示一个预期的空值 │ │
│ │ • typeof null → "object" (历史遗留 bug) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.2 ?? vs ||——空值合并的精髓
这是 JS 中最容易被误解的操作符对之一。|| 判断的是"假值"(falsy),而 ?? 只判断 null 和 undefined。
// || 的陷阱:0、空字符串、false 都被当作"假"
const count = 0;
const display = count || 10; // 10 — 0 被错误地替换了!
const enabled = false;
const flag = enabled || true; // true — false 被吞掉了
// ?? 才是真正的"空值"判断
const correct = count ?? 10; // 0 — 0 是合法值,保留
const flag2 = enabled ?? true; // false — false 是合法值,保留
const name = null ?? '匿名'; // '匿名' — null 被替换┌─────────────────────────────────────────────────────────────┐
│ ?? vs || 决策表 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 值 || 'default' ?? 'default' │
│ ───────────────────────────────────────────────────── │
│ null 'default' 'default' │
│ undefined 'default' 'default' │
│ false 'default' false │
│ 0 'default' 0 │
│ '' 'default' '' │
│ NaN 'default' NaN │
│ │
│ 规则:|| 替换 8 个 falsy 值,?? 只替换 null/undefined │
│ │
└─────────────────────────────────────────────────────────────┘2.3 可选链 ?.——优雅的安全访问
// 之前的写法:层层检查,噪音巨大
const city = user && user.address && user.address.city;
// 可选链:短路求值,遇到 null/undefined 立即返回 undefined
const city = user?.address?.city;
// 方法调用
const result = api?.fetch?.('/data');
// 数组索引
const first = arr?.[0];
// 与 ?? 组合:安全访问 + 默认值
const displayName = user?.profile?.name ?? '匿名用户';注意:?. 是短路操作符,不要滥用——如果 user 为 null 表示程序 bug,让它抛出错误比静默返回 undefined 更好。
第3部分:作用域与闭包
3.1 三种作用域
┌─────────────────────────────────────────────────────────────┐
│ JS 作用域体系 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 全局作用域 (Global) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ var 声明的全局变量挂在 window/globalThis 上 │ │
│ │ let/const 声明的全局变量不挂载 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 函数作用域 (Function) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ var 声明的变量属于整个函数体 │ │
│ │ 存在"变量提升" (hoisting) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 块级作用域 (Block) — let / const │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ { }、if、for、while 内部的 let/const 绑定 │ │
│ │ 不存在提升(存在 TDZ — 暂时性死区) │ │
│ │ const 声明的是不可重新赋值的绑定,不是不可变值 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘// var 的函数作用域——经典陷阱
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}
// 只有一个 i,所有回调共享它
// let 的块作用域——每次迭代创建新的绑定
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0); // 0, 1, 2
}
// TDZ 示例
console.log(x); // ReferenceError — 不是 undefined!
let x = 1;3.2 闭包的底层模型
闭包 = 函数 + 其定义位置的词法环境引用。JS 引擎在执行函数时会创建词法环境(Lexical Environment),包含环境记录和对 [[OuterEnv]] 的引用。
┌─────────────────────────────────────────────────────────────┐
│ 闭包的词法环境链 │
├─────────────────────────────────────────────────────────────┤
│ │
│ function outer(prefix) { ← outer 的词法环境 │
│ const secret = 42; ← { prefix, secret } │
│ │
│ return function inner(msg) { ← inner 的词法环境 │
│ return `${prefix}: ${msg}-${secret}` │
│ } ↑ │
│ } │ │
│ │ │
│ const fn = outer('INFO') │ │
│ fn('start') // 'INFO: start-42' ─┘ │
│ │
│ inner.[[OuterEnv]] → outer 的词法环境 │
│ inner 存活 → outer 的环境不能被 GC │
│ │
└─────────────────────────────────────────────────────────────┘3.3 闭包的实用模式
// 模式1:封装私有状态
function createCounter(initial = 0) {
let value = initial;
return {
next: () => { value += 1; return value; },
reset: () => { value = initial; return value; },
get value() { return value; }, // 只读访问器
};
}
const counter = createCounter(5);
counter.next(); // 6
counter.next(); // 7
// value 无法从外部直接访问——真正的私有
// 模式2:once 函数
function once<T extends (...args: any[]) => any>(fn: T): T {
let called = false;
let result: ReturnType<T>;
return ((...args: any[]) => {
if (!called) {
called = true;
result = fn(...args);
}
return result;
}) as T;
}
const initApp = once(() => { console.log('初始化完成'); return 'ready'; });
initApp(); // 打印 '初始化完成',返回 'ready'
initApp(); // 什么都不做,直接返回 'ready'
// 模式3:模块模式(IIFE 闭包)
const UserModule = (() => {
const users: string[] = [];
return {
add(name: string) { users.push(name); },
list() { return [...users]; }, // 返回副本,保护内部数据
};
})();3.4 闭包的内存考量
闭包引用的是整个外层词法环境,不是它实际用到的变量。这在 V8 中可能导致不必要的大对象长期驻留。
function outer() {
const hugeData = new Array(10_000_000).fill('x'); // 大对象
const small = 'used';
return function inner() {
// 只用了 small,但 hugeData 也可能无法被 GC
return small;
};
}
const fn = outer();
// hugeData 在某些引擎中可能仍然可达
// 最佳实践:不需要的变量手动置 null
function outerFixed() {
let hugeData: string[] | null = new Array(10_000_000).fill('x');
const small = 'used';
hugeData = null; // 明确切断引用
return function inner() {
return small;
};
}第4部分:函数调用与 this 绑定
4.1 四种绑定规则
┌─────────────────────────────────────────────────────────────┐
│ this 绑定的四种规则 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 默认绑定 (Default) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 独立函数调用 → 严格模式 undefined,非严格模式 window │ │
│ │ fn() // this = undefined (strict) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. 隐式绑定 (Implicit) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 通过对象调用 → this 指向调用者 │ │
│ │ obj.fn() // this = obj │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3. 显式绑定 (Explicit) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ call / apply / bind → 显式指定 this │ │
│ │ fn.call(ctx) // this = ctx │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 4. new 绑定 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ new Fn() → 创建新对象,this 指向新对象 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 优先级:new > 显式 > 隐式 > 默认 │
│ │
└─────────────────────────────────────────────────────────────┘4.2 this 丢失的经典场景与修复
const user = {
name: 'Ada',
greet() { return `Hello, ${this.name}`; },
};
// 场景1:方法被提取后独立调用——丢失隐式绑定
const g = user.greet;
// g(); // TypeError: Cannot read properties of undefined
// 修复:bind 硬绑定
const boundGreet = user.greet.bind(user);
boundGreet(); // 'Hello, Ada'
// 场景2:回调函数中 this 丢失
class Toggle {
constructor(public active = false) {}
handleClick() {
this.active = !this.active;
}
// ❌ 错误:事件回调中 this 指向 DOM 元素
// button.addEventListener('click', this.handleClick);
// ✅ 修复方案1:bind 构造函数
// constructor() { this.handleClick = this.handleClick.bind(this); }
// ✅ 修复方案2:箭头函数包装
attach(el: HTMLElement) {
el.addEventListener('click', () => this.handleClick());
}
}
// 场景3:setTimeout 回调
const obj = {
name: 'Timer',
delayed() {
// ❌ setTimeout(() => this.log(), 100); — 如果 log 未定义会出错
// ✅ this 被箭头函数捕获
setTimeout(() => { console.log(this.name); }, 100);
},
};4.3 箭头函数与普通函数的 this 差异
箭头函数没有自己的 this,它的 this 是词法解析时从外层作用域捕获的——this 的值在定义时就决定了,不受调用方式影响。
const group = {
name: 'Team A',
members: ['Alice', 'Bob'],
// 普通函数方法——this 由调用方式决定
printMembers() {
return this.members.map(function(m) {
// ❌ this 指向 undefined(严格模式下的独立调用)
return `${m} of ${this.name}`;
});
},
// 箭头函数版——this 从 printMembersArrow 的词法作用域捕获
printMembersArrow() {
return this.members.map(m => `${m} of ${this.name}`);
},
};
// group.printMembersArrow() → ['Alice of Team A', 'Bob of Team A']判断口诀:谁调用的不重要,看函数定义时的外层作用域的 this 是什么。如果函数不需要动态接收者,更清晰的做法是通过参数传入依赖。
第5部分:原型链深度剖析
5.1 __proto__ 与 prototype——最容易混淆的两个属性
┌─────────────────────────────────────────────────────────────┐
│ __proto__ vs prototype │
├─────────────────────────────────────────────────────────────┤
│ │
│ __proto__ ([[Prototype]]) — 每个对象都有 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 指向该对象的原型 │ │
│ │ obj.__proto__ → 属性查找时沿此链向上回溯 │ │
│ │ 规范名称:[[Prototype]],__proto__ 是历史访问器 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ prototype — 只有函数(含 class)有这个属性 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 当用 new 调用该函数时,新对象的 __proto__ 指向它 │ │
│ │ Fn.prototype → 新对象的原型 │ │
│ │ 箭头函数没有 prototype! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ new Fn() 发生的事: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 创建空对象 obj │ │
│ │ 2. obj.__proto__ = Fn.prototype │ │
│ │ 3. 以 obj 为 this 执行 Fn 构造函数 │ │
│ │ 4. 返回 obj(除非构造函数显式返回对象) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘5.2 属性查找与原型链
┌─────────────────────────────────────────────────────────────┐
│ 原型链查找示意 │
├─────────────────────────────────────────────────────────────┤
│ │
│ const arr = [1, 2, 3] │
│ │
│ arr.push(4) // 属性查找路径: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ arr 自身有 push 吗? → 没有 │ │
│ │ ↓ arr.__proto__ → Array.prototype │ │
│ │ Array.prototype 有 push 吗? → 有!调用它 │ │
│ │ ↓ 如果还没有 → Object.prototype │ │
│ │ Object.prototype 有吗? → 如果还没有 → null(终点) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ arr.__proto__ === Array.prototype // true │
│ arr.__proto__.__proto__ === Object.prototype // true │
│ arr.__proto__.__proto__.__proto__ === null // true │
│ │
└─────────────────────────────────────────────────────────────┘5.3 class 是语法糖——验证
class Animal {
constructor(public name: string) {}
speak() { return `${this.name} makes a sound`; }
}
class Dog extends Animal {
speak() { return `${this.name} barks`; }
}
// class 底层等价于:
function AnimalFn(this: any, name: string) {
this.name = name;
}
AnimalFn.prototype.speak = function() {
return `${this.name} makes a sound`;
};
function DogFn(this: any, name: string) {
AnimalFn.call(this, name);
}
DogFn.prototype = Object.create(AnimalFn.prototype);
DogFn.prototype.constructor = DogFn;
DogFn.prototype.speak = function() {
return `${this.name} barks`;
};
// 验证原型链
const dog = new Dog('Rex');
dog instanceof Dog; // true
dog instanceof Animal; // true
dog instanceof Object; // true5.4 Object.create()——无构造函数的原型继承
// 直接指定原型,不经过构造函数
const baseConfig = {
timeout: 5000,
retries: 3,
log() { console.log(`timeout=${this.timeout}`); },
};
// devConfig.__proto__ = baseConfig
const devConfig = Object.create(baseConfig);
devConfig.timeout = 10000; // 在 devConfig 自身上创建属性
devConfig.log(); // 'timeout=10000' — 查找:自身有 timeout,不再沿链找
delete devConfig.timeout;
devConfig.log(); // 'timeout=5000' — 自身没有了,沿链找到 baseConfig 的
// 创建"纯字典"(无原型链,避免 __proto__ 键污染)
const dict = Object.create(null);
dict.__proto__ = 'evil';
console.log(dict.__proto__); // 'evil' — 被当作普通属性,不影响原型5.5 instanceof 与属性遮蔽
// instanceof 检查构造函数的 prototype 是否在对象的原型链上
class A {}
class B extends A {}
const b = new B();
b instanceof B; // true — B.prototype 在 b 的原型链上
b instanceof A; // true — A.prototype 也在链上
// 属性遮蔽 (Property Shadowing)
const parent = { value: 'parent' };
const child = Object.create(parent);
child.value = 'child'; // 在 child 自身上创建 value,遮蔽了 parent 的
console.log(child.value); // 'child' — 自身属性
console.log(parent.value); // 'parent' — 未被改变
delete child.value; // 移除遮蔽
console.log(child.value); // 'parent' — 再次沿链查找第6部分:Map、Set、WeakMap、WeakSet
6.1 四种集合类型对比
┌─────────────────────────────────────────────────────────────┐
│ 集合类型全景对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 特性 │ Map │ Set │ WeakMap │ WeakSet │
│ ├─────────────┼───────┼───────┼─────────┼───────────┤
│ │ 键类型 │ 任意 │ — │ 仅对象 │ 仅对象 │
│ │ 迭代 │ ✅ │ ✅ │ ✗ │ ✗ │
│ │ size 属性 │ ✅ │ ✅ │ ✗ │ ✗ │
│ │ GC 行为 │ 阻止 │ 阻止 │ 不阻止 │ 不阻止 │
│ │ 键顺序 │ 插入 │ 插入 │ — │ — │
│ │ 典型场景 │ 缓存 │ 去重 │ 私有数据 │ 标记 │
│ │
└─────────────────────────────────────────────────────────────┘6.2 Map vs Object——何时用 Map
// Map 的优势场景
// 1. 键可以是任意类型(对象、函数、NaN)
const cache = new Map<object, string>();
const key1 = { id: 1 };
cache.set(key1, 'value1');
cache.get(key1); // 'value1' — 对象作为键
// 2. 保持插入顺序,有 size 属性
const m = new Map([['a', 1], ['b', 2]]);
console.log(m.size); // 2
// 3. 遍历便捷
for (const [key, value] of m) { /* ... */ }
const keys = [...m.keys()];
const values = [...m.values()];
// 4. 高性能频繁增删——Map 对此优化,Object 更适合静态属性集6.3 WeakMap——私有数据与弱引用
WeakMap 最关键的特性:键是弱引用,不阻止垃圾回收。键所指向的对象如果没有其他引用,即使它还是 WeakMap 的键,也会被 GC 回收。
┌─────────────────────────────────────────────────────────────┐
│ WeakMap 弱引用示意 │
├─────────────────────────────────────────────────────────────┤
│ │
│ const wm = new WeakMap(); │
│ let obj = { data: 'important' }; │
│ wm.set(obj, 'metadata'); │
│ │
│ ┌─────────┐ ┌──────────────────┐ │
│ │ obj │ ───────→ │ { data: ... } │ │
│ │ (强引用) │ │ │ │
│ └─────────┘ └──────────────────┘ │
│ ↑ │
│ ┌─────────────────┐ │ │
│ │ wm │ ───────┘ (弱引用) │
│ │ 不阻止 GC │ │
│ └─────────────────┘ │
│ │
│ obj = null; // 唯一的强引用消失 │
│ → GC 可以回收 { data: ... } │
│ → WeakMap 中的条目也随之消失 │
│ │
└─────────────────────────────────────────────────────────────┘// 实战:用 WeakMap 存储私有数据
const _private = new WeakMap<object, { count: number }>();
class Counter {
constructor() {
_private.set(this, { count: 0 });
}
increment() {
const p = _private.get(this)!;
p.count += 1;
return p.count;
}
}
// 当 Counter 实例被 GC 时,_private 中的关联数据自动消失
// 没有内存泄漏风险
// 对比:如果用 Map,即使 Counter 实例已经无用了,
// 只要 Map 还活着,实例和私有数据都无法被回收6.4 Set 与 WeakSet
// Set:去重、成员检查、集合运算
const tags = new Set<string>(['js', 'ts']);
tags.add('js'); // 已存在,忽略
tags.has('js'); // true — O(1)
tags.delete('js');
console.log(tags.size); // 1
// 数组去重的简洁方式(注意:仅基本类型可靠)
const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
// WeakSet:弱引用对象集合,常用于标记
const visited = new WeakSet<object>();
function process(node: object) {
if (visited.has(node)) return; // 防止循环处理
visited.add(node);
// ... 处理逻辑
}第7部分:Proxy 与 Reflect
7.1 Proxy——对对象操作的全面拦截
Proxy 可以拦截对象上的 13 种内部方法,包括属性读取、赋值、删除、函数调用、new 操作等。
┌─────────────────────────────────────────────────────────────┐
│ Proxy 拦截模型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 操作 → Proxy (handler) → target │
│ │
│ ┌──────────┐ ┌──────────────────┐ ┌──────────┐ │
│ │ 代码 │ ──→ │ Proxy 拦截层 │ ──→ │ 目标对象 │ │
│ │ proxy.x │ │ get() 陷阱 │ │ target.x │ │
│ │ proxy.x=1│ │ set() 陷阱 │ │ │ │
│ │ x in proxy│ │ has() 陷阱 │ │ │ │
│ │ delete │ │ deleteProperty() │ │ │ │
│ │ proxy() │ │ apply() 陷阱 │ │ │ │
│ │ new proxy│ │ construct() 陷阱 │ │ │ │
│ └──────────┘ └──────────────────┘ └──────────┘ │
│ │
│ 共 13 个陷阱,覆盖对象的所有内部操作 │
│ │
└─────────────────────────────────────────────────────────────┘7.2 实战用例
// 用例1:验证代理
function createValidator<T extends object>(
obj: T,
rules: { [K in keyof T]?: (v: T[K]) => boolean }
): T {
return new Proxy(obj, {
set(target, prop, value) {
const validator = (rules as any)[prop];
if (validator && !validator(value)) {
throw new TypeError(`Invalid value for ${String(prop)}: ${value}`);
}
return Reflect.set(target, prop, value);
},
}) as T;
}
const user = createValidator({ name: '', age: 0 }, {
age: (v: number) => v > 0 && v < 150,
});
// user.age = -1; // TypeError
// 用例2:访问日志
function withLogging<T extends object>(obj: T, label: string): T {
return new Proxy(obj, {
get(target, prop) {
const value = Reflect.get(target, prop);
console.log(`[${label}] GET ${String(prop)} →`, value);
return typeof value === 'function'
? value.bind(target) // 方法需要绑定正确的 this
: value;
},
set(target, prop, value) {
console.log(`[${label}] SET ${String(prop)} =`, value);
return Reflect.set(target, prop, value);
},
}) as T;
}
// 用例3:默认值代理
function withDefault<T extends object>(obj: Partial<T>, defaults: T): T {
return new Proxy(obj, {
get(target, prop) {
const value = Reflect.get(target, prop);
return value !== undefined ? value : Reflect.get(defaults, prop);
},
}) as T;
}7.3 Reflect——Proxy 的最佳搭档
Reflect 提供了与 Proxy 陷阱一一对应的静态方法,是执行默认操作的规范方式。
// Proxy 陷阱与 Reflect 方法对照
// ┌──────────────────┬─────────────────────────┐
// │ Proxy 陷阱 │ Reflect 方法 │
// ├──────────────────┼─────────────────────────┤
// │ get │ Reflect.get(target, prop, receiver) │
// │ set │ Reflect.set(target, prop, value, rcvr) │
// │ has │ Reflect.has(target, prop) │
// │ deleteProperty │ Reflect.deleteProperty(target, prop) │
// │ apply │ Reflect.apply(fn, thisArg, args) │
// │ construct │ Reflect.construct(ctor, args) │
// │ ownKeys │ Reflect.ownKeys(target) │
// │ getPrototypeOf │ Reflect.getPrototypeOf(target) │
// Reflect 的优势:有返回值(成功/失败),不抛异常
try { delete obj.prop; } catch {} // 旧方式
if (Reflect.deleteProperty(obj, 'prop')) { // ✅ 返回 boolean
// 删除成功
}7.4 Vue 3 响应式概述
Vue 3 的 reactive() 基于 Proxy 实现,追踪属性访问(get 中调用 track)和修改(set 中调用 trigger)。
┌─────────────────────────────────────────────────────────────┐
│ Vue 3 响应式简化模型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ reactive(obj) → new Proxy(obj, { │
│ get(target, key, receiver) { │
│ track(target, key) // 记录:谁在依赖这个属性 │
│ return Reflect.get(target, key, receiver) │
│ }, │
│ set(target, key, value, receiver) { │
│ const old = target[key] │
│ const ok = Reflect.set(target, key, value, receiver) │
│ if (old !== value) trigger(target, key) // 通知更新 │
│ return ok │
│ } │
│ }) │
│ │
│ 这就是为什么 Vue 3 能检测新增/删除属性 │
│ (Vue 2 的 Object.defineProperty 做不到) │
│ │
└─────────────────────────────────────────────────────────────┘第8部分:迭代器与生成器
8.1 迭代器协议
┌─────────────────────────────────────────────────────────────┐
│ 迭代器协议 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Iterable(可迭代对象) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 必须实现 [Symbol.iterator]() 方法 │ │
│ │ 返回一个 Iterator(迭代器) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Iterator(迭代器) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 必须实现 next() 方法 │ │
│ │ 返回 { value: T, done: boolean } │ │
│ │ done 为 true 表示迭代结束 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 消费方式:for...of / [...spread] / Array.from() │
│ │
└─────────────────────────────────────────────────────────────┘// 手写一个可迭代对象
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
},
};
for (const n of range) { console.log(n); } // 1 2 3 4 5
console.log([...range]); // [1, 2, 3, 4, 5]8.2 生成器函数 function*
生成器是创建迭代器的语法糖——调用生成器函数返回一个迭代器,yield 暂停执行,next() 恢复执行。
// 生成器版本——更简洁
function* rangeGen(from: number, to: number) {
for (let i = from; i <= to; i++) {
yield i; // 暂停,返回 { value: i, done: false }
}
// 函数结束 → 返回 { value: undefined, done: true }
}
const r = rangeGen(1, 5);
r.next(); // { value: 1, done: false }
r.next(); // { value: 2, done: false }
// ...
// yield*:委托给另一个可迭代对象
function* flat<T>(arrays: T[][]) {
for (const arr of arrays) {
yield* arr; // 逐个产出 arr 的元素
}
}
console.log([...flat([[1, 2], [3, 4]])]); // [1, 2, 3, 4]┌─────────────────────────────────────────────────────────────┐
│ 生成器的双向通信 │
├─────────────────────────────────────────────────────────────┤
│ │
│ function* biDi() { │
│ const x = yield '给外面的值'; // 暂停,向外发出 │
│ const y = yield x * 2; // 暂停,用传入的值计算 │
│ return y; │
│ } │
│ │
│ const gen = biDi(); │
│ gen.next() // { value: '给外面的值', done: false } │
│ gen.next(10) // 将 10 赋给 x,执行 x * 2 │
│ // → { value: 20, done: false } │
│ gen.next(99) // 将 99 赋给 y,函数结束 │
│ // → { value: 99, done: true } │
│ │
│ 关键点:next(arg) 的参数会成为上一个 yield 的返回值 │
│ │
└─────────────────────────────────────────────────────────────┘8.3 异步迭代与 for await...of
// 异步可迭代对象:Symbol.asyncIterator
async function* asyncRange(from: number, to: number) {
for (let i = from; i <= to; i++) {
await new Promise(r => setTimeout(r, 100)); // 模拟异步
yield i;
}
}
// 对异步可迭代对象使用解构 ❌ 不可以(需要 await)
// 使用 for await...of
for await (const n of asyncRange(1, 5)) {
console.log(n); // 每隔 100ms 打印一次
}
// 异步生成器在处理分页 API、流式数据时非常有用
async function* paginatedFetch(url: string) {
let page = 1;
while (true) {
const res = await fetch(`${url}?page=${page}`);
const data = await res.json();
if (data.items.length === 0) break;
yield* data.items;
page++;
}
}第9部分:模块系统
9.1 ESM 导出/导入变体全集
┌─────────────────────────────────────────────────────────────┐
│ ESM 语法全览 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 导出 (export) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ // 命名导出 │ │
│ │ export const x = 1; │ │
│ │ export function fn() {} │ │
│ │ export class C {} │ │
│ │ export { a, b as alias }; // 集中导出+重命名 │ │
│ │ │ │
│ │ // 默认导出(一个模块只能有一个) │ │
│ │ export default function() {} │ │
│ │ export { fn as default }; │ │
│ │ │ │
│ │ // 重导出(聚合模块) │ │
│ │ export { foo } from './other.js'; │ │
│ │ export { default as Foo } from './other.js'; │ │
│ │ export * from './other.js'; // 所有命名导出 │ │
│ │ export * as Lib from './lib.js'; // 命名空间 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 导入 (import) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ import defaultExport from './mod.js'; │ │
│ │ import { a, b as alias } from './mod.js'; │ │
│ │ import defaultExport, { a } from './mod.js'; │ │
│ │ import * as Lib from './mod.js'; // 命名空间 │ │
│ │ import './side-effects.js'; // 仅执行副作用 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘9.2 动态导入与 import.meta
// 动态 import() — 返回 Promise,支持按需加载
async function loadLocale(lang: string) {
const locale = await import(`./locales/${lang}.js`);
return locale.default;
}
// import.meta — 模块元信息
console.log(import.meta.url); // file:///path/to/module.js
console.log(import.meta.dirname); // Node.js 22+ 支持
// 条件加载
if (process.env.NODE_ENV === 'development') {
const { devTools } = await import('./devtools.js');
devTools.enable();
}9.3 ESM 与 CJS 互操作
┌─────────────────────────────────────────────────────────────┐
│ ESM ↔ CJS 互操作 │
├─────────────────────────────────────────────────────────────┤
│ │
│ CJS 模块 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ // math.cjs │ │
│ │ module.exports = { add: (a,b) => a + b }; │ │
│ │ // 或 exports.add = (a, b) => a + b; │ │
│ │ │ │
│ │ // 使用 │ │
│ │ const { add } = require('./math.cjs'); │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ESM 导入 CJS (Node.js 支持) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ import math from './math.cjs'; │ │
│ │ // 或 import { add } from './math.cjs'; │ │
│ │ // 或 import * as math from './math.cjs'; │ │
│ │ // Node.js 会包装 CJS 的 exports 作为默认导出 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ CJS 导入 ESM (有限制) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ const mod = await import('./module.mjs'); │ │
│ │ // CJS 中不能同步 require ESM 模块 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ package.json 中的 type 字段: │
│ • "type": "module" → .js 文件被解析为 ESM │
│ • "type": "commonjs" 或无 → .js 被解析为 CJS │
│ • .mjs 强制 ESM,.cjs 强制 CJS,不受 type 影响 │
│ │
└─────────────────────────────────────────────────────────────┘第10部分:错误处理
10.1 try/catch/finally 的完整语义
// finally 总是在控制流离开 try/catch 之前执行
function demo() {
try {
console.log('1. try');
return 'from try';
} catch (e) {
console.log('2. catch');
return 'from catch';
} finally {
console.log('3. finally — always runs');
// ⚠️ finally 中的 return 会覆盖 try/catch 的 return!
}
}
console.log(demo());
// 输出:1. try → 3. finally — always runs → 'from try'
// catch 可以省略绑定变量(ES2019+)
try {
riskyOperation();
} catch { // 不需要 error 对象
fallback();
}10.2 自定义错误类
// 自定义错误——包含业务上下文
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500,
options?: { cause?: unknown }
) {
super(message, options); // Error 构造函数支持 cause 选项
this.name = 'AppError';
// 确保 instanceof 正确工作(TS 目标 < ES2015 时需要)
Object.setPrototypeOf(this, AppError.prototype);
}
}
class ValidationError extends AppError {
constructor(
message: string,
public readonly fields: Record<string, string>
) {
super(message, 'VALIDATION_ERROR', 400);
this.name = 'ValidationError';
}
}
// 使用 error.cause 保留原始错误(ES2022+)
try {
await fetch('/api/data');
} catch (err) {
throw new AppError('Failed to fetch data', 'FETCH_ERROR', 502, {
cause: err, // 保留底层错误
});
}
// 上层可以访问 error.cause 进行根因分析10.3 AggregateError——聚合多个错误
// 并行操作中同时发生了多个错误
const errors: Error[] = [];
try { await validateName(input.name); } catch (e) { errors.push(e as Error); }
try { await validateEmail(input.email); } catch (e) { errors.push(e as Error); }
try { await validateAge(input.age); } catch (e) { errors.push(e as Error); }
if (errors.length > 0) {
throw new AggregateError(errors, 'Validation failed');
}
// 捕获后逐个检查
try {
await validateForm(input);
} catch (e) {
if (e instanceof AggregateError) {
for (const err of e.errors) {
console.error(err.message);
}
}
}10.4 全局错误处理
┌─────────────────────────────────────────────────────────────┐
│ 全局错误处理层次 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 浏览器端 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ window.onerror — 未捕获的同步错误 │ │
│ │ window.onunhandledrejection — 未处理的 Promise 拒绝 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Node.js 端 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ process.on('uncaughtException') │ │
│ │ process.on('unhandledRejection') │ │
│ │ 注意:uncaughtException 后应优雅退出,状态已不可靠 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 框架级(Express 示例) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ app.use((err, req, res, next) => { │ │
│ │ // 4 参数中间件自动成为错误处理中间件 │ │
│ │ }); │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘// 业务层的错误处理原则
// 1. Result 类型——可预期的失败不是异常
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function safeParseJSON<T>(text: string): Promise<Result<T>> {
try {
return { ok: true, value: JSON.parse(text) };
} catch (error) {
return { ok: false, error: error as Error };
}
}
const result = await safeParseJSON<User>('{"name":"Ada"}');
if (result.ok) {
console.log(result.value.name); // 类型安全
} else {
console.error(result.error.message);
}
// 2. 错误应传播到能够补充上下文或执行恢复的层次
// 3. 永远不要空 catch——吞掉错误让系统变得不可观测核心总结
总结1:JS 运行时的五根支柱
┌─────────────────────────────────────────────────────────────┐
│ JS 核心语义五根支柱 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 值与引用 — 基本类型复制,对象共享;浅拷贝不穿透嵌套 │
│ 2. 作用域与闭包 — let/const 块作用域,闭包 = 函数+词法环境│
│ 3. this 绑定 — 四规则(new > 显式 > 隐式 > 默认),箭头 │
│ 函数无自己的 this │
│ 4. 原型链 — __proto__ 查找,prototype 构造,class 是语法糖 │
│ 5. 模块与错误 — ESM 静态结构,错误应在能处理的层面捕获 │
│ │
└─────────────────────────────────────────────────────────────┘总结2:数据结构选型速查
| 需求 | 使用 | 不使用 |
|---|---|---|
| 键值对,键是对象或需要顺序/大小 | Map | Object |
| 唯一值集合 | Set | Array (手动去重) |
| 对象私有数据(无泄漏) | WeakMap | Map |
| 已处理对象标记 | WeakSet | Set |
| 拦截对象操作 | Proxy | Object.defineProperty |
| 纯字典(无原型) | Object.create(null) | {} |
总结3:工程心态
JavaScript 的工程能力来自对值、作用域、调用、原型、模块和异步的准确预测。框架会变化,但这些运行时语义长期稳定。能把 this 丢失场景一眼看穿、知道闭包何时引起内存驻留、能用 Proxy 和 WeakMap 设计出零泄漏的私有数据方案——这些才是区分 JS 使用者与 JS 工程师的真正标志。
章节测试
测试1:值与引用
以下代码的输出是什么?
const a = { x: { y: 1 } };
const b = { ...a };
b.x.y = 2;
console.log(a.x.y);A. 1 B. 2 C. undefined D. 抛出错误
测试2:空值合并
以下哪个表达式返回 0? A. 0 || 10 B. 0 ?? 10 C. null ?? 10 D. undefined ?? 10
测试3:闭包
一段代码执行后,闭包引用的外层变量何时可以被垃圾回收?
测试4:this 绑定
const obj = {
value: 100,
getValue: () => this.value,
};
const { getValue } = obj;getValue() 的 this 指向什么?为什么?
测试5:原型链
Object.create(null) 创建的对象能否使用 hasOwnProperty?为什么?
测试6:Proxy
Proxy 的 handler 中,get 陷阱的三个参数分别是什么?receiver 参数在什么场景下不可或缺?
测试7:WeakMap 与 Map
为什么 WeakMap 不提供 size 属性和迭代方法?
参考答案
测试1答案
答案:B (2)
展开语法只做浅拷贝。b.x 和 a.x 指向同一个 { y: 1 } 对象,修改 b.x.y 会影响 a.x.y。需要深拷贝请使用 structuredClone(a)。
测试2答案
答案:B (0 ?? 10)
?? 只在左侧为 null 或 undefined 时使用右侧值,0 是合法值,不会被替换。|| 会把所有 falsy 值(包括 0)替换掉。
测试3答案
当没有任何闭包引用该变量所在的词法环境,且该环境不可从任何可达对象的引用链到达时,可以被 GC 回收。具体而言:所有引用该闭包的函数都已不可达,且没有循环引用导致整个作用域链被保留。在现代引擎中,如果一个闭包只捕获了环境中的部分变量,未捕获的变量通常可以被提前回收——但最安全的做法是手动将不再需要的大对象引用置为 null。
测试4答案
getValue() 中的 this 指向全局对象(浏览器中是 window,严格模式下是 undefined),而不是 obj。
原因:箭头函数没有自己的 this,它从定义位置的词法作用域捕获 this。obj 对象字面量不创建新的作用域,所以箭头函数捕获的是模块顶层或全局作用域的 this,不是 obj。
测试5答案
不能。Object.create(null) 创建的对象没有原型链(__proto__ 为 null),因此无法从 Object.prototype 继承 hasOwnProperty 方法。这在创建"纯字典"(不需要原型属性和方法)时是有意为之的行为。如果需要对这样的对象使用 hasOwnProperty,可以通过 Object.prototype.hasOwnProperty.call(obj, key) 或使用 Object.hasOwn(obj, key)(ES2022+)。
测试6答案
get 陷阱的三个参数:
- target — 被代理的目标对象
- property — 被访问的属性名(string 或 symbol)
- receiver — 最初被调用的对象(通常是 proxy 自身)
receiver 在原型链继承场景下不可或缺:当通过子对象访问从原型继承的属性时,如果该属性是 getter 且 getter 内部使用了 this,正确的 receiver 能确保 this 指向子对象而非原型对象。Proxy 实现响应式系统时,正确传递 receiver 给 Reflect.get(target, prop, receiver) 是追踪响应式依赖的前提。
测试7答案
WeakMap 不提供 size 和迭代方法,因为其键是弱引用——键所指向的对象随时可能被垃圾回收,且 GC 的发生时机是不可预测的。如果提供了 size,它的值在不同时刻可能不同(不确定的行为)。如果提供了迭代方法,迭代过程中键可能被 GC,导致不可预期的结果。这种设计确保了 WeakMap 的弱引用语义不会产生不确定的行为。
相关笔记
- [[00-overview]] - JS/TS 学习路线全景图
- [[02-typescript-type-system]] - TS 类型系统从基础到类型体操
- [[03-async-and-event-loop]] - 异步编程与事件循环深度图解
下一步学习
- [ ] 动手:写一个基于闭包的简易状态管理器(不含任何框架)
- [ ] 动手:用 WeakMap 实现一个零泄漏的事件发射器
- [ ] 动手:用 Proxy 实现一个支持嵌套对象和数组的响应式
reactive函数 - [ ] 动手:用生成器实现一个可暂停/恢复的任务调度器
- [ ] 阅读 TypeScript 类型系统 — 在理解 JS 运行时后,建立编译期类型约束
学习状态:🟡 开始学习