前言
才发现之前没有对JavaScript中的继承做过总结,不过看得到是不少,接下来就对这几种继承方式做一下总结。
class继承
class继承是ES6引入的标准的继承方式。
ES6引入了class(类)这个概念,作为对象的模板,通过class关键字可以定义对象。
<script>
// class 继承
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ',' + this.y + ')';
}
}
var point = new Point(2,3);
class ColorPoint extends Point{
constructor(x,y,color){
super(x,y); //等同于super.constrctor(x,y)
this.color = color;
}
toString(){
return this.color+ ' ' + super.toString();
}
}
var colorPoint = new ColorPoint(2,3,'red');
console.log(colorPoint);
</script>
super的使用
上面代码中,
constructor
方法和toString
方法之中,都出现了super
关键字,它在这里表示父类的构造函数,用来新建父类的this
对象。子类必须在
constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。
我们来看一下通过class继承的类实例化对象的原型链是怎样组织的。
class继承的特点
- 实例化对象的prototype的指向
之前了解过的对象的prototype属性 是指向对象的原型对象,一个没有继承的类实例化出的对象的prototype是Object。
如本段代码中的point实例,他的prototype就是Object。而通过class继承的类实例化出来的colorPoint实例对象的prototype就是他的父类。
- 类声明中方法的位置
继续观察通过class实例化和继承的类实例化的对象原型链的组成方式,发现在class定义的方法和属性并非都在实例化的对象下,而是在对象的prototype指向的下原型对象中。
这是class继承的特点,同样是ES6标准化继承的规范的东西,接下来其他几种继承方式将与class继承进行比较。
原型链继承
原型链是JavaScript中面向对象的一大特点。,通过人为的对原型链的操作可以实现继承的效果。注在ES6之前对象的实例化是通过构造函数function来实现的。
function Animal() {
this.colors = ['black', 'white']
}
Animal.prototype.getColor = function () {
return this.colors
}
function Dog() { }
Dog.prototype = new Animal()
let dog1 = new Dog()
let animal = new Animal();
console.log(dog1);
console.log(animal);
这里为了与class类定义实现统一,且方便观察原型链的变化,将构造函数用一种类似class的方式编写的。
整个原型链继承的特点就是将子类构造函数的prototype直接指向父类构造函数的实例化。
很明显这样做的问题就是会将父类的所有属性和方法挂在子类的原型对象上,子类本身并无属性和方法。这就导致了原型中包含的引用类型属性将被所有实例共享。
(很容易理解,我们每次创建子类实例对象的的时候,都是使用Dog的构造方法,而这个构造方法的prototype被一次性指向的父类构造方法实例,所以无论我们实例化多少子类对象,他们的prototype都是指向的同一个父类实例,引用共享)
很明这种继承方式有很大的缺陷。
构造函数继承
构造函数继承也是ES6之前的一种用于对象继承的方式。
// 构造函数继承
function Animal(name) {
this.name = name
this.getName = function () {
return this.name
}
}
function Dog(name) {
Animal.call(this, name)
}
Dog.prototype = new Animal()
let dog1 = new Dog("小明")
let animal = new Animal("小明父亲");
console.log(dog1);
console.log(animal);
在子类构造函数内部调用父类的构造函数,这一步骤很明显会将父类的所有属性和方法都克隆到子类对象上。
然后再根据class标准将子类构造函数的prototype指向父类构造函数实例化。
解决了共享引用数据类型的问题和无法传参的问题。
现在存在的问题是方法是定义在父类构造函数当中的,其实接下来要讲的组合继承与现在这个构造函数继承很相似只不过是将需要共享的方法放在了父级类的原型对象上了。这里也是对于ES6中class继承的对照。
构造函数继承实现的子类实例中的方法和属性完全继承父类,而子类构造函数原型上的方法或属性实际上是冗余的,并没有作用。这并不符合class继承的标准。
组合继承
组合继承是原型链继承和构造函数继承的一种组合。实际体现在在我们编写构造函数之后,只是在构造函数内部声明属性,而将需要共享的方法通过Animal.prototype定义在父类构造函数的原型对象上,然后在通过构造函数继承的方式进行继承:
1.在子类构造函数中调用父类构造函数(继承父类的属性)
2.将子类构造函数的prototype指向父类构造函数的实例(继承原型链上定义的共享方法)
3.将子类构造函数的constructor写回子类构造函数本身(重写prototype会改变constructor)
function Animal(name) {
this.name = name
this.colors = ['black', 'white']
}
Animal.prototype.getName = function() { //将需要共享的方法写在构造函数的原型上
return this.name
}
function Dog(name, age) {
Animal.call(this, name)
this.age = age
}
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog
let dog = new Dog('旺财',12)
console.log(dog)
很容易看出来,现在已经实现了方法共用的功能。
思考
仔细观察发现属性冗余的问题仍旧无法解决,其实通过子级构造函数中调用父级构造函数,来初始化数据之后,子级所缺的也仅仅是定义在父级的原型对象的方法了。我们直接将子级构造函数的原型指向父级构造函数的原型是不是就可以了呢,操作如下:
// 组合继承
function Animal(name) {
this.name = name
this.colors = ['black', 'white']
}
Animal.prototype.getName = function () {
return this.name
}
function Dog(name, age) {
Animal.call(this, name)
this.age = age
}
Dog.prototype.getName = function () {
return this.name;
}
Dog.prototype = Animal.prototype;
Dog.prototype.constructor = Dog;
let animal = new Animal("杰哥");
let dog = new Dog('旺财', 2)
console.log(dog)
console.log(animal);
很明显这样是可以实现继承的,但是子类的后代继续继承的话就会出现问题,原型链就不再有条理了。
总结
构造函数和原型链继承是ES6之前的使用方式,组合继承是将两种方式相结合。
组合继承和标准的class继承是最相似的,只是会在原型上遗留一些无用的属性。
class继承机制十分完善,形成的原型链很有条理。可以依照class继承与其他继承方式的对比来学习JavaScript中的对象继承知识。