Skip to content

Latest commit

 

History

History
513 lines (430 loc) · 16.9 KB

杂记-11.JS实现继承的几种方式.md

File metadata and controls

513 lines (430 loc) · 16.9 KB

JS实现继承的几种方式

既然要实现继承,那么首先我们得有一个父类,代码如下:

// 定义一个动物类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

1、原型链继承

核心: 将父类的实例作为子类的原型

function Cat(){ 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true

特点:

  1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  2. 父类新增原型方法/原型属性,子类都能访问到
  3. 简单,易于实现

缺点:

  1. 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
  2. 无法实现多继承
  3. 来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)(详细请看附录代码: 示例1)
  4. 创建子类实例时,无法向父类构造函数传参

推荐指数:★★(3、4两大致命缺陷)

2、构造继承

核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特点:

  1. 解决了1中,子类实例共享父类引用属性的问题
  2. 创建子类实例时,可以向父类传递参数
  3. 可以实现多继承(call多个父类对象)

缺点:

  1. 实例并不是父类的实例,只是子类的实例
  2. 只能继承父类的实例属性和方法,不能继承原型属性/方法
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

推荐指数:★★(缺点3)

3、实例继承

核心:为父类实例添加新特性,作为子类实例返回

function Cat(name){
  var instance = new Animal();
  instance.name = name || 'Tom';
  return instance;
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false

特点:

  1. 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果

缺点:

  1. 实例是父类的实例,不是子类的实例
  2. 不支持多继承

推荐指数:★★

4、拷贝继承

function Cat(name){
  var animal = new Animal();
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
  Cat.prototype.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特点:

  1. 支持多继承

缺点:

  1. 效率较低,内存占用高(因为要拷贝父类的属性)
  2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)

推荐指数:★(缺点1)

5、组合继承

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

特点:

  1. 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  2. 既是子类的实例,也是父类的实例
  3. 不存在引用属性共享问题
  4. 可传参
  5. 函数可复用

缺点:

  1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

推荐指数:★★★★(仅仅多消耗了一点内存)

6、寄生组合继承

核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();

// Test Code
var cat = new Cat();
Cat.prototype.constructor = Cat; // 需要修复下构造函数
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

特点:

  1. 堪称完美

缺点:

  1. 实现较为复杂

推荐指数:★★★★(实现复杂,扣掉一颗星)

继承方式

  1. 原型链继承
  2. 借用构造函数模式继承
  3. 组合继承
  4. 原型式继承
  5. 寄生式继承
  6. 寄生组合

原型链继承

        相信小伙伴们都知道到原型链继承(ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法),因为原型链继承非常的强大,但是也有它的缺点,接下来咱们就按照上面的维度看看原型链继承到底是什么鬼 代码实现:(需要两个构造函数来完成一个原型链继承)

// SuperType 构造函数称为超类
function SuperType (){
    this.name='super';
    this.friend=[];
    this.property = true; 
}
SuperType.prototype.getName=function(){
   return this.name;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}; 
// SubType 构造函数称为子类
function SubType(name,age){
  this.name=name;
  this.age=age;
  this.subproperty = false; 
}
SubType.prototype=new SuperType();
SubType.prototype.constrcutor=SubType;
SubType.prototype.getAge=function(){
   return this.age;
}
SubType.prototype.getSubValue = function (){
return this.subproperty;
}; 
var child = new SubType('shiny',12);
console.log(child.getName)//shiny
console.log(child.getAge())//12

复制代码图解部分 属性

基本原理

   使用类似作用域的原型链,进行继承查找

语言实现

        定义两个构造函数,分别为父类(SuperType)、子类(SubType),为了实现子类能够使用父类的属性(本身和原型上面的属性)。重写子类的原型,让子类的原型指向父类实例,这样子类的构造函数就是父类的实例地址,实现子类可以使用父类的本身和原型上的属性

优点

        子类可以通过原型链的查找,实现父类的属性公用与子类的实例

缺点

        

一些引用数据操作的时候会出问题,两个实例会公用继承实例的引用数据类 谨慎定义方法,以免定义方法也继承对象原型的方法重名 无法直接给父级构造函数使用参数

借用构造函数模式继承

        虽然原型链继承很强大但是也有他的缺点,借用构造函数继承可以解决原型链继承的缺点,开线面的解释 代码实现:

// 把父类当中一个函数使用
function SuperType(name){
this.name=name
this.friend=['a','b']
}
SuperType.prototype.getFriend=function(){
  return this.firend
}
function SubType(name){
  // 执行父类函数
  SuperType.call(this,name);
}
var child = new SubType('shiny')
var childRed = new SubType('red')
console.log(child.name)//shiny
console.log(childRed.name)//red
child.firend.push('c')
console.log(child.friend)//a,b,c
console.log(childRed.friend)//a,b
console.log(childRed.getFriend)//undefined

复制代码基本原理 使用call apply方法,通过执行方法修改tihs (上下文),是的父级的this变成子类实例的this,这样每个实例都会得到父类的属性,实现引用属性备份 使用场景 父类中需要一些子类使用共享的引用类型,并且子类可能会操作父类共享的引用类型 但是父类的非this绑定的属性和方法是不可以使用的(放在父类prototype的属性和方法) 语言实现 不要把父类当中构造函数,当中一个函数来处理这样更容易理解,在子类的构造函数中借用父类函数通过修改this来执行,这样子类的实例包含父类的属性

优点

解决了原型链继承的 引用类型操作问题 解决了父类传递参数问题

缺点

仅仅使用借用构造函数模式继承,无法摆脱够着函数。方法在构造函数中定义复用不可谈 对于超类的原型定义的方法对于子类是不可使用的,子类的实例只是得到了父类的this绑定的属性 考虑到这些缺点,单独使用借用构造函数也是很少使用的

组合继承

        上面的两种继承方式(原型链继承+借用构造函数继承),都有自己优缺点,但是他们不是很完美,下面解释一下组合继承 代码实现:

代码实现:

function SuperType(name){
    this.name=name;
    this.firend=['a','b']
}
SuperType.prototype.getName=function(){
   return this.name
}
function SubType(name,age){
   this.age=age;
   SuperType.call(this,name)
}
SubType.prototype=new SuperType();
SubType.prototype.constrcutor = SubType;
SubType.prototype.getAge=function(){
   return this.age
}
var childShiny=new SubType('shiny',23);
var childRed = new SubType('red',22);
childShiny.firend.push('c');
childRed.firend.push('d');
console.log(childShiny.getName());
console.log(childShiny.getAge());
console.log(childRed.getName());
console.log(childRed.getAge());
console.log(childRed.friend);//[a,b,d]
console.log(childShiny.friend);//[a,b,c]

复制代码基本原理

使用原型链的继承实现,通过原型查找功能来满足原型链共享方法 使用借用构造函数方法,使用实例备份父类共享引用类型备份

使用场景 得到原型链继承和构造函数继承的优点,是被开发人员认可的一种继承方式,但是也有他的缺点 语言实现

定义两个构造函数,分别为父类(SuperType)、子类(SubType),为了实现子类能够使用父类的属性(本身和原型上面的属性)。重写子类的原型,让子类的原型指向父类实例,这样子类的构造函数就是父类的实例地址,实现子类可以使用父类的本身和原型上的属性 不要把父类当中构造函数,当中一个函数来处理这样更容易理解,在子类的构造函数中借用父类函数通过修改this来执行,这样子类的实例包含父类的属性

优点

解决了原型链继承引用类型的实例操作导致引用改变 解决了借构造函数继承方式的,父类原型子类实例可以使用

缺点

  • 父类的构造函数被实例换了两次
  • 实例会有父类的构造函数的一些this属性、子类的构造函数(prototype)上也有一份实例的上有的属性

原型式继承

        话说上面的的组合继承不是已经被开发者认可了吗,原型式继承是啥?下面咱们看看原型式继承是什么样的。 代码实现: 代码实现:

function object(o){
  function F(){};
  F.prototype=o;
   return new F()
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var personShiny = object(person);
var personRed = object(person);
console.log(personShiny.name)//Nicholas
console.log(personRed.name)//Nicholas
personShiny.friends.push('red');
personRed.friends.push('shiny');
console.log(personShiny.friends)//["Shelby", "Court", "Van","red","shiny"]

//ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一 //个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下, //Object.create()与 object()方法的行为相同。

var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var personShiny = Object.create(person);
var personRed = Object.create(person);
console.log(personShiny.name)//Nicholas
console.log(personRed.name)//Nicholas
personShiny.friends.push('red');
personRed.friends.push('shiny');
console.log(personShiny.friends)//["Shelby", "Court", "Van","red","shiny"]

复制代码基本原理 通过Object.create()方法来创建一个有基础类的实例,这实例的__proto__指向基础类 使用场景 在不使用构造函数的情况下,只想让一个对象与另一个对象保持类似的情况下 语言实现 需要创建一个基础对象,作为一个新对象的基础对象,通过object方法或者Object.create方法处理得到一个新实例,这个新实例上的__proto__指向基础对象

优点

再不用创建构造函数的情况下,实现了原型链继承,代码量减少一部分

缺点

一些引用数据操作的时候会出问题,两个实例会公用继承实例的引用数据类 谨慎定义方法,以免定义方法也继承对象原型的方法重名 无法直接给父级构造函数使用参数

寄生继承

        咱们看了上面的原型式继承,其实就是和原型链继承差别不大,只是省去了构造函数这一部,但是原型式继承也是有缺点的(不能够给备份的对象添加属性),下面寄生继承来解决。 代码实现: 代码实现: // 和工厂模式非常类似,创建一个对象,增强一些功能并返回该对象

function createAnother(o){
   var clone = Object(o);
   clone.sayHi=function(){
     console.log('hi')
}
   return clone
}
var person = {
   name:'shiny',
   friends:['a','b']
}
var personShiny = createAnother(person);
console.log(personShiny.sayHi())//Ho

复制代码基本原理 备份一个对象,然后给备份的对象进行属性添加,并返回 使用场景 在考不使用构造函数的情况下实现继承,前面示 范继承模式时使用的 object()函数不是必需的;任何能够返回新对象的函数都适用于此模式 语言实现 类似构造函数,通过一个执行方法,里面创建一个对象,为该对象添加属性和方法,然后返回

优点

再不用创建构造函数的情况下,实现了原型链继承,代码量减少一部分 可以给备份的对象添加一些属性

缺点

类似构造函数一样,创建寄生的方法需要在clone对象上面添加一些想要的属性,这些属性是放在clone上面的一些私有的属性

寄生组合继承

        咱们看了上面的组合继承看上去已经很完美了,但是也有缺点(父类被实例化两次、子类实例和子类的构造函数都有相同的属性),寄生组合就是来解决这些问题的 代码实现: 代码实现:

 function inheritPrototype({SubType,SuperType}){
      const prototype = Object(SuperType.prototype);
      prototype.constrcutor=SubType;
      SubType.prototype=prototype;
}
function SuperType(name){
    this.name=name;
    this.friends=['a','b']
}
SuperType.prototype.getName=function(){
   return this.name;
}
function SubType(name,age){
    this.age=age;
    SuperType.call(this,name)
}
inheritPrototype({SubType,SuperType});
SubType.prototype.getAge=function(){
   return this.age
}
var SubTypeShiny = new SubType('Shiny',23);
SubTypeShiny .friends.push('c')
var SubTypeRed = new SubType('Red',21);
SubTypeRed .friends.push('d')
console.log(SubTypeShiny.getName())//Shiny
console.log(SubTypeShiny.getAge())//22
console.log(SubTypeShiny.friends)//['a','b','c']
console.log( SubTypeRed.getName())//Red
console.log( SubTypeRed.getAge())//21
console.log( SubTypeRed.friends)//['a','b','d']

复制代码基本原理

子类构造函数内通过call、apply方法进行修改父类构造函数的this和执行父类构造函数,使的子类的实例拥有父类构造函数的一些属性, 结合子类的原型修改成父类构造函数的原型,并把父类的原型的constructor指向子类构造函数

使用场景 在考不使用构造函数的情况下实现继承,前面示 范继承模式时使用的 object()函数不是必需的;任何能够返回新对象的函数都适用于此模式 语言实现 极度类似组合寄生方式,只是修改了子类原型链继承的方式,组合寄生是继承父类的实例,寄生组合寄生则是通过一子类的原型继承父类的原型,并把该原型的constructor指向子类构造函数

优点

在少一次实例化父类的情况下,实现了原型链继承和借用构造函数 减少了原型链查找的次数(子类直接继承超类的prototype,而不是父类的实例)

缺点

暂无 下面是组合继承和寄生组合继承的原型图对比