继承

继承是面向对象中一个比较核心的概念。其他正统面向对象语言都会用两种方式实现继承:一个是接口实现,一个是继承。而ECMAScript只支持继承,不支持接口实现,而实现继承的方式依靠原型链完成。

)原型链继承

function Box() {//Box构造

this.name = 'xiaoming';

}

function Desk() {//Desk构造

this.age = 100;

}

Desk.prototype = new Box();//Desc继承了Box,通过原型,形成链条

var desk = new Desk();

alert(desk.age);                           //100

alert(desk.name);//xiaoming得到被继承的属性

function Table() {//Table构造

this.level = 'AAAAA';

}

Table.prototype = new Desk();//继续原型链继承

var table = new Table();

alert(table.name);//继承了Box和Desk


继承_第1张图片
原型链继承流程图

如果要实例化table,那么Desk实例中有age=100,原型中增加相同的属性age=200,最后结果是多少呢?

Desk.prototype.age = 200;//实例和原型中均包含age

PS:以上原型链继承还缺少一环,那就是Obejct,所有的构造函数都继承自Obejct。而继承Object是自动完成的,并不需要程序员手动继承。

经过继承后的实例,他们的从属关系会怎样呢?

alert(table instanceof Object);//true

alert(desk instanceof Table);//false,desk是table的超类

alert(table instanceof Desk);//true

alert(table instanceof Box);//true

在JavaScript里,被继承的函数称为超类型(父类,基类也行,其他语言叫法),继承的函数称为子类型(子类,派生类)。继承也有之前问题,比如字面量重写原型会中断关系,使用引用类型的原型,并且子类型还无法给超类型传递参数。

原型链继承的缺陷:

1、 无法从子类中调用父类的构造函数, 这样就没有办法把子类中属性赋值给父类。

2、 父类中属性是在子类的原型中的,这违背了我们前面所讲的封装的理念( 属性在对象中,方法在原型中), 会出现前面值的混淆问题。

2)对象冒充的方式继承

为了解决引用共享和超类型无法传参的问题,我们采用一种叫借用构造函数的技术,或者成为对象冒充(伪造对象、经典继承)的技术来解决这两种问题。

function Box(age) {

this.name = ['xiaoming', 'Jack', 'Hello']

this.age = age;

}

function Desk(age) {

Box.call(this, age);//对象冒充,给超类型传参

}

var desk = new Desk(200);

alert(desk.age);

alert(desk.name);

desk.name.push('AAA');//添加的新数据,只给desk

alert(desk.name);

3)组合继承

借用构造函数虽然解决了刚才两种问题,但没有原型,复用则无从谈起。所以,我们需要原型链+借用构造函数的模式,这种模式成为组合继承

function Box(age) {

this.name = ['xiaoming', 'Jack', 'Hello']

this.age = age;

}

Box.prototype.run = function () {

return this.name + this.age;

};

function Desk(age) {

Box.call(this, age);//对象冒充

}

Desk.prototype = new Box();//原型链继承

var desk = new Desk(100);

alert(desk.run());

组合式继承是JavaScript最常用的继承模式;但,组合式继承也有一点小问题,就是超类型在使用过程中会被调用两次:一次是创建子类型的时候,另一次是在子类型构造函数的内部。

function Box(name) {

this.name = name;

this.family = ['哥哥','妹妹','父母'];

}

Box.prototype.run = function () {

return this.name;

};

function Desk(name, age) {

Box.call(this, name);//第二次调用Box

this.age = age;

}

Desk.prototype = new Box();//第一次调用Box

4)空对象继承

以上代码是之前的组合继承,那么用空对象直接继承prototype方法,解决了两次调用的问题。

function extend(Child, Parent) {

var F = function(){};

F.prototype = Parent.prototype;

Child.prototype = new F();

Child.prototype.constructor = Child;

//Child.uber = Parent.prototype;

}

这个extend函数,就是YUI库如何实现继承的方法。

另外,说明一点,函数体最后一行

Child.uber = Parent.prototype;

意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是"向上"、"上一层"。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

function Box(name) {

this.name = name;

this.arr = ['哥哥','妹妹','父母'];

}

Box.prototype.run = function () {

return this.name;

};

function Desk(name, age) {

Box.call(this, name);

this.age = age;

}

extend( Desk,Box);//通过这里实现继承

var desk = new Desk('xiaoming',100);

desk.arr.push('姐姐');

alert(desk.arr);

alert(desk.run());//只共享了方法

var desk2 = new Desk('Jack', 200);

alert(desk2.arr);//引用问题解决

5)拷贝继承

上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用"拷贝"方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?这样我们就有了第五种方法。

首先,还是把Animal的所有不变属性,都放到它的prototype对象上。

function Animal(){}

Animal.prototype.species = "动物";

然后,再写一个函数,实现属性拷贝的目的。

function extend2(Child, Parent) {

var p = Parent.prototype;

var c = Child.prototype;

for (var i in p) {

c[i] = p[i];

}

c.uber = p; }

这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。

6)ECMA6 extends关键字 类的继承

先来说一说,es6的class语法

JavaScript语言的传统方法是通过构造函数,定义并生成新对象。下面是一个例子。

functionPoint(x,y){

this.x=x;

this.y=y;

}

Point.prototype.toString=function(){

return'('+this.x+', '+this.y+')';

};

varp=newPoint(1,2);

上面这种写法跟传统的面向对象语言(比如C++和Java)差异很大,很容易让新学习这门语言的程序员感到困惑。

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用ES6的“类”改写,就是下面这样

//定义类

classPoint{

constructor(x,y){

this.x=x;

this.y=y;

}

toString(){

return'('+this.x+', '+this.y+')';

}  }


上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5的构造函数Point,对应ES6的Point类的构造方法。

Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

ES6的类,完全可以看作构造函数的另一种写法。

classPoint{

// ...

}

typeofPoint// "function"

Point===Point.prototype.constructor// true

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

classBar{

doStuff(){

console.log('stuff');

}

}

varb=newBar();

b.doStuff()// "stuff"


构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

classPoint{

constructor(){

// ...

}

toString(){

// ...

}

toValue(){

// ...

}

}

//等同于

Point.prototype={

toString(){},

toValue(){}

};

在类的实例上面调用方法,其实就是调用原型上的方法

classB{}

letb=newB();

b.constructor===B.prototype.constructor// true

上面代码中,b是B类的实例,它的constructor方法就是B类原型的constructor方法。

由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。

classPoint{

constructor(){

// ...

}

}

Object.assign(Point.prototype,{

toString(){},

toValue(){}

});


prototype对象的constructor属性,直接指向“类”的本身,这与ES5的行为是一致的。

Point.prototype.constructor===Point// true


类的实例对象需要注意:

生成类的实例对象的写法,与ES5完全一样,也是使用new命令。如果忘记加上new,像函数那样调用Class,将会报错。

/报错

varpoint=Point(2,3);

//正确

varpoint=newPoint(2,3);

不存在变量提升

Class不存在变量提升(hoist),这一点与ES5完全不同。

newFoo();// ReferenceError

class Foo{}

上面代码中,Foo类使用在前,定义在后,这样会报错,因为ES6不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。

Class的继承

Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。

class ColorPoint extends Point{}

上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。下面,我们在ColorPoint内部加上代码。

class ColorPoint extends Point{

constructor(x,y,color){

super(x,y);//调用父类的constructor(x, y)

this.color=color;

}

toString(){

returnthis.color+' '+super.toString();//调用父类的toString()

}    }

上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。


子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

class Point{/* ... */}

class ColorPoint extends Point{

constructor(){

}

}

letcp=newColorPoint();// ReferenceError


上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。

constructor(...args){

super(...args);

}

另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

class Point{

constructor(x,y){

this.x=x;

this.y=y;

}

}

class ColorPoint extends Point{

constructor(x,y,color){

this.color=color;// ReferenceError

super(x,y);

this.color=color;//正确

}

}

上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。

下面是生成子类实例的代码。

letcp=newColorPoint(25,8,'green');

cpinstanceofColorPoint// true

cpinstanceofPoint// true

上面代码中,实例对象cp同时是ColorPoint和Point两个类的实例,这与ES5的行为完全一致。

你可能感兴趣的:(继承)