原型与原型链基础
在学习之前需要回顾一下这些基础知识
prototype是所有函数 都包含的一个属性(对象),而对于内置构造函数通常在上面预定义了部分方法,例如:.push、.toString等。__proto__是所有 JS 对象都有的一个内部属性,指向该对象的原型对象(即父对象的prototype)。constructor每个prototype对象 都有一个默认的constructor属性,指回其构造函数。
不妨来看个例子:
js
// 构造函数
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
console.log(alice.__proto__) // Person.prototype
console.log(Person.prototype.constructor) // Person
而它的原型链就是:
bash
// --> 代表.__proto__属性
alice --> Person.prototype --> Object.prototype --> null(所有原型链的终点都是 null)
四种原型继承方式详解
1. 直接赋值父类原型(不推荐)
先来看一个例子:
js
// 父类构造函数
function Animal(name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.species = "动物";
function Cat(color, name, age) {
// 继承父类的属性
Animal.apply(this, [name, age]);
// 使用 .call 也可以
// Animal.call(this, name, age);
this.color = color;
}
Cat.prototype = Animal.prototype; // 指向父类原型
补充一下
call和apply的区别:
call是 逐个传参 ,即:fn.call(this, arg1, arg2, ...)apply是 数组传参 ,即:fn.apply(this, [arg1, arg2, ...])
这样做下来感觉并没有什么不合适,继承了父类的属性,同时也指向了父类的原型对象。但是这样并不完整,因为如果调用子类的prototype上的constructor属性,正确的继承应该是指向子类自身。
而当我们在代码中执行console.log(Cat.prototype.constructor)最后得到的结果却是 Animal

所以在最后还需要手动修复构造函数指向,即添加:
js
Cat.prototype.constructor = Cat;
但是这样做并非万无一失,在这里我们需要了解 JS 的一个特性,那就是 引用式赋值 。在 JS 中,基本数据类型(8种)是按值赋值 的,而对象类型是按引用赋值的
引用式赋值:指当我将一个对象赋值给另一个变量时,并不是复制了这个对象本身,而是复制了对象在内存中的地址引用,这样就导致两个变量都指向同一个内存位置,不论修改哪个都会对另一个造成影响。
举个最简单的例子:
js
let obj1 = { name: 'Alice' };
let obj2 = obj1; // 引用式赋值
obj2.name = 'Bob';
console.log(obj1.name); // "Bob"
console.log(obj1 === obj2); // true(指向同一对象)
回到我们的继承函数,里面就有一个是引用式赋值
js
Cat.prototype = Animal.prototype;
这就导致了当我们在Cat.prototype上添加方法还是什么的,会污染Animal.prototype,所以尽量别使用直接赋值父类原型
2. 原型链继承(有点缺点)
我们将上面的例子拿下来
js
function Animal(name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.species = "动物";
function Cat(color, name, age) {
Animal.apply(this, [name, age]);
this.color = color;
}
但是我们这里使用原型链式继承
js
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修复构造函数指向
而这里需要了解一下 new 的伪代码了
js
// 伪代码 new Animal()
let obj = {};
Animal.call(obj); // 也可以用 apply
obj.__proto__ = Animal.prototype;
return obj
首先创建一个空对象,再将父类的this指向空对象,并将空对象的__proto__指向父类的prototype,也就是连上原型链,最后再返还这个空对象。
但是需要注意的是,这里后续创建的所有实例都是共享父类的属性的,在任意一个实例中对父类属性进行修改都会对其他实例造成影响,例如:
js
function Animal(name) {
this.name = name;
this.colors = ['red', 'blue']; // 引用类型属性
}
function Cat() {}
Cat.prototype = new Animal(); // 所有 Cat 实例共享 colors
Cat.prototype.constructor = Cat;
const cat1 = new Cat();
const cat2 = new Cat();
cat1.colors.push('green');
console.log(cat2.colors); // ['red', 'blue', 'green'] 共享引用
3. 空对象中介模式(经典解决方案)
在直接赋值中,不论怎样都会对父类造成影响,那么如果我们在 父类和子类 中间找一个中介来隔断,是不是就能解决这个问题,而这也是我们最经典的解决方法----空对象中介模式
依旧将前面的例子拿来:
js
function Animal(name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.species = "动物";
function Cat(color, name, age) {
Animal.apply(this, [name, age]);
this.color = color;
}
不妨来看看中介模式是怎么使用的
js
var F = function() {}; // 空对象中介
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
其中我们将F.prototype直接继承Animal.prototype,虽然会导致 引用式赋值,但是只要我对Cat.prototype修改不对F造成影响,那么间接对Animal就没有影响。
而最精妙一点就是Cat.prototype = new F();这步,我们根据之前的伪代码可以知道,这步是将Cat.prototype.__proto__ = F.prototype,也就在变相变成Cat.prototype.__proto__ = Animal.prototype
那么即使我们对Cat.prototype本身进行重新赋值,或者添加任何其他属性也不会影响Cat.prototype.__proto__,除非我们显示修改它(或者对修改F.prototype)
拓展:
当然我们也可以将其写成继承函数(extend),这也算手写题吧 QwQ
js
function extend(Parent, Child) {
// 中介函数
var F = function() {}; // 函数表达式(有内存开销,但是因为是空函数问题不大)
// 指向父类原型
F.prototype = Parent.prototype;
// 指向空对象实例
Child.prototype = new F(); // 实例的修改不会影响原型对象
// 修复构造函数指向
Child.prototype.constructor = Child;
}
extend(Animal, Cat)
4. Object.create()(ES5 推荐方式)
js
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
以 Animal.prototype 为原型创建新对象,在不污染父类构造函数的前提下,更安全地建立子类到父类的原型链连接,并且更加适配现代继承写法。
总结
| 继承方式 | 是否推荐 | 说明 |
|---|---|---|
| 直接赋值原型 | ❌ | 污染父类 constructor |
| 原型链继承 | ❌ | 引用属性共享问题 |
| 中介函数 | ✅(兼容旧环境) | 安全隔离 |
Object.create() |
✅✅ | 现代标准,语义清晰 |
最佳实践:
- 属性继承 → 用
Parent.call(this, ...args) - 方法继承 → 用
Child.prototype = Object.create(Parent.prototype) - 修复 constructor → 显式设置
Child.prototype.constructor = Child
原型继承是 JS 的灵魂。理解 call/apply ,掌握 Object.create 如何安全构建原型链,了解其他构建方法有何不妥,为我们写出健壮的继承结构添一把力