原型与原型链:从 proto 到 class 继承,一次讲透对象关系的本质
## 一、先记住三者的关系
每个函数都有 `prototype`,每个对象都有 `__proto__`,对象默认还有个 `constructor` 指回构造函数:
```js
function Person(name) {
this.name = name;
}
const tom = new Person('Tom');
```
三者的指向规则:
- `Person.prototype` → 原型对象(`{ constructor: Person }`)
- `tom.__proto__` === `Person.prototype`(实例的隐式原型指向构造函数的显式原型)
- `Person.prototype.constructor` === `Person`
- 原型对象本身也是普通对象,它的 `__proto__` 指向 `Object.prototype`
- `Object.prototype.__proto__` === `null`,链条到此终止
判定原型用 `Object.getPrototypeOf(obj)`,`__proto__` 只是它的访问器形式,二者等价。
## 二、属性查找沿原型链向上搜索
读一个属性时,引擎先在**对象自身**找,找不到就顺着 `__proto__` 一层层往上,直到 `null`:
```js
tom.toString(); // tom 自身没有 → Person.prototype 没有 → Object.prototype 有 → 用之
```
这条链就是"原型链"。两个直接推论:
1. **原型上的引用类型属性会被所有实例共享**——在 `Person.prototype.list = []` 上 push,所有实例的 list 都变。数组、对象这类值必须放构造函数里用 `this` 创建。
2. **写入不会触发原型链**:给 `tom.name = 'x'` 是给自身加属性,读才查链。想让修改生效到原型上,必须显式 `tom.__proto__.name = 'x'`。
## 三、继承的四种写法演进
### 1. 原型链继承(有缺陷)
```js
function Child() {}
Child.prototype = new Parent(); // 实例共享父类引用属性,且传不了参
```
### 2. 构造函数借用(call)
```js
function Child(name) {
Parent.call(this, name); // 拿到实例属性,但拿不到原型方法
}
```
### 3. 组合继承(经典)
```js
function Child(name) {
Parent.call(this, name);
}
Child.prototype = new Parent(); // 重复调用一次,浪费但不影响正确性
```
### 4. 寄生组合继承(最优解)
```js
function Child(name) {
Parent.call(this, name);
}
Child.prototype = Object.create(Parent.prototype); // 只借原型,不调用父构造函数
Child.prototype.constructor = Child;
```
`Object.create(proto)` 直接以指定对象为原型创建新对象,避免了组合继承那次多余的父类调用。**class extends 编译后就是这个模式**。
## 四、class 是语法糖,但有两个细节不同
```js
class Child extends Parent {
constructor(name) {
super(name); // 必须先调 super 才能用 this
}
}
```
编译成 ES5 后核心就是寄生组合继承,但有两个行为差异要知道:
1. **class 的方法不可枚举**:挂在 prototype 上但 `enumerable: false`,而 `Person.prototype.say = fn` 是可枚举的,`for...in` 能遍历到。
2. **class 必须 new 调用**:直接 `Person('x')` 会抛 TypeError,ES5 函数不会。
## 五、两个高频误区
1. **instanceof 的判断逻辑**:`a instanceof B` 查的是 `B.prototype` 是否出现在 `a` 的原型链上,不是"看构造函数名"。所以 `[] instanceof Object` 是 `true`,跨 iframe 的对象判断还会失效。要精确判断数组用 `Array.isArray`。
2. **Object.create(null) 创建的对象没有原型**:`__proto__` 是 null,`toString` 都不存在,但它是纯字典对象,`for...in` 干净无继承属性,适合做映射表(Vue 源码里大量使用)。
## 六、手写 new(检验掌握程度)
```js
function myNew(constructor, ...args) {
const obj = Object.create(constructor.prototype);
const result = constructor.apply(obj, args);
return result instanceof Object ? result : obj;
}
```
三步:以原型创建对象、绑定 this 执行、构造函数返回对象则用返回值,否则用新对象。理解了这三步,原型链的核心就通了。
## 七、总结
原型关系的记忆锚点就一句:**实例.__proto__ === 构造函数.prototype,链条终点是 Object.prototype.__proto__ === null**。读属性沿链向上,写属性落在自身;继承的最优解是寄生组合,class 只是它的语法糖。抓住这条主线,所有原型题都能自己推演。
- 点赞
- 收藏
- 关注作者
评论(0)