Skip to content

继承的七种方式 #53

Open
Open
@TieMuZhen

Description

@TieMuZhen
  • 原型链继承
  • class 继承
  • 经典继承
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 圣杯继承(寄生组合式继承)

一、原型链继承

function SuperType(){
   this.property = true;
}
SuperType.property.getSuperValue = function(){
   return this.property;
};

function SubType(){
   this.subproperty = false;
}

// 继承了SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
    return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSuperValue);    //true

原型链继承存在的问题

  • 在SuperType构造函数中定义的引用类型的属性(如this.arrNames=["A", "B", "C"]),会被SubType中的所有instance共享,且SubType修改引用类型的时候,因为与SuperType中的引用类型是同一个,所以SuperType的引用类型也会跟着修改
function SuperType(){
  this.arrNames = ["A", "B", "C"];
}

function SubType(){}
SubType.prototype = new SuperType();

var instance1 = new SubType();
var instance2 = new SubType();

instance1.arrNames.push("D");

console.log(instance1.arrNames);    //"A", "B", "C", "D"
console.log(instance2.arrNames);    //"A", "B", "C", "D"
  • 创建子类实例时,无法向超类型的构造函数中传递参数。

二、借用构造函数(经典继承或伪造对象)

在子类的构造函数中通过apply()/call()调用超类的构造函数,相当于让超类在子类中重新执行一次,使得子类具有超类的属性/方法。

function SuperType(){
   this.arrNames = ["A", "B", "C"];
}

function SubType(){
   SuperType.call(this);
}

var instance1 = new SubType();
var instance2 = new SubType();

instance1.arrNames.push("D");

console.log(instance1.arrNames);    //"A", "B", "C", "D"
console.log(instance2.arrNames);    //"A", "B", "C"

优点

  • 这样做的好处在于解决了原型链中的两个问题。

问题

  • 仅仅得到了SuperType的自身属性,在SuperType.prototype中定义的函数方法无法访问,因此无法进行函数复用。

三、组合继承

组合继承避免了原型链和借用构造函数的缺陷,融合了两者的优点,成为了JavaScript中最常用的继承模式。而且,instanceofisPrototypeOf()也能够用于识别基于组合继承创建的对象。

function SuperType(name){
   this.name = name;
   this.colors = ["r", "g"];
}

SuperType.prototype.sayName = function(){
   console.log(this.name);
};

function SubType(name, age){
   SuperType.call(this, name); // 继承属性      第二次调用超类构造函数

   this.age = age;
}

SubType.prototype = new SuperType(); // 继承方法       第一次调用超类构造函数
SubType.prototype.constructor = SubType; // 设置constructor
SubType.prototype.sayAge = function() {
   console.log(this.age);
}

var instance1 = new SubType("Nic", 21);
instance1.colors.push("B");
console.log(instance1.colors);  //"r", "g", "b"
instance1.sayName();            //"Nic"
instance1.sayAge();             //21

var instance2 = new SubType("Ali", 24);
console.log(instance2.colors);  //"r", "g"
instance2.sayName();            //"Ali"
instance2.sayAge();             //24

问题

  • 调用两次超类构造函数:

四、原型式继承

可以基于已有的对象创建新对象,同时还不必因此创建自定义类型:

function object(o){
   function F(){}
   F.prototype = o;
   return new F();
}

在object函数内部创建一个临时性的构造函数,将传入的对象作为其原型,返回该类型的新实例。本质上object()对传入的对象执行了一次浅复制。

也就是ES5中的Object.create(instance[, params]),其参数为用作新对象的原型对象和(可选的)一个为新对象定义额外属性的对象:

 var person = {
     name: "Nic",
     friends: ["A", "B"]
 };

 var anotherPerson = Object.create(person, {
     name: {
         value: "Jack"
     }
 });

 console.log(anotherPerson.name);    // "Jack"

 person.friends.push("C");
 console.log(anotherPerson.friends); // ['A', 'B', 'C']
 console.log(person.friends); // ['A', 'B', 'C']

适用于在没有必要创建构造函数,只是想让一个对象与另一个对象保持类似的情况下或者浅复制。

五、寄生式继承

类似于寄生构造函数+工厂模式

function createAnother(original){
     var clone = Object.create(original);
     clone.sayHi = function() {
         console.log(this.name);
     };
     return clone;
 }

 var person = {
     name: "Nic"
 };

 var anotherPerson = createAnother(person);
 anotherPerson.sayHi();      //"Nic"

问题

  • 对象中添加函数不能复用,上面的sayHi函数被写死在createAnother中无法复用

六、圣杯模式(寄生组合式继承)

解决组合继承模式的调用两次超类构造函数问题,是引用类型最理想的继承方式

原理: 增加一个中间层,增强隔离性,解决子类对父类的污染问题

// 对圣杯模式的封装
var inherit = (function () {
    function Buffer () {}
    return function (Target, Origin) {
        Buffer.prototype = Origin.prototype;
        Target.prototype = new Buffer();
        Target.prototype.constructor = Target; // 添加constructor属性以备用
        Target.prototype.super_class = Origin; // 添加super_class 属性以备用
    }
})()

// 使用
function Teacher(){}
Teacher.prototype.name = "Tom";

function Student(){}

inherit(Student, Teacher);

var s = new Student();
var t = new Teacher();

console.log(s, t);

prototype内存结构

function Teacher(){}
Teacher.prototype.name = "Tom";

function Student(){}

var s = new Student();
var t = new Teacher();

上面代码的内存结构如下图

调用inherit(Student, Teacher);继承之后,内存结构改变如下:

首先是function Buffer () {}new Buffer()后内存的变化

然后是下面语句中prototype保存地址的改变如下图

Buffer.prototype = Origin.prototype;
Target.prototype = new Buffer();
Target.prototype.constructor = Target; // 添加constructor属性以备用
Target.prototype.super_class = Origin; // 添加super_class 属性以备用

七、Class

类声明和类表达式的主体都执行在严格模式下。比如,构造函数,静态方法,原型方法,getter和setter都在严格模式下执行。

我们先回顾用函数实现Student的方法:

function Student(name) {
    this.name = name;
}

Student.prototype.hello = function () {
    console.log(this.name);
}

如果用新的class关键字来编写Student,可以这样写:

class Student {
    constructor(name) {
        this.name = name;
    }

    hello() {
        console.log(this.name);
    }
}

比较一下就可以发现,class的定义包含了构造函数constructor和定义在原型对象上的函数hello()(注意没有function关键字),这样就避免了Student.prototype.hello = function () {...}这样分散的代码。

注意: 如果constructor中也有个hello函数的话则会执行constructor中的,不会执行外面这个

最后,创建一个Student对象是一样的:

var xiaoming = new Student('小明');
xiaoming.hello();

class继承

通过extends来实现:

class PrimaryStudent extends Student {
    constructor(name, grade) {
        super(name); // 记得用super调用父类的构造方法!
        this.grade = grade;
    }

    myGrade() {
        alert('I am at grade ' + this.grade);
    }
}

ES6引入的class和原有的JavaScript原型继承有什么区别呢?

实际上它们没有任何区别,class的作用就是让JavaScript引擎去实现原来需要我们自己编写的原型链代码。简而言之,用class的好处就是极大地简化了原型链代码

如果说非要说有差别的话,this指针的创建顺序不一样
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上(Parent.apply(this)),ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this

参考文章

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions