Fork me on GitHub

Any application that can be written in JavaScript, will eventually be written in JavaScript.

Javascript 继承:经典继承,ES5 继承,ES6 继承

js 中类的实现是基于其原型继承机制。如果两个实例都从同一个原型对象上继承了属性,那么它们是同一个类的实例,实例就继承了原型。
介绍3种最为普遍的实现继承的方法。

1.组合继承方法(经典方法,将原型继承和借用构造函数继承合二为一):

构造原型:
使用构造函数构建一个 Person 函数,将属性赋值在构造函数中。
使用原型将方法挂在原型上,将方法赋值在原型中。

1
2
3
4
5
6
7
8
9
10
function Person(first,last,age,gender,interests) {
this.name = first + ' ' + last;
this.age = age;
this.gender = gender;
this.interests = interests;
this.sprits = ["kind","good","happy"];
};
Person.prototype.greeting = function() {
return ('Hi! I\'m ' + this.name + '.');
}

借用函数 call(或 apply)来继承属性:

1
2
3
4
5
6
function Teacher(first, last, age, gender, interests, subject) {
Person.call(this, first, last, age, gender, interests);
// apply 方法
// Person.apply(this, [first, last, age, gender, interests]);
this.subject = subject;
}

使用原型来继承方法:

1
2
3
4
Teacher.prototype = new Person();
// 接下来就需要将 Teacher()的 prototype.constructor 属性指向 Teacher,若没有这句的话构造器会指向 Person:
Teacher.prototype.constructor = Teacher;
// console.log('Teacher constructor', Teacher.prototype.constructor); //function Teacher(){...}

假如这时候需要 Teacher 添加一个新的 greeting 方法,那么之前原型继承下来的方法就被覆盖了,但是原型上这个方法不会改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Teacher.prototype.greeting = function() {
var prefix;
if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
var prefix;
if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
prefix = 'Mr.';
} else if (this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
prefix = 'Mrs.';
} else {
prefix = 'Mx.';
}
return ('Hello. My name is ' + prefix + ' ' + this.name + ', and I teach ' + this.subject + '.');
}
};

查看下结果:

1
2
3
4
5
6
var hao = new Person("Bevis","Shen","26","male","coding");
var bevis = new Teacher("Bevis","Shen","26","male","coding","Math");
console.log("hao: ", hao); // 这个对象就直接继承了 Person 对象
console.log("hao greeting: ", hao.greeting()); // 这个对象就直接继承了 Person 对象
console.log("bevis: ", bevis);
console.log("bevis greeting: ", bevis.greeting());

2.ES5 继承方法

ES5 继承方法主要是使用新增加 Object.create() 方法规范了原型继承:

构造原型:
使用构造函数构建一个 Person 函数,将属性赋值在构造函数中。
使用原型将方法挂在原型上,将方法赋值在原型中。

1
2
3
4
5
6
7
8
9
10
function Person(first,last,age,gender,interests) {
this.name = first + ' ' + last;
this.age = age;
this.gender = gender;
this.interests = interests;
this.sprits = ["kind","good","happy"];
};
Person.prototype.greeting = function() {
return ('Hi! I\'m ' + this.name + '.');
}

借用函数 call(apply)来继承属性:

1
2
3
4
5
6
function Teacher(first, last, age, gender, interests, subject) {
Person.call(this, first, last, age, gender, interests);
// apply 方法
// Person.apply(this, [first, last, age, gender, interests]);
this.subject = subject;
}

使用原型来继承方法,ES5 中的区别就只是在用来继承方法上新增加的这个方法而已:

1
2
Teacher.prototype = Object.create(Person.prototype);
Teacher.prototype.constructor = Teacher;

假如这时候需要 Teacher 添加一个新的 greeting 方法,那么之前原型继承下来的方法就被覆盖了,但是原型上这个方法不会改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Teacher.prototype.greeting = function() {
var prefix;
if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
var prefix;
if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
prefix = 'Mr.';
} else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
prefix = 'Mrs.';
} else {
prefix = 'Mx.';
}
return ('Hello. My name is ' + prefix + ' ' + this.name + ', and I teach ' + this.subject + '.');
}
};

查看下结果:

1
2
3
var bevis = new Teacher("Bevis","Shen","26","male","coding","Math");
console.log(bevis);
console.log("greeting: ", bevis.greeting());

3.ES6 继承方法

ES6 提供了更接近传统语言的方法,引入了 Class(类)这个概念,作为对象的模版。通过class关键字,可以定义类。
ES6 的 class 只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法

定义类:
类本身就是一个构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Point {
// 定义了一个 constructor 构造方法,一个类必须有constructor方法,这个是默认方法
// constructor 中存放的是属性
constructor(x,y) {
this.x = x;
this.y = y;
}
// 定义了一个 toString() 方法
// 定义“类”的方法的时候,前面不需要加上 function 这个关键字
// 方法之间不需要逗号分隔,加了会报错
toString() {
return this.x + ' and ' + this.y;
}
toValueOne() {
return this.x;
}
// 如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”
// 但是这个方法会被继承到子类,但是也必须直接通过类调用
static classMethod() {
return 'hello';
}
}
// 类上的方法其实本质上还是挂在构造函数的 prototype 属性上
// 上面的代码相当于以下:
// Point.prototype = {
// toString(){},
// toValue(){}
// };

// 但是类上面定义的方法都是不可以枚举的(non-enumerabel),这个和 ES5 的行为不一致
console.log("key: ", Object.keys(Point.prototype)); // []
console.log("getOwnPropertyNames: ", Object.getOwnPropertyNames(Point.prototype)); // ["constructor", "toString", "toValueOne"]

使用也是直接对类 new 命令,和构造函数用法是一致的:

1
2
3
4
5
let point1 = new Point("A","b");
console.log("point1: ", point1.toString()); // A and b
console.log("point1 static: ", Point.classMethod()); // hello
// 下面这样使用会抛错,因为静态方法是实例不会继承的,只能通过类直接调用
// console.log("point1 static: ", point1.classMethod());

定义子类:
class 之间可以通过 extends 关键字来实现继承,这个比 ES5 中的继承方法来的清晰方便很多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class colorPoint extends Point {
constructor(x,y,color) {
// super关键字表示父类的构造函数,用来新建父类的this对象
// 子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错,ES6 要求,子类的构造函数必须执行一次 super 函数
// 只有调用 super 之后,才可以使用 this 关键字,否则会报错,所以 super 应该放在 constructor 中的第一个
// 在这里 super() 相当于 Point.prototype.constructor.call(this, x, y)
// 在 constructor 中 super 是作为函数操作的,作为函数时,super()只能用在子类的 constructor 之中,用在其他地方就会报错
super(x,y); // 调用父类的 constructor(x,y)
this.color = color;
}
toString() {
// 当 super 做为对象时,可以放在子类的静态方法中,指向父类
// 这里的 super.p()就相当于A.prototype.p()
return this.color + ' and ' + super.toString(); // 继承父类的 toString()
}
colorValueOne() {
return super.toValueOne();
}
}

查看下结果:

1
2
3
4
let color1 = new colorPoint("e","t","blue");
console.log("colorPoint: ", color1.toString()); // blue and e and t
console.log("colorPoint: ", color1.colorValueOne()); // e
console.log("colorPoint: ", colorPoint.classMethod()); // hello