class 只是原型继承的糖纸 — 属性沿 __proto__ 一路向上找, this 由"怎么调用"当场决定
原型链像备胎链: 问对象要属性, 自己没有就去 __proto__ 指向的"上一级"要, 一路问到 Object.prototype, 还没有就 undefined。class 写法只是把这层关系包了层现代糖纸 — constructor 里的赋值是每个实例自己的, 方法是全实例共享的(挂在 prototype 上)。
this 则完全由调用方式决定: 用 new 调指向新对象, 用 call/bind 调指向指定者, 用 obj.fn() 调指向 obj, 光秃秃调用就是全局/undefined; 唯独箭头函数没有 this, 它像闭包一样抓定义处外层的。四条规则背下来, 90% 的 this 灵异事件当场破案。
prototype 是函数的属性(造实例时当蓝本); __proto__ 是实例的属性(指向构造器的 prototype)。 function P() {} P.prototype.hi = () => 'hi'; const p = new P(); p.__proto__ === P.prototype; // → true
const base = { greet: () => 'base' }; const o = Object.create(base); o.greet(); // → 'base' 来自链上 o.greet = () => 'mine'; o.greet(); // → 'mine' 遮蔽
class A {} A.prototype.constructor === A; // → true new A() instanceof A; // → true, 看原型链
function Fake() { return { odd: 1 }; } new Fake(); // → { odd: 1 }, 不是 Fake 实例! new Fake().instanceof Fake; // → false
const obj = { name: 'A', hi() { return this.name; } }; obj.hi(); // → 'A' 隐式 const f = obj.hi; f(); // → undefined 丢了 this obj.hi.call({ name: 'B' }); // → 'B' 显式赢了
const f = (function() { return this.tag; }).bind({ tag: 1 }); f.call({ tag: 2 }); // → 1, call 改不动 bind
class A { m() {} } typeof A; // → 'function' Object.keys(A.prototype); // → [] 方法不可枚举 A(); // → TypeError: cannot invoke w/o new
class A { hi() { return 'A'; } } class B extends A { hi() { return 'B+' + super.hi(); } } new B().hi(); // → 'B+A'
const dict = Object.create(null); dict.toString; // → undefined 干净 dict['__proto__'] = 1; // 普通键, 无劫持风险
[] instanceof Array; // → true [] instanceof Object; // → true 链上都有 // 跨 iframe: Array.isArray(x) 才可靠
const o = Object.create({ inh: 1 }); o.own = 2; o.hasOwnProperty('inh'); // → false Object.hasOwn(o, 'own'); // → true (ES2022 静态版)
const Log = C => class extends C { log(...a) { console.log('[' + this.name + ']', ...a); } }; class Base { name = 'X'; } class S extends Log(Base) {} new S().log('hi'); // → [X] hi
经典 CVE 模式: 深合并用户输入, 攻击者传 {"__proto__": {"isAdmin": true}} 改掉所有对象的原型:
function safeMerge(target, src) { for (const k of Object.keys(src)) { // 关键: 黑名单 + 用 create(null) 承载合并结果 if (k === '__proto__' || k === 'constructor') continue; if (isPlainObject(src[k])) target[k] = safeMerge(target[k] ?? {}, src[k]); else target[k] = src[k]; } return target; } // 或直接用无原型对象接收外部输入: Object.create(null)
上线前用 safeMerge({}, JSON.parse('{"__proto__":{"x":1}}')) 做回归用例锁死。
以用户输入做 key 的缓存被 "constructor" 撞出 bug, 换无原型字典:
const cache = Object.create(null); cache['constructor'] = { data: 1 }; // 普通 key, 无风险 cache['toString'] = { data: 2 }; Object.keys(cache); // → ['constructor', 'toString'] 语义正确 // 高频写场景换 Map: 任意类型键 + 天然无原型问题
JSON.parse 出来的对象没有方法(原型是 Object), 直接调 user.sayHi() 报错。用原型重建:
const raw = JSON.parse('{"name":"alice","age":18}'); // 错: raw.sayHi() → TypeError, raw.__proto__ 是 Object.prototype // 对: 借构造器接上原型链 const user = Object.assign(new User(), raw); user.sayHi(); // 方法可用 user instanceof User; // → true
日志器方法被解构传递后 this 全丢。绑定上下文是基建级修复:
class Logger { constructor(prefix) { this.prefix = prefix; this.info = this.info.bind(this); // 关键: 构造时统一绑定 } info(msg) { console.log(`[${this.prefix}] ${msg}`); } } const { info } = new Logger('api'); info('boot'); // → [api] boot 随便解构都不丢
微前端/多 iframe 页面里, 子页面的数组传到主页面, instanceof Array 为 false — 两个 realm 各有一套 Array 构造器:
// 错: 跨 realm 不可靠 data instanceof Array // 对: 用静态方法做"品牌"检查 Array.isArray(data) // 跨 realm 可靠 // 自定义类跨 realm: 检查标记属性而非 instanceof if (obj?.$type === 'Money') reviveMoney(obj);
表格组件要日志/缓存/校验三个可选能力, 单继承装不下, 用类工厂 mixin 按需叠加:
const WithLog = C => class extends C { log(...a) { console.log(`[${this.id}]`, ...a); } }; const WithCache = C => class extends C { #c = new Map(); memo(k, fn) { if (!this.#c.has(k)) this.#c.set(k, fn()); return this.#c.get(k); } }; class Table { id = 'tbl'; } class SmartTable extends WithCache(WithLog(Table)) {} new SmartTable().log('ready');
能力可插拔, 顺序即优先级; 比多层级继承扁平得多。
金额计算不想让业务侧直接改内部分值, 用 #field 锁死写入路径:
class Money { #cents; // 真私有, 外部无法碰 constructor(yuan) { this.#cents = Math.round(yuan * 100); } add(other) { return new Money((this.#cents + other.#cents) / 100); } get yuan() { return this.#cents / 100; } valueOf() { return this.yuan; } // 参与算术/比较时的转换钩子 } new Money(0.1).add(new Money(0.2)).yuan; // → 0.3 无浮点误差
老字段 user.name 要废弃成 fullName, 用原型上的存取器平滑过渡并打点:
class User { #fullName = ''; get name() { // 兼容旧读法 console.warn('name 已废弃, 用 fullName'); return this.#fullName; } set name(v) { this.#fullName = v; } } new User().name; // 老代码继续跑 + 上报迁移进度
遍历配置对象把库注入的原型方法也遍历了出来, 业务键多了 toString 一项:
const cfg = { a: 1, b: 2 }; Object.prototype.extra = 1; // 假设被某个老库污染了 // 错: for (const k in cfg) → k 含 'extra' // 对 1: 只遍历自有可枚举 for (const k of Object.keys(cfg)) {} // → a b // 对 2: 保留 for...in 时过滤 for (const k in cfg) if (Object.hasOwn(cfg, k)) {}
给老浏览器补 Array.prototype.at, 必须特性检测 + 用 defineProperty 设为不可枚举, 避免重复注入与污染 for...in:
if (!Array.prototype.at) { // 关键: 先检测再注入 Object.defineProperty(Array.prototype, 'at', { value: function (i) { i = Math.trunc(i) || 0; if (i < 0) i += this.length; if (i < 0 || i >= this.length) return undefined; return this[i]; }, writable: true, configurable: true, enumerable: false, }); }
// 错: Person('a'); // name 挂到 window // 对: class Person { ... } new Person('a');
Object.getPrototypeOf。 p.prototype; // → undefined, 实例没有 Object.getPrototypeOf(p) === P.prototype; // → true
Object.create 定型。 // 错: o.__proto__ = otherBase; // deopt + 慢 // 对: const o = Object.create(otherBase);
// 错: class C {} C.prototype.tags = []; // 全实例共享 // 对: class C { tags = []; } // 每实例一份
// 错: class C { hi = () => this.name; } // 在字段区还行, 但每实例一份且上不了原型 // 对: class C { hi() { return this.name; } }
constructor: Person 或逐个 defineProperty。 P.prototype = { hi() {} }; // constructor 丢了
P.prototype.constructor = P; // 补回// 错: constructor(n) { this.n = n; super(); } // 对: constructor(n) { super(); this.n = n; }
Object.keys 或 hasOwn 过滤。 for (const k in obj) if (Object.hasOwn(obj, k)) use(k);
Object.hasOwn(o, k) 静态版。 // 错: o.hasOwnProperty(k) // o 可能没有这个方法 // 对: Object.hasOwn(o, k)
undefined 或用 Map。 // 错: delete obj.cache; // shape 破坏 // 对: obj.cache = undefined;
structuredClone。 // 错: const b = Object.assign({}, a); b.cfg.x = 1; // a.cfg 也变 // 对: const b = structuredClone(a);
class C { static make() {} } C.make(); // ✓ new C().make(); // → TypeError
function F() { return {}; } new F() instanceof F; // → false, 陷阱
JSON.parse('{"__proto__":{"x":1}}'); // merge 后所有对象读 x 都中招 — merge 必须过滤这两个键
// 错: 只写 get name() → 赋值 obj.name=1 变自有字段绕过逻辑 // 对: get name() {...} set name(v) {...} 成对
Array.isArray。 arr instanceof Array; // iframe 间可能 false Array.isArray(arr); // 永远可靠
// 错: class Row { onClick = () => {}; } // 1000 行 1000 份 // 对: class Row { onClick() {} } + 绑定/委托
const B = F.bind(null); new B() instanceof F; // → false
class C { hi() { return this.name; } } const { hi } = new C(); hi(); // → TypeError: this undefined
sort。 Object.keys({ 2: 0, b: 0, 1: 0 }); // → ['1','2','b'] 整数键优先
// 业务顺序: Object.entries(x).sort(...) 显式排