前言:
随着es6的出现,继承方式逐渐被固定为使用extends关键字实现继承。虽然es6实现继承起来非常简单,但本人仍然觉得es5的继承是必须学习的,因为es5的继承方式,描述了JavaScript继承思想的发展历程,已经影响到了es6继承的实现方案。该篇文章打算从es5的多种继承思想开始阐述继承思路,同时也包括es6的继承的方式。希望能够帮助读者或者本人在前端技术快速发展的前提下,不断打下坚实的基础,以期待厚积薄发。
一、ES5继承思想
许多面向对象的开发方式中,两种主流的继承方式为:接口继承(只继承方法签名)和实现继承(继承实际的方法)。ECMAScript 是实现继承的方式,依靠原型链实现继承的。
(1)、原型链继承(基本方式)
原型链模式虽然实现了继承,但也存在一些问题。首先,再通过原型来实现继承时,原型实际上会变成另一个对象类型的实例,如果多次继承的情况很容易无法实现继承。其次,在创建子类实例时,不能向父类的构造函数传递参数。因为这种继承方式存在着问题,因此引申出了后面的继承方式。
//父类
function Parent(){
this.type="parent";
}
Parent.prototype.getType=function(){
console.log('type:', this.type);
return this.type;
}
//子类
function Child(){
this.childType="child";
}
//继承
Child.prototype=new Parent();
Child.prototype.constructor=Parent;//此处用来处理instanceof或isPropertyOf检查类型失败的问题
Child.prototype.getChildType=function(){
console.log('childType:', this.childType);
return this.childType;
}
//创建实例
var instance = new Child();
alert(instance.getType());
(2)、借用构造函数
该继承方式解决了原型链继承方式无法向父类传递参数的问题。通过call或者apply的this绑定的功能,实现了此种继承方式。但此种继承方式使方法都在构造函数中定义,无法解决复用问题。同时,父类原型中定义的方法,对子类型而言是看不到的。因此此种继承类型也经常不是单独出现的
//父类
function Parent(){
this.attr=['name','age','sex'];
}
//子类
function Child(){
Parent.call(this,arguments);
}
//实例
var instance1=new Child();
instance1.attr.push('salary');
alert(instance1.attr);//name,age,sex,salary
var instance2=new Child();
alert(instance2.attr);//name,age,sex
(3)、组合继承
也叫伪经典继承,指的是将原型链和借用构造函数的技术组合到一起,在创造出来的新的继承方式。组合继承是之前最常用的继承模式。同时能够被instanceof和isPrototypeOf所识别。但该模式也存在一个问题,是无论什么情况下都会调用两次父类的构造方法。
//父类
function Parent(name){
this.name=name;
this.colors=['red','green','blue'];
}
Parent.prototype.sayName=function(){
return this.name;
}
//子类
function Child(name,age){
Parent.call(this,name);
this.age=age;
}
//继承
Child.prototype=new Parent();
Child.prototype.sayAge=function(){
return this.age;
}
var instance1=new Child("张三",29);
instance1.colors.push('black');
alert(instance1.colors);//'red','green','blue','black'
instance1.sayName();//张三
instance1.sayAge();//29
var instance2=new Child("李四",27);
alert(instance2.colors);//'red','green','blue'
instance2.sayName();//李四
instance2.sayAge();//27
(4)、原型式继承
由道格拉斯●克罗克福德提出。此处只是原型式继承概念,下面代码并未真正实现继承,因为他仍然没有解决在创建子类实例时,不能向父类的构造函数传递参数的问题。
//继承方法
function extend(O){
function F(){};
F.prototype=O;
return new F();
}
var person={
name:'张三',
friends:['李四','王五','赵柳']
};
var anotherPerson=extend(person);
anotherPerson.name="李琦";
anotherPerson.friends.push("赵三");
var anotherPerson1=extend(person);
anotherPerson.name="刘总";
anotherPerson.friends.push("六子");
alert(person.friends);//李四,王五,赵柳,赵三,六子
alert(person.name);//张三
(5)、寄生式继承
此种继承方式与原型式继承紧密相连,是原型式继承思路的实现。使用该方式为对象来添加函数,会由于不能共用相同的方法而造成效率降低,因此不推荐使用.
//继承方法
function extend(O){
function F(){};
F.prototype=O;
return new F();
}
//继承对象
function createAnother(original){
var clone =extend(original);
clone.sayHi=function(){
console.log('HI');
};
return clone;
}
var person={
name:'张三',
friends:['李四','王五','赵柳']
};
var anotherPerson=createAnother(person);
anotherPerson.sayHi();//"HI"
(6)、寄生组合式继承
//父类
function Parent(name){
this.name=name;
this.colors=['red','green','blue'];
}
Parent.prototype.sayName=function(){
return this.name;
}
//继承方法
function extend(O){
function F(){};
F.prototype=O;
return new F();
}
function inherit(parent,child){
var prototype=extend(parent.prototype);
prototype.constructor=child;
child.prototype=prototype;
}
//子类
function Child(name,age){
Parent.call(this,name);
this.age=age;
}
//继承
Child.prototype=new Parent();
Child.prototype.sayAge=function(){
return this.age;
}
二、es6继承方式
随着JavaScript的不断发展,es6、es7等新的JavaScript语法规范逐渐发展起来,但其发展的趋势逐渐接近面向对象编程的语言(如java)。如此发展的趋势,给了前端开发工程师更多面向对象开发语言的思想方式。因此建议读者尝试使用es6等最新标准进行开发,因为能够在开发过程中,学会更多的思想以及乐趣。
(1)、extends关键字与super关键字
es6规范了继承方式,采用extends关键字的方式继承父类,继承后同时拥有父类公开属性以及公开方法。在子类中必须调用super,否则会报错,我们先看一下阮一峰老师的描述:
子类必须在constructor 方法中调用super 方法,否则新建实例时会报错。这是因为子类自己的this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super 方法,子类就得不到this 对象。 |
如上描述,可知es6 通过exteds方式实现继承的过程,会创建父类方法以及属性,创建完成后才会创建子类。在子类中调用super是为了创建父类的实例。此处如果仍然存在迷惑,可以想想es5的继承实现方式,首先需要在子类中通过call或者apply方式执行父类的构造函数,但es6和这个过程不同,会优先创建父类。
注意:
1、super
虽然代表了父类的构造函数,但是返回的是子类的实例,即super
内部的this
指的是子类的实例。
2、super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类(如下方的super.p())。
3、由于super
指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super
调用的(如下方代码中在子类中调用super.pointName,页面将会报错)。
4、在子类的静态方法中通过super
调用父类的方法时,方法内部的this
指向当前的子类,而不是子类的实例。
class Point {
pointColor="#f00";//创建后,会写到实例上,而不是原型上,属于实例属性
constructor(){
this.pointName="parent";//创建后,会写到实例上,而不是原型上,属于实例属性
}
p() {
return 2;
}
static printPointName(){
//同一个类中的静态方法和非静态方法可以同名
console.log('pointName:',this.pointName);
}
printPointName(){
//同一个类中的静态方法和非静态方法可以同名
console.log('pointName:',this.pointName);
}
}
class ColorPoint extends Point {
pointName="child";
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
static printName() {
super.printPointName();
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
ColorPoint.pointName='childName';
ColorPoint.printName();//childName
(2)、new.target
在es6中可以使用new.target方式来判断函数的创建方式,因为new.target
指向当前正在执行的函数(指向子类)。如果构造函数不是通过new
命令或Reflect.construct()
调用的,new.target
会返回undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
(3)、es6继承方式
如下图,使用es6的继承方式时,实例对象和构造器对象在继承上会有不同的原型链。继承后构造器对象子类的__proto__属性指向父类构造函数,子类的prototype.__proto__指向父类的prototype。当通过new方式创建实例属性后,子类的__proto__.__proto__指向父类的__proto__。但同时也可以看出父类的this指向子类,bName成为子类的实例属性。
class A {
aName="aaaa"
sayName(){return this.aName}
}
class B extends A {
bName="bbbb"
sayName(){return this.bName}
}
let bTarget=new B();