JavaScript · 原型链与 this

class 只是原型继承的糖纸 — 属性沿 __proto__ 一路向上找, this 由"怎么调用"当场决定

__proto__ null 链终 alice (实例) 自有属性: name, age Person.prototype sayHi() · constructor Object.prototype toString() · hasOwnProperty null — 查找到此为止 alice.sayHi(): 自有没有 → prototype 层找到 → 沿链再向上 写入永远落在对象自己身上 new Person(...) 引擎做的四件事 ① 创建空对象 obj {} — 一张白纸 ② obj.__proto__ = Person.prototype 接上原型链, 方法继承自此 ③ 构造器执行, this = obj this.name = name 落在 obj 自己身上 ④ 返回 obj 除非构造器 return 一个对象, 那以它为准 忘写 new: 普通调用 this=undefined(严格) 属性挂在 window/全局 → 污染 this 绑定四规则 (按优先级) ① new 绑定 — 优先级最高 new Person() → this 是新对象 构造器里 return 对象会顶掉 this class 里强制走这条 ② 显式绑定 call / apply / bind fn.call(ctx) → this 就是 ctx bind 返回永久绑定的函数 call 传散参, apply 传数组 ③ 隐式绑定 obj.fn() 谁点出来的, this 就是谁 赋值传递 const f = obj.fn 后调用 → 丢 this 多层 a.b.fn() 以最后一层为准 ④ 默认绑定 + 箭头函数特例 普通调用: this = undefined(严格) / window 箭头函数没有 this, 沿词法抓外层 — 定义时定型 不能被 call/bind 改, 不能做构造器 口诀: new 最大, call 次之, 点号第三, 默认兜底; 箭头函数不吃这套看外层 class Person { constructor(name) { this.name = name; } // 本质: 往新对象上挂自有属性 sayHi() { /* 方法其实挂在 Person.prototype 上, 实例共享一份 */ } } // typeof Person === 'function' · 类体自动严格模式 · 方法不可枚举 · 必须用 new 调用

原型链 = 备胎链

  • • 读属性: 自己没有就沿 __proto__ 向上
  • • 写属性永远落在对象自身(遮蔽上层)
  • • __proto__ 是实例指向, prototype 是函数持有
  • • Object.prototype 之上是 null

class 只是糖纸

  • • 方法在 prototype 上, 全实例共享一份
  • • 字段在构造器里逐实例创建
  • • extends 就是把两条链接对位置
  • • 类体自动严格模式, 必须 new 调用

this 看调用方式

  • • new > call/bind > obj.fn() > 默认
  • • 箭头函数没 this, 用定义处外层的
  • • 回调传递=赋值, 隐式 this 必丢
  • • 排查 this: 先问"它怎么被调用的"

💡 一句话理解

原型链像备胎链: 问对象要属性, 自己没有就去 __proto__ 指向的"上一级"要, 一路问到 Object.prototype, 还没有就 undefined。class 写法只是把这层关系包了层现代糖纸 — constructor 里的赋值是每个实例自己的, 方法是全实例共享的(挂在 prototype 上)。

this 则完全由调用方式决定: 用 new 调指向新对象, 用 call/bind 调指向指定者, 用 obj.fn() 调指向 obj, 光秃秃调用就是全局/undefined; 唯独箭头函数没有 this, 它像闭包一样抓定义处外层的。四条规则背下来, 90% 的 this 灵异事件当场破案。

🧠 必知必会 必考 & 必会

__proto__ vs prototype
prototype 是函数的属性(造实例时当蓝本); __proto__ 是实例的属性(指向构造器的 prototype)。
function P() {}
P.prototype.hi = () => 'hi';
const p = new P();
p.__proto__ === P.prototype;  // → true
链上查找与遮蔽
读沿链向上, 找到即停; 写只落在自身, 产生"遮蔽"(shadowing), 删掉自有属性又露出上层的。
const base = { greet: () => 'base' };
const o = Object.create(base);
o.greet();        // → 'base'  来自链上
o.greet = () => 'mine'; o.greet();  // → 'mine' 遮蔽
constructor
prototype 上自带的指回构造器的属性; 被覆盖后 instanceof 不受影响(它看链不看 constructor)。
class A {}
A.prototype.constructor === A;   // → true
new A() instanceof A;            // → true, 看原型链
new 的四步
建空对象 → 接原型链 → this 绑新对象执行 → 返回; 构造器 return 一个对象时以返回值为准。
function Fake() { return { odd: 1 }; }
new Fake();        // → { odd: 1 }, 不是 Fake 实例!
new Fake().instanceof Fake;   // → false
this 四规则
new > 显式(call/apply/bind) > 隐式(obj.fn) > 默认; 箭头函数无 this, 沿词法取外层且不可改。
const obj = { name: 'A', hi() { return this.name; } };
obj.hi();                    // → 'A'  隐式
const f = obj.hi; f();       // → undefined  丢了 this
obj.hi.call({ name: 'B' });  // → 'B'  显式赢了
bind 的持久性
bind 返回的新函数 this 永久锁定, 再 call 也改不动; 但对箭头函数 bind 无效。
const f = (function() { return this.tag; }).bind({ tag: 1 });
f.call({ tag: 2 });   // → 1, call 改不动 bind
class 是语法糖
typeof 类是 function; 方法挂在 prototype 且不可枚举; 类体自动严格模式; 不 new 直接调用抛 TypeError。
class A { m() {} }
typeof A;                      // → 'function'
Object.keys(A.prototype);       // → []  方法不可枚举
A();                            // → TypeError: cannot invoke w/o new
extends 与 super
子类原型链指向父类, 静态方法同理; constructor 里必须先 super() 才能碰 this。
class A { hi() { return 'A'; } }
class B extends A { hi() { return 'B+' + super.hi(); } }
new B().hi();   // → 'B+A'
Object.create(null)
造一个"没有原型"的纯字典: 没有 toString/hasOwnProperty, 任何 key 都不撞车, 原型污染免疫。
const dict = Object.create(null);
dict.toString;          // → undefined  干净
dict['__proto__'] = 1;  // 普通键, 无劫持风险
instanceof
沿右侧函数的 prototype 在左侧对象的原型链上找; 不跨 realm(iframe), 不认 interface。
[] instanceof Array;        // → true
[] instanceof Object;       // → true  链上都有
// 跨 iframe: Array.isArray(x) 才可靠
hasOwnProperty
区分自有属性与链上继承属性; 对象可能没有该方法(create(null)), 安全写法用静态版。
const o = Object.create({ inh: 1 }); o.own = 2;
o.hasOwnProperty('inh');                    // → false
Object.hasOwn(o, 'own');                     // → true (ES2022 静态版)
mixin 组合
把多个来源的方法混进一个 prototype/类, 绕开单继承限制; 更现代做法是组合+委托。
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

🏭 生产实战 real world

场景 1 · 原型污染漏洞: 递归 merge 被打进 __proto__

经典 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}}')) 做回归用例锁死。

