原型式继承
原型式继承和之前的继承方式不太相同,原理上相当于对对象进行一次浅拷贝,父对象中属性如果是引用类型的值,拷贝给子对象的也仅仅是个地址,这个地址是指向同一个引用类型的,原型式继承目前可以通过Object.create()
方式来实现,有关这个方法的具体用法可以参考MDN。
Object.create()
方法接收两个参数:
- 第一个参数作为新对象的原型的对象
- 第二个参数定义为新对象增加额外属性的对象(可选)
我想你肯定直接懵逼,比如说,我们现在创建一个新对象B,那么首先要传入第一个参数对象A,这个A将被当做B的原型对象,然后可以再传入一个参数对象C,C对象中可以定义我们需要的一些额外属性,代码举例:
let A = {
name: 'A',
color: ['red', 'green']
}
let B = Object.create(A);
B.name = 'B';
B.color.push('black');
我们打印一下B看下结果:
A.name
和B.name
:其实这就是原型式继承的核心了,它的好处是:如果只是想简单的保持一个对象和另一个对象类似,直接调用就可以实现。
寄生式继承
寄生式继承类似于原型式继承,核心思路是:创建一个只负责封装继承过程的函数,在函数中根据需要增强对象,然后返回对象。
示例代码:
function create(name) {
let obj = Object(name);
obj.sayO = function () {
console.log('O');
}
return obj;
}
let A = {
name: 'A',
color: ['red', 'green', 'blue']
}
let B = create(A);
console.log(B);
B.sayO();
结果如下:这种继承的结果是B拥有A的所有属性和方法,效果和原型式继承类似。
寄生组合式继承
组合继承存在的缺陷
之前我们说过,js中最常用的就是组合继承,但是看似完美的组合继承依然有缺点:子类型会两次调用父类型的构造函数,一次是在子类型的构造函数里,另一次是在实现原型链的步骤。
看之前的代码:
function Father(name) {
this.name = name;
this.hobby = ['sport', 'read'];
}
Father.prototype.sayF = function () {
console.log('father');
}
function Son(name, age) {
Father.call(this, name);
this.age = age;
}
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.sayS = function () {
console.log('son');
}
let son1 = new Son('Jack', 20);
let son2 = new Son('Bob', 24);
console.log(son1);
console.log(son2);
son1.sayF();
son2.sayS();
第一次调用的时候,生成了Son.prototype
对象,它具有name
和hobby
属性,因为它是Father的一个实例;第二次调用是在实例化son1
和son2
的时候,这个时候son1和son2也具有name
和hobby
属性,原型链的意义就是:当对象本身不存在某个属性或方法的时候,可以沿着原型链向上查找,如果对象自身已经有某种属性或者方法,就访问自身的,现在发现,通过组合继承,只要是Father
里面原有的属性,Son.prototype
对象一定会有,son1
和son2
也一定会有,那这样其实就造成了一种浪费,Son.prototype
上的属性我们根本用不上,为了解决这个问题,我们采用寄生组合式继承。
寄生组合继承的核心思路就是换一种方式实现Son.prototype = new Father()
,从而避免两次调用父类型的构造函数。使用寄生式继承来继承父类型的原型,然后将结果指定给子类型的原型。
看例子:
function inheritPrototype(Son, Father) {
//复制一个Father的原型对象
let pro = Object(Father.prototype);
// 改写这个原型对象的constructor指针指向Son
pro.constructor = Son;
// 改写Son的prototype指针指向这个原型对象
Son.prototype = pro;
}
上面代码短短三行,却发生了很多事情:首先复制了一个Father
的原型对象,然后修改了一下这个原型对象的constructor
指向,再替换掉Son
的原型对象。这样就替换了Son.prototype = new Father();Son.prototype.constructor = Son;
我们可以想一下,我们最初为什么要把Son的prototype属性指向Father的一个实例呢?无非就是想得到Father的prototype的一个复制品,然后实现原型链。而我们现在这样的做法,同样达到了目的,而且此时Son的原型对象上不会再有Father的属性了,因为它不是Father的实例。因此,只要把上面的 Son.prototype = new Father();Son.prototype.constructor = Son;
替换成inheritPrototype(Son, Father)
,就完成了寄生组合式继承。
寄生组合式继承保持了组合继承的优点,又避开了组合继承会有无用属性的缺陷,被认为是最理想的继承方式。
ES6中类的继承
在es6中,引入了class类的概念,类可以通过extends
关键字来实现继承,相比es5,要清晰和方便很多。在子类中使用super
关键字,就可以用来访问和调用父类上的方法了,既可以调用构造函数,也可以调用普通函数。
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
console.log(this.x + this.y);
}
}
class Son extends Father {
constructor(x, y) {
super(x, y); //调用了父类中的构造函数
}
}
let son = new Son(1, 2);
son.sum();
上面代码,如果我们不加super(x, y)
这句话,结果就会报错,这是因为使用extends
继承了父类的sum()
方法,但是sum()方法中的this指向的是父类constructor
内的数据,new Son()
里面的1和2是传给了子类中的constructor
,父类并没有得到1和2这两个数值,所以才会报错。使用super(x, y)
调用了父类中的构造函数,这时,1和2首先传给子类的constructor
,然后通过调用父类构造函数,1和2又传给了父类的constructor
,现在sum()
就可以使用父类的x和y了。
super()
方法也可以调用普通函数:
class Father {
say() {
return 'father';
}
}
class Son extends Father {
say() {
console.log('son');
}
}
let son = new Son();
son.say();
这里输出结果是son,在这个过程中如果实例化子类输出一个方法,首先会查找子类有没有这个方法,如果有就先执行子类的,如果没有,再查找父类有没有,如果有就执行父类的(就近原则)。
我们将上面代码中console.log('son');
替换为console.log(super.say() + ' son');
,结果为:father son
,可以看出super()
关键字也是可以调用父类中的普通函数的。
那如果我们想要子类既要继承父类中的方法,同时还需要扩展自己的方法呢?
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
console.log(this.x + this.y);
}
}
class Son extends Father {
constructor(x, y) {
super(x, y);
this.x = x;
this.y = y;
}
subtract() {
console.log(this.x - this.y);
}
}
let son = new Son(5, 3);
son.subtract();
son.sum();
结果是2和8,这里super()
必须在子类this之前调用,否则会报错。也就是说必须先调用父类的构造方法,再使用子类的构造方法。