最近对JS继承中的几种方式复习了下,在这里发表一下:
首先我们先定义一下父类的构造函数
function Person(name) {
this.name = name;
}
然后我们在父类的原型上添加一个printName函数
Person.prototype.printName = function () {
return "I am a person";
};
下面开始我们的表演:
一、原型继承
原型继承,顾名思义就是通过原型链来让子类继承父类的属性以及函数,代码如下:
function Student(number){
this.number = number;
}
Student.prototype = new Person('grey');
let s1 = new Student(111);
console.log(s1.name);
console.log(s1.printName());
//grey
//I am a person
我们先是创建了父类Person的一个实例,然后让子类Student的原型指向了父类的实例。
接着我们创建一个子类的实例,输出父类的name与父类原型上的printName函数,发现子类能够获取。
这个时候原型链上是什么情况呢?我们来看一下:
通过上面这张原型关系图,我们来分析一下原型继承的三个缺点:
(1)constructor指向错误
我们尝试输出一下s1的constructor,看看是什么:
console.log(s1.constructor);
//[Function: Person]
我们看到,子类的构造函数变成了父类Person了,这是因为我们在找子类的constructor时,通过原型链直到Person.prototype才找到constructor属性,而这时候的constructor正是父类Person自己的构造函数。
解决办法:
解决办法很简单,既然constructor在子类的原型中找不到,我们就在子类的原型中直接加上去就好了。
Student.prototype = new Person('grey');
Student.prototype.constructor = Student;
改完后原型关系如下,可以看到现在子类中的构造函数constructor已经指向自己了。
(2)父类属性共享
这个很好理解,因为子类Student的原型是一个Person实例,如果我们创建多个子类对象,那么一个子类对象设置了name属性,另一个子类对象也设置了name属性,那么后一个子类对象的更改就会覆盖掉前一个子类对象的更改,这样就造成了污染。
let s1 = new Student(111)
let s2 = new Student(222)
console.log(s1.name) // grey
s2.__proto__.name = "jack"
console.log(s1.name); // jack
(3)父类属性固定
由于我们是将子类的原型指向了父类的一个实例,也就是说父类中的属性在这个时候已经被初始化了。
就像例子中我们用了new Person(‘grey’)来作为子类的原型,在这个时候已经将父类的name属性固定为‘grey’了,这明显是不合理的。
二、构造函数继承
构造函数继承的意思就是说,我们在子类的构造函数中也执行父类的构造函数,将父类构造函数中的属性继承给子类,代码如下:
function Student(name, number) {
Person.call(this, name);
this.number = number;
}
这里我们在子类Student的构造函数中使用了call,将父类构造函数中的this指向改成子类构造函数中的this,并且执行父类的构造函数。这样一来的话,父类构造函数中声明的变量就会被子类给继承,如下:
let s1 = new Student("grey", 111);
console.log(s1.name); // grey
可以看到,这个时候name已经成为s1自己的属性,不会被别的子类实例污染。
我们也来分析一下构造函数的缺点:
(1)父类原型上的函数没有继承
我们尝试打印一下s1的printName:
console.log(s1.printName); // undefined
可以看到s1上面并没有printName这个函数。其实这是显而易见的,毕竟我们只是单纯的在子类的构造函数中执行了一下父类的构造函数,简单地改变了下其中的this指向从而添加了父类的属性,根本都没接触到父类的原型,自然就没有父类原型上的方法了。
三、组合继承
组合继承其实就是将我们前面提到的原型继承与构造函数继承给结合了起来,这样能够同时解决原型继承与构造函数继承的缺点,代码如下:
function Student(name, number) {
Person.call(this, name);
this.number = number;
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
let s1 = new Student('grey', 111);
这个时候的原型链关系是什么样的呢?如下:
可以看到,现在子类实例不仅有自己的父类属性,而且还继承了父类原型链上的方法。这样子看起来好像已经很完美了,但是组合继承还是有不足之处的。我们看这里:
我们在子类的构造函数中执行了父类的构造函数,同时我们在设置子类原型的时候又去执行了一次父类的构造函数,这样其实是有些浪费的。我们再看原型链上:
虽然子类实例有了自己的父类属性,但是子类原型上还有一个冗余的name属性,这明显也是不合理的。知道了组合继承不友好的地方,我们就可以在这基础上改造一下组合继承,来达到一种最佳的继承手段。
四、最佳的继承
话不多说,直接上代码:
function Student(name, number) {
Person.call(this, name);
this.number = number;
}
let subProto = Object.create(Person.prototype);
subProto.constructor = Student;
Student.prototype = subProto;
let s1 = new Student('grey', 111);
这就是我们得到的最佳的继承方法,我们来分析一下过程:
1、我们还是先在子类的构造函数中调用父类的构造函数,确保子类的实例独自拥有父类的属性。
2、我们使用Object.create()方法创建了一个对象,这个对象的__proto__指向父类Person的原型。
3、我们将创建对象的constructor属性指向子类的构造函数。
4、我们将子类的原型指向我们创建的对象。
这个时候的原型链关系如下:
这种方式不仅解决了重复调用父类构造函数的问题,同时也避免了在原型链上存在冗余父类属性的情况。
总结
1、原型继承是通过将子类原型连接到父类实例上来实现继承,缺点有constructor指向错误、父类属性共享、参数固定。
2、构造函数继承是通过在子类的构造函数中执行父类的构造函数,从而继承父类的属性。缺点是无法继承父类原型链上的函数。
3、组合继承是将原型继承与构造函数继承组合起来的继承方式,解决了原型继承与构造函数继承的缺点。组合继承的缺点是重复调用了父类的构造函数,原型链上存在冗余的父类属性。
4、最佳的继承通过新建连接到父类原型的空对象的方法解决了组合继承中存在冗余父类属性的缺点。