🧭 前端原型与继承全景学习图解版(整合 + 深入底层原理)
1. 为什么会有"原型链"?(设计动机 & 用处)
背景(一句话) :JavaScript 在早期没有"类(class)"语法时,需要一种轻量、动态的方式让对象复用逻辑 ------ 所以用"对象指向对象"的方式实现继承(prototype-based)。
主要用处
- 方法复用:把方法放在原型上,所有实例共享,节省内存。
- 动态扩展:运行时可以给原型新增方法,所有实例自动可用。
- 实现继承:通过链式引用把"父对象"的行为借给子对象(实现"is-a"或"has-shared-behaviour")。
- 类型识别 :通过
instanceof
、isPrototypeOf
来判断关系(基于原型链)。
直观比喻:每个对象背后有一个"模板(prototype)"当做影子,访问属性时先看自己,没找到就看影子,影子的影子,依此类推。
2. 核心三角:__proto__
、prototype
、constructor
ini
function Person(name) {
this.name = name;
}
const person = new Person('Alice');
ASCII 图(概念关系):
javascript
┌────────────────────┐
│ 构造函数 Person │
│────────────────────│
│ function Person() │
│ │
│ ┌───────────────┐ │
│ │ prototype 对象│◀─────────────┐
│ └───────────────┘ │ │
└────────────────────┘ │
▲ │
│ constructor │
│ │
┌────────────────────┐ │
│ 实例对象 person │────────────┘
│────────────────────│
│ { name: "Alice" } │
│ │
│ __proto__ → Person.prototype
└────────────────────┘
重要说明
prototype
:只有函数(通常作为构造函数)有这个属性。它是用来放共享方法/属性的对象。__proto__
(规范名为内部槽[[Prototype]]
):每个对象都有,指向其原型对象。constructor
:只是prototype
上的一个普通属性,指回构造函数(可被覆盖,非"内置魔法")。
3. 原型链总览(图 + 解释)
javascript
person (实例)
├─ own: name: "Alice"
└─ __proto__ → Person.prototype
├─ sayName()
└─ __proto__ → Object.prototype
├─ toString()
├─ hasOwnProperty()
└─ __proto__ → null
要点
Object.prototype
是绝大多数对象在原型链上的最终祖先(除了Object.create(null)
创建的无原型对象)。- 访问属性时会沿这条链 逐级向上 搜索,直到找到或走到
null
。
4. 属性查找与赋值:底层行为(非常重要)
4.1 读取属性(概念化流程)
访问 obj.prop
时(简化伪逻辑):
- 如果
obj
本身有名为prop
的 own(自有)属性 (数据属性或访问器),返回它或调用其 getter(getter 中的this
指向访问者,即obj
)。 - 否则,令
proto = Object.getPrototypeOf(obj)
。若proto
为null
,返回undefined
。 - 在
proto
上重复步骤 1(即沿__proto__
向上查找)。 - 直到找到或到
null
。
ECMAScript 规范:这由内部方法
[[Get]]
/OrdinaryGet
/HasProperty
等实现。
4.2 写入属性(赋值)要小心
行为取决于是否存在 setter 以及赋值目标:
- 若在原型链上某处找到一个
set
(访问器属性),则调用该 setter,this
为赋值时的目标(receiver,通常是最初的obj
)。 - 若没有 setter,赋值总是在 当前对象本身 创建/更新一个 own 数据属性(即会遮蔽原型上的同名属性),不会修改原型上的属性。
示例(展示 setter 行为):
ini
const proto = {
get x(){ return this._x || 0; },
set x(v){ this._x = v; }
};
const obj = Object.create(proto);
obj.x = 5; // 调用了 proto 上的 setter,this 指向 obj
console.log(obj._x); // 5
4.3 property descriptor(属性描述符)
属性有两类:
- 数据属性 :
{ value, writable, enumerable, configurable }
- 访问器(getter/setter)属性 :
{ get, set, enumerable, configurable }
用 Object.getOwnPropertyDescriptor(obj, 'p')
查看。理解这些字段对调试原型问题非常关键(例如不可写/不可配置会阻止覆盖等)。
4.4 instanceof
的底层实现(伪代码)
obj instanceof Ctor
的逻辑大意是:检查 Ctor.prototype
是否出现在 obj
的原型链上(逐级比较 Object.getPrototypeOf(current)
)。可模拟:
ini
function myInstanceof(obj, Ctor) {
let proto = Object.getPrototypeOf(obj);
const prototype = Ctor.prototype;
while (proto !== null) {
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
5. 继承的五种实现(逐一看底层差别、优缺点与示例)
下面给出每种方式的实现原理图、代码与底层问题。
5.1 原型链继承(Prototype Chain)
实现:
javascript
function Parent(){ this.colors = ['red']; }
function Child(){}
Child.prototype = new Parent();
图:
ini
Child.prototype === Parent 实例
↑
child.__proto__
底层问题
Parent
构造被用于创建Child.prototype
(也就是 Parent 的实例),因此引用类型(colors
)被共享给所有 child 实例。- 无法传参给 Parent。
适用场景:很少单独使用;教学用途。
5.2 构造函数继承(Constructor Borrowing)
实现:
javascript
function Parent(name){ this.name = name; }
function Child(name){ Parent.call(this, name); }
核心:在子构造内部执行父构造,把父构造里的 own 属性复制到当前实例(this)。
优点 :每个实例都有独立属性,能传参。
缺点 :无法继承 Parent.prototype
上的方法(不能复用)。
5.3 组合继承(Combination)
实现:
javascript
function Child(name){
Parent.call(this, name); // 继承属性(实例化)
}
Child.prototype = new Parent(); // 继承方法(但会再次执行 Parent)
Child.prototype.constructor = Child;
本质 :把构造函数继承和原型链继承合并。
问题 :Parent
被调用两次(一次用于 Child.prototype = new Parent()
,另一次用于 Parent.call(this)
),导致性能或副作用(如果 Parent 初始化有开销或副作用)。
5.4 寄生组合继承(Parasitic Combination --- 最优的 ES5 方式)
实现(推荐) :
javascript
function inherit(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}
function Child(name){
Parent.call(this, name); // 只调用一次
}
inherit(Child, Parent);
图:
javascript
child.__proto__ → Child.prototype
└── __proto__ → Parent.prototype
优点:
Parent
只执行一次(在Parent.call(this)
),避免两次调用;- 原型方法复用;
- 属性独立。
结论 :这是 ES5 下最常用、最合理的继承模式;ES6 class extends
本质上以此为基础实现语义。
5.5 ES6 class
继承(现代语法糖)
scala
class Parent { ... }
class Child extends Parent {
constructor() { super(); }
}
底层关系
Child.prototype.__proto__ === Parent.prototype
(实例链)Child.__proto__ === Parent
(构造函数链,支持静态方法继承)
super()
行为:
- 在子类构造中必须先调用
super()
(它执行父构造函数并设置this
)。 - 在方法中使用
super.method()
会按[[HomeObject]]
和原型关系查找父方法并以当前实例作为this
调用。
优点:语义清晰、可读性高、同时支持私有字段、静态字段等现代特性。底层仍是原型链。
6. ES6 class
进阶细节(底层语义 & 私有字段)
6.1 class
与原型的等价关系
class
的实例方法被放到 prototype
上,静态方法直接放在构造函数上。类声明只是语法糖,背后仍是 function
+ prototype
的组合(但有更严格的内部行为,如类方法默认不可枚举、类构造函数不能被当作普通函数直接调用等)。
6.2 私有字段(#x
)
- 私有字段不是在
prototype
上,它们是按实例 存放在引擎内部私有字段表中(规范中为 PrivateFields)。外部无法通过obj['#x']
访问。 - 私有字段的初始化发生在构造执行期间(类字段初始化阶段),并遵从 temporal behavior (不能在
super()
前使用this
)。
6.3 super
在方法中的查找(简述)
- 每个类方法有内部
[[HomeObject]]
(home object)关联,用于super
时确定查找起点。 super.foo()
实际上在父类原型上查找foo
,以当前实例作为this
进行调用(即super
的 receiver 是当前实例)。
7. 优缺点汇总 & 能否"打破"原型链?
7.1 优点回顾
- 内存节约:方法放原型只存一份。
- 动态特性:运行时可扩展、更灵活。
- 自然继承:链式结构直观支持"继承"。
7.2 缺点回顾
- 查找成本:每次属性访问可能沿链多次查找(层级深性能差)。
- 调试难:属性来源可能在原型上,难以追踪。
- 共享副作用:错误地把可变数据放在原型会被所有实例共享(常见坑)。
- 变更影响范围大:改 prototype 会影响所有实例。
7.3 是否可以打破原型链?(可以,但需权衡)
方法 A:创建无原型对象
ini
const dict = Object.create(null);
→ dict
的 __proto__ === null
,没有 hasOwnProperty
、toString
等方法,适合"纯字典"。
方法 B:把某对象的原型设为 null
javascript
Object.setPrototypeOf(obj, null); // 或 obj.__proto__ = null
→ 断开继承链,但会失去默认行为且性能差(Object.setPrototypeOf
很慢,应避免在热路径中使用)。
方法 C(不是真正"打破",只是限制) :
Object.freeze(Object.prototype)
可以防止在Object.prototype
加入新属性(但不会移除已有原型链),通常不常用。
结论 :可以打破(Object.create(null)
最常用),但通常不建议随意打断原型链。现代代码更倾向用模块化、组合(composition)替代复杂的深继承。
8. 常用命令/查验速查表(代码片段)
javascript
// 获取原型
Object.getPrototypeOf(obj);
// 检查 own 属性
obj.hasOwnProperty('p');
// 检查 in(会查原型)
'p' in obj;
// instanceof
obj instanceof Constructor;
// 创建以 proto 为原型的新对象
const o = Object.create(proto);
// 设置原型(慢)
Object.setPrototypeOf(obj, proto);
// 查看 own property 描述符
Object.getOwnPropertyDescriptor(obj, 'p');
// 列出 own keys (不包含原型)
Object.getOwnPropertyNames(obj);
Object.keys(obj);
// 列出 prototype 上的方法
Object.getOwnPropertyNames(Object.getPrototypeOf(obj));
9. 调试建议与练习题
调试技巧
- 在浏览器控制台使用
console.dir(obj)
,展开查看__proto__
链。 - 使用
Object.getPrototypeOf(obj)
明确获取原型而不是__proto__
(后者为非标准但广泛支持)。 - 用
Object.getOwnPropertyDescriptors(obj)
一次查看所有 own 属性及其 descriptor。 - 使用
Reflect.get(obj, 'p', receiver)
与Reflect.set
理解 getter/setter 的receiver
行为。
练习题(推荐动手)
- 写出
myInstanceof(obj, Ctor)
的实现(见 §4)。 - 用
Object.create
实现一个继承 helper 并验证constructor
指向是否正确。 - 比较实例在以下两种写法下内存差异:方法定义在构造函数 vs 方法定义在 prototype 上(创建大量实例观察内存/性能)。
- 实现一个类
BankAccount
(私有字段)并尝试从外部访问私有字段(应报错)。
10. 总结(学习路线与建议)
- 先理解三角关系 (实例 →
__proto__
→prototype
→constructor
),再理解原型链查找(read)与赋值(write)差异。 - 优先用寄生组合继承(ES5)或
class extends
(ES6) 来实现继承。 - 把可变状态放构造函数 、把方法放 prototype/class method,避免共享引用陷阱。
- 不要频繁修改原型链 (
Object.setPrototypeOf
对性能影响大)。 - 用组合优于继承的思路(composition over inheritance)减少复杂继承层次。
附:三张 ASCII 图(可直接复制到你的 Markdown)
1) 原型链结构总览图
javascript
📦 person (实例)
│
├── name: "Alice" // own property
│
└── __proto__ → Person.prototype
├── sayName() // prototype 方法
└── __proto__ → Object.prototype
├── toString()
└── __proto__ → null
2) 继承演化路线图
scala
Prototype Chain ──> Constructor Borrowing ──> Combination
(早期) (能传参) (两次调用)
↓
Parasitic Combination (Object.create + call) ←── 推荐(ES5)
↓
ES6 class extends (语法糖,底层相同)
3) 查找机制流程图
javascript
访问 obj.prop
│
▼
obj 是否有 own prop?
├─ 是:直接返回(若为 getter,则调用 getter,this 指 obj)
└─ 否:proto = Object.getPrototypeOf(obj)
├─ proto === null → 返回 undefined
└─ 在 proto 上重复同样步骤(直到找到或到 null)