原型链继承
// 在父类原型上定义方法
Parent.prototype.getName = function () {
return this.name;
};
// 子类
function Child() {
this.name = "child";
}
// 实现原型链继承(关键步骤)
Child.prototype = new Parent();
// 实例化子类
const child1 = new Child();
// 测试继承方法
console.log(child1.getName()); // 输出: "child"
- 缺点:原型链继承的一个主要问题是包含引用类型值的原型属性会被所有实例共享。 换而言之,如果一个实例改变了该属性,那么其他实例的该属性也会被改变!
比如以下代码就会出问题:我明明只想修改了 child1 的arr,我不想修改 child2 的 arr。但是实际结果却是, child2 的 arr 也被修改了。
function Parent() {
this.arr = [1, 2, 3];
}
function Child() {}
Child.prototype = new Parent();
var child1 = new Child();
var child2 = new Child();
child1.arr.push(4);
// 更改child1,发现child2也改变了
console.log(child1.arr); // [1, 2, 3, 4]
console.log(child2.arr); // [1, 2, 3, 4]
上述问题能否避免呢?答案是可以的。我们可以通过构造函数继承来避免上述问题。
构造函数继承
构造函数继承,通过使用 call 或 apply 方法,我们可以在子类型构造函数中 执行父类型构造函数(这样父类的属性和方法会在子类的构造函数中重新全执行一遍,这样父类有的属性和方法,子类也就都有了),从而实现继承。比如以下例子,child1 和 child2 都继承了 Parent 的 sayHello() 方法。
// 父类
function Parent() {
this.sayHello = function () {
console.log("Hello");
};
}
Parent.prototype.a = "我是父类prototype上的属性";
// 子类的构造函数中,执行父类的构造函数
function Child() {
Parent.call(this);
}
// 创建两个 Child 实例
var child1 = new Child();
var child2 = new Child();
console.log(child1.sayHello === child2.sayHello); // 输出 false
- **优点:**原型属性不会被共享(所以不会出现上述问题)。换而言之, 修改其中任何一个 sayHello ,不会影响另一个 sayHello。
- **缺点:**它不能继承父类 prototype 上的属性。只能继承父类构造函数中的属性和方法。
// 父类
function Parent() {
this.sayHello = function () {
console.log("Hello");
};
}
Parent.prototype.a = "我是父类prototype上的属性";
// 子类
function Child() {
Parent.call(this);
}
var child1 = new Child();
var child2 = new Child();
var parentObj = new Parent();
console.log(parentObj.a); // "我是父类prototype上的属性"
console.log(child1.a); // undefined
为了解决以上缺点,我们引入了组合继承。
组合继承
组合继承可以理解为 原型链继承 + 构造函数继承 。 可以看以下例子:
// 父类
function Parent() {
this.sayHello = function () {
console.log("Hello");
};
}
Parent.prototype.a = "我是父类prototype上的属性";
// 子类
// 原型链继承 + 构造函数继承
function Child() {
Parent.call(this);
}
Child.prototype = new Parent();
var child1 = new Child();
实际上,只是在构造函数继承的基础上,加了一行代码:Child.prototype = new Parent();
这就解决了构造函数继承的缺点。 构造函数继承不能继承父类 prototype 上的属性。但是加上这行代码之后,每当我们在 child 对象上找不到属性,就会去 Child.prototype 上去寻找,而 Child.prototype 是一个 Parent 对象,如果 Parent 对象上还找不到,就会去 Parent.prototype 上去寻找。如此以来,组合继承就能继承父类 prototype 上的属性。
-
**缺点:**调用了 2 次 Parent()。 所以它会在 child 的 prototype 上添加父类的属性和方法(其实相当于是重复添加)。
var child1 = new Child();
已经给child1实例添加了一个sayHello方法。Child.prototype = new Parent();
又给Child.prototype添加了一个sayHello方法。重复,没有意义。
那么有没有办法解决这个问题呢?于是我们引入了寄生组合继承。
寄生组合继承
// 父类
function Parent() {
this.sayHello = function () {
console.log("Hello");
};
}
Parent.prototype.a = "我是父类prototype上的属性";
// 子类
function Child() {
Parent.call(this);
}
// 创建一个没有实例方法的父类实例作为子类的原型
Child.prototype = Object.create(Parent.prototype);
// 修复构造函数的指向
Child.prototype.constructor = Child;
var child1 = new Child();
我们发现,只需要在组合继承的基础上,把 Child.prototype = new Parent();
替换为 Child.prototype = Object.create(Parent.prototype);
关于Object.create(Parent.prototype) :
- 创造了一个空对象,并且指定这个空对象的__proto__ 是 Parent.prototype 。
所以它继承了 Parent 原型链上的属性和方法。由于我们删除了Child.prototype = new Parent(); 我们不再调用 Parent() 构造函数,因此 Child.prototype 不再包含 Parent 的属性和方法。所以第三小节部分提到的问题就解决了,Child.prototype 上不再有 sayHello 方法。
那么寄生组合继承有没有缺点呢?当然也有缺点。看以下例子:
// 父类
function Parent() {
this.sayHello = function () {
console.log("Hello");
};
}
Parent.prototype.a = "我是父类prototype上的属性";
// 子类
function Child() {
Parent.call(this);
}
Child.prototype.childFunction = () => {
console.log("我是child方法");
};
// 创建一个没有实例方法的父类实例作为子类的原型
Child.prototype = Object.create(Parent.prototype);
// 修复构造函数的指向
Child.prototype.constructor = Child;
var child1 = new Child();
child1.childFunction();
我们在 Child.prototype 上添加了 childFunction 。所有通过 Child() 创建的实例对象,都应该有该childFunction() 方法。但是当我们调用 child1.childFunction() 的时候却报错,这是为什么呢?因为 Child.prototype = Object.create(Parent.prototype); 这一行代码使 Child.prototype 指向了一个新对象, 原来对象上的属性和方法都会丢失。
- 优点:
- 原型属性不会被共享
- 可以继承父类的原型链上的属性和方法
- 只调用了1次Parent()。因此,它不会在Child的prototype上添加Parent的属性和方法
- 缺点:Child.prototype 的原始属性和方法会丢失。