场景 2 · 纯字典: 配置表用 create(null) 防撞车

以用户输入做 key 的缓存被 "constructor" 撞出 bug, 换无原型字典:

const cache = Object.create(null);
cache['constructor'] = { data: 1 };   // 普通 key, 无风险
cache['toString'] = { data: 2 };
Object.keys(cache);   // → ['constructor', 'toString']  语义正确
// 高频写场景换 Map: 任意类型键 + 天然无原型问题

场景 3 · JSON 反序列化恢复类实例

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

场景 4 · 回调丢 this: setTimeout 与事件转发

日志器方法被解构传递后 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  随便解构都不丢

场景 5 · 跨 iframe 的 instanceof 失效

微前端/多 iframe 页面里, 子页面的数组传到主页面, instanceof Array 为 false — 两个 realm 各有一套 Array 构造器:

// 错: 跨 realm 不可靠
data instanceof Array
// 对: 用静态方法做"品牌"检查
Array.isArray(data)          // 跨 realm 可靠
// 自定义类跨 realm: 检查标记属性而非 instanceof
if (obj?.$type === 'Money') reviveMoney(obj);

场景 6 · mixin 插件机制: 组合优于继承

表格组件要日志/缓存/校验三个可选能力, 单继承装不下, 用类工厂 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');

能力可插拔, 顺序即优先级; 比多层级继承扁平得多。

场景 7 · 私有字段 + 原型方法封装领域对象

金额计算不想让业务侧直接改内部分值, 用 #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 无浮点误差

场景 8 · getter/setter 做兼容层: 字段改名不破调用方

老字段 user.name 要废弃成 fullName, 用原型上的存取器平滑过渡并打点:

class User {
  #fullName = '';
  get name() {                       // 兼容旧读法
    console.warn('name 已废弃, 用 fullName');
    return this.#fullName;
  }
  set name(v) { this.#fullName = v; }
}
new User().name;   // 老代码继续跑 + 上报迁移进度

场景 9 · for...in 的原型链问题

遍历配置对象把库注入的原型方法也遍历了出来, 业务键多了 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)) {}

场景 10 · polyfill 安全注入原型

给老浏览器补 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,
  });
}

⚠️ 编码注意与常见坑 pitfalls

