前言
ES5中,继承方式是将子类的原型指向父类的原型。有原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合继承,ES6新增了class继承。
这里首先描述最新语法Class继承,而后再逐一介绍ES5继承,内容很多,但只需要弄懂class继承就可以了,没必要分散精力。但如果想要完全弄懂继承或者为了应付面试,需要认真地读懂本文的所有代码。每一段代码都有相关注解,很简单哦~
ES6-Class继承
关键字:class extends constructor super
class
基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。类的数据类型就是函数,类本身就指向构造函数。
// ES5构造函数
function Point(x,y){
this.x=x;
this.y=y;
}
Point.prototype.toString=function(){
return '(' + this.x + ',' + this.y + ')';
}
var p=new Point(1,2);
// ES6 Class构造函数
class Point{
constructor(x,y){
this.x=x;
this.y=y;
}
toString(){
return '(' + this.x + ',' + this.y + ')';
}
}
上面代码中,constructor()、toString()这两个方法,其实都是定义在Point.prototype上面。由于类的方法都定义在prototype对象上面,所以类的新方法可以自动添加在prototype对象上面。Object.assign()方法可以很方便地一次向类添加多个方法。
class Point{constructor(){ // ... }}
Object.assign(Point.prototype,{toString(){},toValue(){}});
上面代码中,toString()方法是Point类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致。
class Point{
constructor(x, y){
// ...
}
toString(){ // ... }
}
Object.keys(Point.prototype) // []
// class构造函数不可枚举
Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"]
var Point =function(x, y){ // ... };
Point.prototype.toString =function(){ // ... };
Object.keys(Point.prototype) // ["toString"]
/ ES5构造函数可枚举
Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"]
/
constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。constructor()方法默认返回实例对象(即this),也完全可以指定返回另外一个对象。
class Foo{
constructor(){
return Object.create(null);
}
}
newFoo() instanceof Foo; // false
上面代码中,constructor()函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。 类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,普通构造函数不用new也可以执行。
class Foo{
constructor(){
return Object.create(null);
}
}
Foo() // TypeError: Class constructor Foo cannot be invoked without 'new'
class继承
Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
class Point {}
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() {
return this.color + ' ' + super.toString(); // 调用父类的toString()}}
上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}}
let cp = new ColorPoint(); // ReferenceError
上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。 如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
class ColorPoint extends Point {}
// 等同于class ColorPoint extends Point {
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; // ReferenceErrorsuper(x, y);
this.color = color; // 正确}}
上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。
原型链继承
主要代码:Child.prototype=new Father();
特点:不可传参,可复用,会污染。
看懂此处代码需要具备原型知识,如果需要补充,请移步JS-原型
// 构建父类方法
function Person(){
this.name = 'Hello world';
}
// 为父类方法添加原型属性
Person.prototype.getName = function(){
console.log(this.name);
}
// 构建子类方法
function Child(){
}
// 将父类实例作为子类原型new Person()._prpto_=Person.prototype=Child.prototype._proto_;
// 这样就将父类原型连接到了子类原型地原型链上
Child.prototype = new Person();
// 此时相当于child._proto_=Child.prototype;所以Child._proto_._proto_=Person.prototype;
// 从而将父类的原型链接到了Child实例的原型链上,使得child可以访问到父类的原型方法。
var child1 = new Child();
child1.getName(); // Hello world
// 学理科的宝贝们,有没有发现这像极了数学推理,很有趣吧~
构造函数继承
主要代码:function Child(){Father.call(this);}
特点:可传参,不可复用
this相关内容可移步:JS-this 史上最简单的this
// 构建父类方法
function Person(){
this.name = 'xiaoming';
this.colors = ['red', 'blue', 'green'];
}
// 为父类方法添加原型属性
Person.prototype.getName = function(){
console.log(this.name);
}
// 构建子类方法
// Person.call(this)的意思是,将Person函数(父类方法)的this指向Child的this
function Child(age){
Person.call(this);
this.age = age
}
// 创建Child实例,此时Child里面的this指向child1,所以父类的this也指向child1
var child1 = new Child(23);
// 创建Child实例,此时Child里面的this指向child2,所以父类的this也指向child2
var child2 = new Child(12);
// 所以child1和child2成功继承了父类的属性和方法
child1.colors.push('yellow');
console.log(child1.name); // xiaoming
console.log(child1.colors); // ["red", "blue", "green", "yellow"]
console.log(child2.colors); // ["red", "blue", "green"]
组合继承(常用)
原型链继承+构造函数继承;可传参,可复用,但调用了两次父函数
// 构造函数继承,实现可传参
function Parent(name){
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function(){
console.log(this.name);
}
function Child(name,age){
Parent.call(this,name);
this.age = age;
}
// 原型链继承,实现可复用
Child.prototype = new Parent();
var child1 = new Child('xiaopao',18);
var child2 = new Child('lulu',19);
原型式继承
主要代码:function creatObj(o){function F(){} F.prototype=o return new F() } ;var father={};var child=creatObj(father);
特点:不可复用
function CreateObj(o){
function F(){}
F.prototype = o;
console.log(o.__proto__ === Object.prototype);
console.log(F.prototype.constructor === Object); // true
return new F();
}
var person = {
name: 'xiaopao',
friend: ['daisy','kelly']
}
var person1 = CreateObj(person);
// var person2 = CreateObj(person);
person1.name = 'person1';
// console.log(person2.name); // xiaopao
person1.friend.push('taylor');
// console.log(person2.friend); // ["daisy", "kelly", "taylor"]
// console.log(person); // {name: "xiaopao", friend: Array(3)}
person1.friend = ['lulu'];
// console.log(person1.friend); // ["lulu"]
// console.log(person.friend); // ["daisy", "kelly", "taylor"]
// 注意: 这里修改了person1.name的值,person2.name的值并未改变,并不是因为person1和person2
// 有独立的name值,而是person1.name='person1'是给person1添加了name值,并非修改了原型上的name值
// 因为我们找对象上的属性时,总是先找实例上对象,没有找到的话再去原型对象上的属性。
// 实例对象和原型对象上如果有同名属性,总是先取实例对象上的值
寄生式继承
主要代码:function createChild(o){原型式继承}
特点:不可复用
var ob = {
name: 'xiaopao',
friends: ['lulu','huahua']
}
function CreateObj(o){
function F(){}; // 创建一个构造函数F
F.prototype = o;
return new F();
}
// 上面CreateObj函数 在ECMAScript5 有了一新的规范写法,Object.create(ob) 效果是一样的 , 看下面代码
var ob1 = CreateObj(ob);
var ob2 = Object.create(ob);
console.log(ob1.name); // xiaopao
console.log(ob2.name); // xiaopao
function CreateChild(o){
var newob = CreateObj(o); // 创建对象 或者用 var newob = Object.create(ob)
newob.sayName = function(){ // 增强对象
console.log(this.name);
}
return newob; // 指定对象
}
var p1 = CreateChild(ob);
p1.sayName(); // xiaopao
寄生组合式继承(常用)
寄生式继承+组合继承 修复了组合继承的问题
function Parent(name){
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function(){
console.log(this.name);
}
// 原型链^
function Child(name,age){
Parent.call(this,name);
this.age = age;
}
//构造函数^
function CreateObj(o){
function F(){};
F.prototype = o;
return new F();
}
// 原型式^
// Child.prototype = new Parent(); // 这里换成下面
function prototype(child,parent){
var prototype = CreateObj(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 寄生式^
prototype(Child,Parent);
var child1 = new Child('xiaopao', 18);
console.log(child1);