JS-手把手教你搞懂JS继承的七种方式

244 阅读7分钟

前言

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);