坑 1 · 忘写 new — 构造器被当普通函数调用, this 变 undefined(严格)或全局, 属性乱挂. 正解: class(强制 new)或开头守卫。
// 错: Person('a');            // name 挂到 window
// 对: class Person { ... }  new Person('a');
坑 2 · __proto__ 与 prototype 混用 — 一个在实例上一个在函数上, 搞反后方法全找不到. 正解: 读实例链用 Object.getPrototypeOf。
p.prototype;                     // → undefined, 实例没有
Object.getPrototypeOf(p) === P.prototype;  // → true
坑 3 · 运行时改 __proto__ — 改变已建对象的原型会打断引擎内联缓存, 热路径明显变慢. 正解: 创建时用 Object.create 定型。
// 错: o.__proto__ = otherBase;   // deopt + 慢
// 对: const o = Object.create(otherBase);
坑 4 · 原型上的可变引用共享 — 数组/对象属性挂原型上, 一个实例 push 全体可见. 正解: 可变状态放实例(构造器里初始化)。
// 错: class C {} C.prototype.tags = [];  // 全实例共享
// 对: class C { tags = []; }             // 每实例一份
坑 5 · 箭头函数做方法 — 箭头函数没有 this/prototype, 拿来做实例方法 this 指向定义处, 也无法 new. 正解: 方法用普通写法, 只有回调要绑外层 this 时才用箭头。
// 错: class C { hi = () => this.name; }  // 在字段区还行, 但每实例一份且上不了原型
// 对: class C { hi() { return this.name; } }
坑 6 · constructor 被覆盖 — 手写原型对象字面量整体替换后 constructor 指向丢失. 正解: 补 constructor: Person 或逐个 defineProperty。
P.prototype = { hi() {} };            // constructor 丢了
P.prototype.constructor = P;          // 补回
坑 7 · super 前碰 this — 子类 constructor 在 super() 之前访问 this 直接 ReferenceError. 正解: 第一行 super。
// 错: constructor(n) { this.n = n; super(); }
// 对: constructor(n) { super(); this.n = n; }
坑 8 · for...in 连原型一起遍历 — 把继承来的可枚举属性也走一遍. 正解: Object.keys 或 hasOwn 过滤。
for (const k in obj) if (Object.hasOwn(obj, k)) use(k);
坑 9 · hasOwnProperty 被覆盖/缺失 — 对象自己带了个同名属性或无原型, 方法调用出错. 正解: Object.hasOwn(o, k) 静态版。
// 错: o.hasOwnProperty(k)   // o 可能没有这个方法
// 对: Object.hasOwn(o, k)
坑 10 · delete 打断优化 — 删属性让对象退化成字典模式, 热路径变慢. 正解: 置 undefined 或用 Map。
// 错: delete obj.cache;   // shape 破坏
// 对: obj.cache = undefined;
坑 11 · Object.assign 浅拷贝 — 嵌套对象仍共享引用, 改一个动全体. 正解: 深拷贝 structuredClone。
// 错: const b = Object.assign({}, a); b.cfg.x = 1; // a.cfg 也变
// 对: const b = structuredClone(a);
坑 12 · 静态方法当实例方法调 — static 挂在类上不在原型上, 实例调不到. 正解: 类名调用。
class C { static make() {} }
C.make();        // ✓
new C().make();  // → TypeError
坑 13 · 构造器 return 对象 — 返回对象会顶掉 this, instanceof 全 false. 正解: 构造器不写 return(或只 return 原始值无效没关系)。
function F() { return {}; }
new F() instanceof F;   // → false, 陷阱
坑 14 · 原型污染 merge — 深合并外部输入让 __proto__/constructor 键改写全体原型. 正解: 键黑名单 + create(null) + 正规校验库。
JSON.parse('{"__proto__":{"x":1}}');
// merge 后所有对象读 x 都中招 — merge 必须过滤这两个键
坑 15 · getter/setter 与字段互相遮蔽 — 同名 getter 在原型、字段在实例, 赋值时 set 没定义就只建自有字段, 语义分裂. 正解: 成对声明 get/set。
// 错: 只写 get name() → 赋值 obj.name=1 变自有字段绕过逻辑
// 对: get name() {...} set name(v) {...} 成对
坑 16 · 检测数组用 instanceof — 跨 realm 失效. 正解: Array.isArray。
arr instanceof Array;   // iframe 间可能 false
Array.isArray(arr);     // 永远可靠
坑 17 · 类字段每实例创建函数 — 箭头类字段在 N 个实例上造 N 份函数, 大列表浪费. 正解: 原型方法 + bind, 或事件委托。
// 错: class Row { onClick = () => {}; } // 1000 行 1000 份
// 对: class Row { onClick() {} } + 绑定/委托
坑 18 · bind 后丢原型 — bind 返回的函数不再有原函数的 prototype, 想再 new 出原类实例不行. 正解: 别 bind 构造器; 组合用工厂函数。
const B = F.bind(null); new B() instanceof F;  // → false
坑 19 · 类体严格模式踩 this — class 方法内 this 为 undefined 时读属性直接 TypeError, 比 var 时代的 window 静默更早炸(是好事但要理解). 正解: 先确认调用方式, 必要时 bind/箭头。
class C { hi() { return this.name; } }
const { hi } = new C(); hi();   // → TypeError: this undefined
坑 20 · 依赖引擎的遍历顺序 — 整数键升序、字符串键按插入序的规则别当业务依赖跨引擎押注. 正解: 需要顺序就显式 sort。
Object.keys({ 2: 0, b: 0, 1: 0 });  // → ['1','2','b'] 整数键优先
// 业务顺序: Object.entries(x).sort(...) 显式排