许多面向对象语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。
由于函数没有签名,在ECMAScript中无法实现接口继承,只支持实现继承,而且实现继承主要是依靠原型链来实现的。
原型链的查找规则是:当查找一个对象中的某个属性/方法时,如果对象本身没有这个属性/方法,那么会去他的内置属性__proto__中查找,即他的构造函数的原型对象prototype中查找,如果构造函数的原型中没有,就沿着构造函数原型中的__proto__属性,依次一层层往上查找。
ES5继承的6种实现方式
- 原型链
- 借用构造函数
- 组合继承
- 原型式继承
- 寄生式继承
- 组合寄生式继承
下面我们详细介绍一下这6种继承的实现。
1. 原型链
其基本思想:
利用原型链,让一个引用类型继承另一个引用类型的属性和方法。
构造函数、原型和实例的关系:
- 所有的构造函数都是函数,所有的函数都有prototype属性
- 每个原型对象,都包含一个指针constructor指向构造函数
- 所有的引用类型都有一个内置属性__proto__指向其构造函数的原型对象
⚠️ 请牢牢记住以上三条! 请牢牢记住以上三条! 请牢牢记住以上三条!
实现原型链继承的基本模式如下:
function One(x = 0) {
this.numOne = 1;
this.numX = x
}
One.prototype.getNumX = function() {
return this.numX;
}
// 原型上定义引用类型的数据,会被所有实例共享
One.prototype.numList = [1, 2, 3]
function Two(x = 0) {
this.numTwo = 2;
this.numTwoX = x;
}
// Two继承了One
// Two的原型对象prototype是One的实例,其内部指针__proto__指向One的原型对象
Two.prototype = new One(6);
Two.prototype.getNumTwo = function() {
return this.numTwo;
}
const two = new Two();
const two2 = new Two()
two.getNumX(); // 6
two2.getNumX(); // 6
console.log(two.numList) // [1 ,2, 3]
two.numList.push(4);
console.log(two.numList) // [1, 2, 3, 4]
two2.numList.push(5);
console.log(two2.numList) // [1, 2, 3, 4, 5],和two共享了numList
实例以及构造函数和原型之间的关系如图所示:
原型链继承的问题:
- 原型属性是包含引用类型的值时,会被所有实例共享。所以,一般属性在构造函数中定义,不在原型链中定义。
- 没有办法在不影响所有对象实例的情况下,给父类的构造函数传递参数。 鉴于以上两点,实践中很少会单独使用原型链。
注意事项:
- 所有的引用类型都默认继承了Object,这个继承也是通过原型链实现的,而Object的原型对象指向null,null是原型链的终极
- 在使用原型链实现继承时,不能使用字面量创建原型方法,因为这样会重写原型链。
- 给原型添加方法的代码一定要放在替换原型的语句之后。
2. 借用构造函数
其基本思想是:
在子类型构造函数的内部调用父类构造函数。通过使用apply()和call()方法在新创建的对象上执行构造函数。
基本模式:
function One(num) {
this.numOne = num;
this.numList = [1, 2];
}
// One原型中定义的方法对于Two的实例是不可见的
One.prototype.getNumOne = function () {
return this.numOne;
};
function Two() {
// 继承了One
One.call(this, 3);
this.numTwo = 2;
}
Two.prototype.getNumTwo = function () {
return this.numTwo;
};
const two = new Two();
two.numList.push(3);
console.log(two.numTwo); // 2
console.log(two.numOne); // 3
console.log(two.numList); // [1, 2, 3]
two.getNumOne(); // 报错:two.getNumOne is not a function
two.getNumTwo(); // 2
const two2 = new Two();
two2.numList.push(4);
console.log(two2.numList); // [1, 2, 4]
two2.getNumOne(); // 报错:two2.getNumOne is not a function
two2.getNumTwo(); // 2
借用构造函数实现继承的问题:
方法在构造函数中定义,无法复用。在父类型的原型中定义的方法,对子类型而言是不可见的,所有类型都只能使用构造函数模式。
所以,借用构造函数的技术也是很少单独使用的。
3. 组合继承
组合继承是将原型链和构造函数结合,发挥二者之长的一种继承模式。
其基本思想是:
使用原型链实现对原型属性和方法的继承,而通过构造函数实现对实例属性的继承。
基本模式如下:
function One(num) {
this.numOne = num;
this.numList = [1, 2];
}
One.prototype.getNumOne = function() {
return this.numOne;
};
function Two(num1, num2) {
// 第二次调用了One(),实例对象继承了One的两个属性,会屏蔽掉原型Two.prototype中的两个同名属性
One.call(this, num1);
this.numTwo = num2;
}
// 第一次调用了One(),继承了One的原型方法, Two.prototype会得到One的两个属性
Two.prototype = new One();
Two.prototype.constructor = Two;
Two.prototype.getNumTwo = function() {
return this.numTwo;
}
var two1 = new Two(3, 5);
two1.numList.push(6);
console.log(two1.numList); // [1, 2, 6]
two1.getNumOne(); // 3
two1.getNumTwo(); // 5
var two2 = new Two(4, 6);
two2.numList.push(7);
console.log(two2.numList); // [1, 2, 7]
two2.getNumOne(); // 4
two2.getNumTwo(); // 6
组合继承的特点
组合继承避免了原型链和构造函数的缺陷,融合了两者的有点,是javascript中最常用的继承模式。但是这个方式也有一个问题,那就是无论在什么情况下,都会调用两次父类型的构造函数:一次是在创建子类型原型的时候,另一次是在构造函数内部。
4. 原型式继承
其基本思想是:
必须有一个对象作为另一个对象的基础,然后把这个对象传给object函数,再根据具体需求,修改得到的对象。 object函数如下:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
可以看出,返回的对象是构造函数F的实例,其内置对象__proto__指向F的原型对象,而F.prototype = o;即返回的对象的__proto__指向传入的对象o,到这里,是不是觉得这个思路有一丝丝的似曾相识,对了,就是Object.create()。
Object.cerate()这个方法与object函数的行为相同。这个方法接收两个参数:一个是用作新对象原型的对象,和一个为新对象定义额外属性的对象,第二个参数是可选项。
基本模式:
var person = {
name: "MMZ",
course: ["Math", "English", "History"]
};
var person1 = Object.create(person);
person1.course.push('Music');
console.log(person1.name); // MMZ
console.log(person1.course); // ["Math", "English", "History", "Music"]
var person2 = Object.create(person, {
name: {
value: "MinMin"
}
});
person2.course.push('PE');
console.log(person2.name); // MinMin
// 对于引用类型的值的属性,会共享
console.log(person2.course); // ["Math", "English", "History", "Music", "PE"]
console.log(person.course); // ["Math", "English", "History", "Music", "PE"]
原型式继承的特点:
- Object.create()有兼容性问题,IE9以下不支持。
- 适用于没必要创建构造函数,只是想让一个对象和另一个对象保持类似的情况。
- 对于包含引用类型值的属性始终都会共享相应的值。
5. 寄生式继承
这种方式是与原型式继承紧密相关的一种思路。其继承的思路与寄生构造函数和工厂模式类似。
其基本思路是:
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。
示例:
function createObject(origin) {
const clone = Object.create(origin);
clone.sayHi = function() {
return 'Hi';
}
return clone;
}
const person = {
name: "MMZ",
course: ["Math", "English", "History"]
};
const person1 = createObject(person);
person1.sayHi(); // Hi
寄生式继承的问题
使用寄生式函数来对对象添加函数,无法做到函数的复用。
6. 组合寄生式继承
其基本思路是:
使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。
基本模式:
// inheritPrototype接收两个参数:子类型的构造函数和父类型的构造函数
function inheritPrototype(child, parent) {
// 创建父类型原型的副本
const prototype = Object.create(parent.prototype);
// 为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性
prototype.constructor = child;
// 将创建的副本赋值给子类型的原型
child.prototype = prototype;
}
function One(num1) {
this.numOne = num1;
this.numList = [1,2];
}
One.prototype.getNumOne = function() {
return this.numOne;
}
function Two(num1, num2) {
One.call(this, num1)
this.numTwo = num2;
}
inheritPrototype(Two, One);
Two.prototype.getNumTwo = function() {
return this.numTwo;
}
var two = new Two(3, 5);
two.numList.push(6);
console.log(two.numList); // [1, 2, 6]
two.getNumOne(); // 3
two.getNumTwo(); // 5
组合寄生式继承构造函数、原型以及实例的关系图如下:
组合寄生式的特点
组合寄生式继承,只调用了一次父类的构造函数,避免了在子类型原型对象prototype上创建不必要的、多余的属性,同时保持原型链不变,是引用类型最理想的继承范式。
ES6的继承实现方式
Class 通过extends关键字来实现继承。
示例
class One{
constructor(name, age) {
this.name = name;
this.age = age;
this.hobby = "Coding"
}
getOneInfo() {
return `姓名:${this.name},年龄:${this.age}`;
}
}
// Two这个类通过extends关键字,继承了One这个类的所有属性和方法。
class Two extends One {
constructor(x, y, gender) {
// 调用父类的constructor
super(x, y);
this.gender = gender;
}
getTwoInfo() {
// 调用父类原型上的方法 getOneInfo(),父类的属性会被子类继承
return `${super.getOneInfo()},性别:${this.gender},爱好:${this.hobby}`;
}
}
const two = new Two("MMZ", "18", "girl");
two.getTwoInfo();
// "姓名:MMZ,年龄:18,性别:girl,爱好:Coding"
⚠️ 子类必须在constructor方法中调用super方法,否则新建实例时会报错,如下所示。这是因为,子类型自己的this对象,必须先通过调用父类的构造函数来塑造,得到和父类实例同样的属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。只有super方法才能调用父类实例,不调用super(),子类就没有自己的this对象。只有调用super之后,才可以使用this关键字,否则会报错。
class One {}
class Two extends One {
// constructor中没有调用super方法
constructor() {}
}
const two = new Two();
super关键字
super这个关键字,既可以当作函数使用,也可以当作对象使用。
- super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。
class A {
constructor() {
// 返回new命令作用于的那个构造函数
console.log(new.target.name);
}
}
class B extends A {
constructor() {
// super() 调用父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,super()在这里相当于A.prototype.constructor.call(this)。
super();
}
}
// 判断B是否继承了A
Object.getPrototypeOf(B) === A; // true
// 子类的__proto__属性,总是指向父类
B.__proto__ === A; // true
// 子类prototype对象的__proto__属性,总是指向父类的prototype属性
B.prototype.__proto__ === A.prototype; // true
B.prototype.constructor === B; // true
const a = new A() // A
const b = new B() // B
// 实例的__proto__属性,指向构造函数的原型对象
a.constructor === A; // true
a.__proto__ === A.prototype; // true
b.constructor === B; // true
b.__proto__ === B.prototype; // true
作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。
class A {}
class B extends A {
m() {
super(); // 报错
}
}
- super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A {
constructor() {
this.m = 3;
}
p() {
return 2;
}
static q() {
return 7;
}
q() {
return 8;
}
}
A.prototype.s = 4;
class B extends A {
constructor() {
super();
// super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。
console.log(super.p()); // 2
console.log(super.m); // undefined,super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的
console.log(super.s); // 4,s定义在父类的原型对象上,super就可以取到。
console.log(super.q());
}
getP() {
// ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。等价于super.p.call(this)
return super.p();
}
static getQ() {
// super在静态方法之中指向父类,而不是父类的原型对象
return super.q();
}
getQ() {
// 在普通方法之中指向父类的原型对象。
return super.q();
}
}
let b = new B();
b.getP(); // 2
b.getQ(); // 8
B.getQ(); // 7
总结:ES5和ES6继承的区别
-
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的属性和方法添加到子类实例对象的this上面(Parent.call(this))。即“实例在前,继承在后”。
-
ES6 的继承机制完全不同,实质是先创建父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this实现继承。先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用
super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类。 -
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。
-
Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
- 子类的__proto__属性,表示构造函数的继承,总是指向父类。
- 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。