学习笔记
主题:Object 迭代方法 / Iterator 惰性求值 / Symbol 完整知识体系
一、Object.entries / Object.fromEntries
Object.entries(obj) — 把对象展开为 [key, value] 二元数组:
Object.entries({ a: 1, b: 2 }) // → [["a",1], ["b",2]]
Object.fromEntries(entries) — 把 [key, value] 数组还原为对象:
Object.fromEntries([["a",1], ["b",2]]) // → { a: 1, b: 2 }
常用组合模式:
const obj = { name: "Alice", age: 30, secret: "hidden" };
// 过滤字段
Object.fromEntries(Object.entries(obj).filter(([k]) => k !== "secret"))
// → { name: "Alice", age: 30 }
// 转换值
Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, String(v).toUpperCase()]))
// → { name: "ALICE", age: "30", secret: "HIDDEN" }
// Map ↔ Object 互转
const map = new Map(Object.entries(obj)) // Object → Map
Object.fromEntries(map) // Map → Object,结果等同于 obj
key in obj vs obj[key] !== undefined:前者能正确识别值为 undefined / null / 0 / false 的字段,后者不能。
二、为什么 Map 有 map.keys() 而普通对象只有 Object.keys(obj)
Map 可以直接调实例方法:
const map = new Map([['name', 'Alice'], ['age', 30]]);
map.keys() // → MapIterator { 'name', 'age' }
map.values() // → MapIterator { 'Alice', 30 }
map.entries() // → MapIterator { ['name','Alice'], ['age',30] }
普通对象不行,只能用静态方法:
const obj = { name: "Alice", age: 30 };
Object.keys(obj) // → ["name", "age"]
Object.values(obj) // → ["Alice", 30]
Object.entries(obj) // → [["name","Alice"], ["age",30]]
原因:普通对象的 key 是任意的,挂实例方法会有命名冲突风险。
如果 JS 给普通对象也挂上 keys 实例方法,那么:
const obj = { name: "Alice", keys: "我自己的字段" };
// ↑ 用户自己定义了叫 keys 的字段
// 就会把内置的 keys 方法覆盖掉
Map 不存在这个问题,因为 Map 的数据存在内部插槽里,不直接暴露为对象属性,实例方法(keys、values、entries)不会被用户数据覆盖。
所以设计上:普通对象用静态方法隔离,Map 可以安全地用实例方法。
三、Array vs Iterator 的区别
const map = new Map([['name', 'Alice'], ['age', 30]]);
const iter = map.keys(); // iter 是迭代器,不是数组
iter.length // undefined,迭代器没有 length
iter.filter // 有(Iterator Helpers,Chrome 117+)
iter.map // 有(Iterator Helpers,Chrome 117+)
const arr = ['a', 'b', 'c'];
arr.length // 3
arr.filter // 有(Array.prototype 上)
arr.map // 有(Array.prototype 上)
iter.filter / iter.map 挂在 Iterator.prototype 上,不在 Array.prototype,两者是不同的类型。
四、Iterator 的惰性(Lazy Evaluation)
.map() / .filter() 调用时不执行回调,只返回一个新的迭代器("注册行为"):
const arr = [1, 2, 3, 4, 5];
const iter = arr.values(); // 从数组拿到迭代器
iter.map(x => x * 2) // 什么都没算,只是说"取值时要 ×2",回调没有执行
必须"消费"才会触发执行:
const iter = [1, 2, 3, 4, 5].values();
for (const val of iter.map(x => x * 2)) { } // 消费时才逐个计算:2, 4, 6, 8, 10
[...[1,2,3].values().map(x => x * 2)] // 展开运算符触发:[2, 4, 6]
[1,2,3].values().map(x => x * 2).toArray() // Iterator Helpers 提供:[2, 4, 6]
惰性的价值:链式操作只遍历一次;可以提前中止(.take(3)),节省大量计算:
// 假设有 100 万条数据的迭代器
const hugeIterator = getMillion().values();
hugeIterator.map(fn).filter(cond).take(3).toArray()
// 找到 3 个就停,不处理剩余的 999997 个
// 对比数组写法:先 map 全部 100万条,再 filter 全部,再取前 3 个——浪费
迭代器是"管道":lazyResult 不存自己的数据,内部引用源 iter。消费 lazyResult 时,内部驱动 iter.next(),拿到原始值再变换。管道用完,源也就耗尽了。
const iter = [1, 2, 3].values();
const lazyResult = iter.map(x => x * 2); // 管道,指向 iter
for (const val of lazyResult) {
console.log(val); // 2, 4, 6
}
// lazyResult 消费完 → iter 也耗尽了
console.log([...iter]); // [],iter 已经没有值了
五、没有 Iterator Helpers 之前如何定义迭代器行为
方法一:手动实现迭代器协议
function createRange(start, end) {
let current = start;
return {
[Symbol.iterator]() { return this; },
next() {
if (current <= end) return { value: current++, done: false };
return { value: undefined, done: true };
}
};
}
方法二:Generator 函数(最主流)
function* range(start, end) {
for (let i = start; i <= end; i++) yield i;
}
// 链式操作要手写嵌套 generator,很丑陋
方法三:转数组(最简单,失去惰性)
function* range(start, end) {
for (let i = start; i <= end; i++) yield i;
}
const iter = range(1, 10);
// 先展开成数组,再用数组方法——简单但失去惰性,会全量计算
[...iter].map(x => x * 2).filter(x => x > 4)
// → [6, 8, 10, 12, 14, 16, 18, 20]
六、[Symbol.iterator]() { return this; } 的作用
JS 有两个迭代协议:
- 可迭代协议:对象有
[Symbol.iterator]()方法 → 能被for...of消费 - 迭代器协议:对象有
next()方法 → 能驱动迭代
[Symbol.iterator]() { return this; } 让对象同时满足两个协议——自己既是可迭代对象,也是自己的迭代器。少了这行,for...of 找不到入口会报 is not iterable。
// createRange 同时满足两个协议,可以直接被 for...of 消费
const range = createRange(1, 5);
for (const n of range) {
console.log(n); // 1, 2, 3, 4, 5
}
// 也可以展开
console.log([...createRange(1, 3)]); // [1, 2, 3]
// 如果去掉 [Symbol.iterator]() { return this; } 这行:
const badRange = { next() { ... } }; // 只有 next,没有 [Symbol.iterator]
for (const n of badRange) { } // TypeError: badRange is not iterable
七、Symbol 是什么
Symbol 是唯一标识符类型。引擎内部为每个 Symbol 分配一个不可见的唯一整数 ID,JS 层面无法读取这个 ID:
Symbol('foo') === Symbol('foo') // false,每次都是新 ID
typeof Symbol('foo') // "symbol"
Symbol('foo').description // "foo"(只能拿描述标签)
不会自动转为字符串,隐式转换报 TypeError,必须显式调用 .toString() 或 .description:
const s = Symbol("id");
// 隐式转换全部报错
console.log("前缀:" + s); // TypeError: Cannot convert a Symbol value to a string
`值是:${s}`; // TypeError: Cannot convert a Symbol value to a string
s + ""; // TypeError
// 必须显式转
s.toString() // "Symbol(id)" ✅
s.description // "id" ✅
这是故意设计成不允许隐式转换的——防止不小心把 Symbol 当字符串用,掩盖 bug。
八、Symbol.iterator 是什么
Symbol.iterator 是 JS 引擎启动时预置的内置 Symbol(Well-Known Symbol),作为静态属性挂在 Symbol 上:
typeof Symbol.iterator // "symbol"
Symbol.iterator === Symbol.iterator // true,永远是同一个值
它是只读的,不可被覆盖:
Object.getOwnPropertyDescriptor(Symbol, 'iterator')
// { writable: false, configurable: false, ... }
Symbol.iterator = Symbol("anything") // 严格模式 TypeError,非严格模式静默失败
九、为什么用 obj[Symbol.iterator] 而不是 obj.iterator
Symbol 的不可见性和唯一性让它天然适合作为"协议暗语":
- 不会和对象的普通字符串属性冲突
Symbol.iterator永远不变,消费方和定义方用的是同一把"钥匙"
完整链条:
Symbol.iterator 只读、永远不变
↓
定义:obj[Symbol.iterator] = fn ← 用它当 key 挂上去
↓
消费:obj[Symbol.iterator]() ← 用同一个 key 取出来
↓
for...of / 展开运算符 / 解构 内部自动做这件事
真实代码示例:
const range = {
from: 1,
to: 3,
[Symbol.iterator]() { // 定义:用 Symbol.iterator 这个 key 挂上去
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) return { value: current++, done: false };
return { value: undefined, done: true };
}
};
}
};
// 消费时,for...of 内部做的是:
// 1. 调用 range[Symbol.iterator](),拿到迭代器
// 2. 反复调用迭代器的 next(),直到 done: true
for (const n of range) {
console.log(n); // 1, 2, 3
}
console.log([...range]); // [1, 2, 3]
十、全局 Symbol 注册表
Symbol.for(key) — 查表,有则复用,无则创建:
Symbol.for("id") === Symbol.for("id") // true,同一个实例
Symbol("id") === Symbol("id") // false,每次新建
跨模块共享:不同模块用相同 key 调 Symbol.for(),拿到的是同一个 Symbol。
Symbol.keyFor(sym) — 反向查找注册表里的 key:
const s = Symbol.for("user-id");
Symbol.keyFor(s) // "user-id"
const local = Symbol("user-id");
Symbol.keyFor(local) // undefined,因为 Symbol() 不走注册表
十一、Symbol.iterator vs Symbol.for 的区别
十二、Object.getPrototypeOf — 获取对象的原型
Object.getPrototypeOf(obj) // 返回 obj 的原型,即原型链的上一层
等价于老写法 obj.__proto__,但 __proto__ 是非标准属性,推荐用 Object.getPrototypeOf。
基础示例:
Object.getPrototypeOf([1, 2, 3]) === Array.prototype // true
Object.getPrototypeOf(new Map()) === Map.prototype // true
Object.getPrototypeOf({}) === Object.prototype // true
Object.getPrototypeOf(Object.prototype) // null(原型链终点)
原型链结构:
const map = new Map();
// map 实例
// ↓ getPrototypeOf
// Map.prototype(keys、values、get、set...)
// ↓ getPrototypeOf
// Object.prototype(toString、hasOwnProperty...)
// ↓ getPrototypeOf
// null(终点)
Object.getPrototypeOf(map) // Map.prototype
Object.getPrototypeOf(Object.getPrototypeOf(map)) // Object.prototype
实际用途 — 跳过被遮蔽的自身属性,直接从原型上取方法:
const map = new Map([['name', 'Alice']]);
map.keys = 333; // 自身属性遮蔽了原型上的 keys 方法
map.keys() // TypeError: map.keys is not a function
// 绕过自身属性,从原型直接取,用 .call(map) 手动绑定 this
Map.prototype.keys.call(map) // → MapIterator { 'name' } ✅
Object.getPrototypeOf(map).keys.call(map) // → MapIterator { 'name' } ✅ 同效果
与 instanceof 的关系:instanceof 内部就是沿原型链逐层用 getPrototypeOf 检查:
map instanceof Map
// 等价于:Object.getPrototypeOf(map) === Map.prototype → true