多种继承方式(含es6)

114 阅读11分钟

Class继承

Class是ES6(ECMAScript 2015)中引入的一个语法糖,它提供了一种更接近传统面向对象语言的写法来创建对象和实现继承。简单来说,Class可以让开发者使用class关键字来定义类,使用constructor来定义构造函数,以及使用extends来实现继承。

class Parent {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 调用父类的constructor(name)
    this.age = age;
  }
  sayAge() {
    console.log(this.age);
  }
}

Class继承的优缺点:

优点:(语法简单清晰,易于理解和维护;可以通过super关键字向父类构造函数传递参数;继承方式与传统的面向对象语言更接近;)

  • 语法清晰:Class的语法更接近其他面向对象编程语言,使得代码更易于理解和维护。
  • 继承简单:使用extends关键字可以很方便地实现继承,代码结构更清晰。
  • 方便添加原型方法:在Class内部可以直接定义原型方法,不需要手动操作原型链。
  • 更好的默认行为:Class声明会自动执行严格模式,避免了意外的全局变量泄露。

缺点:(仍然是基于原型链的继承,对于一些复杂的继承需求可能需要更深入的原型链知识;class语法糖背后仍然是基于原型链的机制,因此仍然存在一些原型链的局限性;)

  • 不是真正的类:JavaScript的Class仍然是基于原型链的,它只是语法糖,并不是像传统面向对象语言中的类。
  • 隐式原型:Class的继承机制仍然依赖于原型链,这可能导致一些不熟悉原型链的开发者感到困惑。
  • 语法限制:Class声明中的一些特性(如构造函数必须使用new调用)可能会导致一些错误,而这些错误在使用函数构造器时可能不会出现。
  • 性能问题:在某些JavaScript引擎中,Class可能会比传统的构造函数和原型链方法稍微慢一些,尽管这种差异通常很小。

总的来说,Class提供了一种更现代化的方式来编写面向对象的JavaScript代码,它使得代码更加简洁和易于理解,但同时也带来了一些与传统JavaScript原型链不同的行为和限制。

原型链继承

  1. 定义一个父类构造函数,比如Parent,并在其原型上添加一些方法。
  2. 定义一个子类构造函数,比如Child
  3. 将子类的原型设置为父类的一个实例,即Child.prototype = new Parent();
  4. 现在,创建子类的实例时,它将能够访问父类原型上的属性和方法。
// 定义父类构造函数
function Parent() {
  this.parentProperty = true;
}

// 在父类原型上添加方法
Parent.prototype.getParentProperty = function() {
  return this.parentProperty;
};

// 定义子类构造函数
function Child() {
  this.childProperty = false;
}

// 设置子类的原型为父类的一个实例
Child.prototype = new Parent();

// 创建子类的实例
var childInstance = new Child();

// 访问父类原型上的方法
console.log(childInstance.getParentProperty()); // 输出 true

原型链继承的优缺点:

优点:(简单、易于实现;实例可以共享原型对象的属性和方法;)

  • 简单:实现起来比较简单,只需要设置子类的原型即可。
  • 共享原型属性和方法:子类实例可以访问父类原型上的属性和方法,实现资源共享。

缺点:(引用类型的属性被所有实例共享,一个实例修改了原型属性,其他实例也会受到影响;创建子类实例时,不能向父类构造函数传参;)

  • 引用类型属性共享问题:如果父类原型上有引用类型属性,那么所有子类实例都会共享这个属性,一个实例的修改会影响其他实例。
  • 不能传递参数:在创建子类实例时,无法向父类构造函数传递参数,因为原型链继承时不会调用父类构造函数。

构造函数继承

  1. 定义一个父类构造函数,比如Parent,并在其内部定义一些属性。
  2. 定义一个子类构造函数,比如Child
  3. 在子类构造函数内部,使用callapply方法调用父类构造函数,并将子类的this作为上下文传递给父类构造函数。 例如:Parent.call(this, arg1, arg2); 或 Parent.apply(this, [arg1, arg2]);
  4. 这样,在创建子类实例时,this会指向子类实例,同时父类构造函数中的属性会被添加到子类实例上。
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

function Child(name) {
  Parent.call(this, name);
}

构造函数继承的优缺点:

优点:(可以在子类构造函数中向父类构造函数传递参数;解决了原型链继承中引用类型属性共享的问题;)

  • 避免了原型链继承中的引用类型属性共享问题:每个子类实例都有自己的属性副本,不会相互影响。
  • 可以向父类构造函数传递参数:通过callapply方法可以在创建子类实例时传递参数给父类构造函数。

缺点:(方法都在构造函数中定义,因此函数无法复用;子类无法访问父类原型上的方法;)

  • 方法无法复用:每个子类实例都会有一份父类构造函数中的方法副本,如果方法很多,会导致内存浪费。
  • 子类无法访问父类原型上的方法:因为只是继承了父类的构造函数内部的属性和方法,并没有继承父类原型上的方法。

组合继承(原型链 + 构造函数)

组合继承是JavaScript中一种常见的继承模式,它将原型链继承和构造函数继承结合起来使用。简单来说,组合继承首先通过原型链让子类的原型指向父类的实例,这样子类就可以继承父类的原型方法;然后,在子类的构造函数中调用父类的构造函数,以此来继承父类的实例属性。

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

Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
  console.log(this.age);
};

构造函数继承的优缺点:

优点:(融合了原型链和构造函数的优点,可以继承原型链上的属性和方法,也能够保证引用类型属性的独立;可以向父类构造函数传参;)

  • 子类可以继承父类的原型方法,同时也能够拥有自己的构造函数属性。
  • 可以在子类构造函数中向父类构造函数传递参数,增加了灵活性。
  • 解决了原型链继承中引用类型属性共享的问题,以及构造函数继承中无法继承原型方法的问题。

缺点:(调用了两次父类构造函数,造成不必要的性能开销;)

  • 父类的构造函数会被调用两次:一次是在子类原型上创建父类实例时,另一次是在子类构造函数内部调用父类构造函数时。这导致了不必要的性能开销。
  • 子类的原型中会包含父类构造函数的实例属性,这些属性在子类实例中也会存在,造成了属性的重复。

总的来说,组合继承是一种比较全面的继承方式,但它的效率并不是最高的,因为存在资源的浪费。在实际应用中,可以根据具体需求选择更适合的继承策略。

原型式继承

原型式继承是一种基于原型的继承模式,它允许对象继承另一个对象的属性和方法。在JavaScript中,这种继承是通过设置一个对象的原型(prototype)来实现的。简单来说,如果一个对象A的原型是另一个对象B,那么A就可以访问B的所有属性和方法。

var parent = {
  name: 'Parent',
  sayName: function() {
    console.log(this.name);
  }
};

var child = Object.create(parent);
child.name = 'Child';
child.sayName(); // 输出 'Child'

原型式继承的优缺点:

优点:(简单,不依赖于构造函数;可以继承一个对象,并保持其原型;)

  • 简单:原型式继承的实现非常简单,易于理解和实现。
  • 高效:在原型链上的属性和方法可以被所有实例共享,减少了内存的使用。
  • 动态性:原型链是动态的,可以在运行时修改,提供了很高的灵活性。

缺点:(引用类型的属性仍然会共享;无法传递参数;)

  • 引用类型的共享问题:如果原型对象包含引用类型值(如数组或对象),那么所有实例都会共享这个引用类型值,一个实例的修改会影响到其他所有实例。
  • 创建子类实例时无法向父类构造函数传递参数:使用原型式继承时,无法像使用构造函数那样传递参数给父类构造函数。
  • 原型链查找效率问题:如果原型链过长,属性查找可能会变得低效,因为需要遍历整个原型链来找到属性或方法。

原型式继承在JavaScript中是一种非常自然的继承方式,但是它的使用需要谨慎,特别是在处理包含引用类型值的原型对象时。随着ES6中Class的引入,原型式继承的某些缺点可以通过更现代的继承模式来规避。

寄生式继承

寄生式继承是一种基于原型式继承的继承模式,其核心思想是在原型式继承的基础上,通过创建一个仅用于封装继承过程的函数来增强对象,该函数内部以某种方式增强对象,最后返回这个对象。

function createAnother(original) {
  var clone = Object.create(original); // 创建对象副本
  clone.sayHi = function() {           // 增强对象
    console.log('hi');
  };
  return clone;                        // 返回增强后的对象
}

var baseObj = {
  name: 'Base',
  sayName: function() {
    console.log(this.name);
  }
};

var another = createAnother(baseObj);
another.sayName(); // 输出 'Base'
another.sayHi();   // 输出 'hi'

寄生式继承的优缺点:

优点:(不依赖于构造函数,因此可以创建一个对象并增强其功能;)

  1. 增强了对象:可以在创建对象副本的过程中添加新的方法或属性,从而增强对象的功能。
  2. 灵活性:寄生式继承提供了比原型式继承更高的灵活性,因为可以在函数内部进行各种操作来定制对象。

缺点:(引用类型的属性仍然会共享;无法传递参数;创建的函数无法复用;)

  1. 函数复用问题:由于每次创建对象时都会创建一个新函数(如例子中的sayHi),因此无法复用函数,造成了一定的资源浪费。
  2. 原型链问题:与原型式继承一样,如果基础对象包含引用类型值,那么所有通过寄生式继承创建的对象都会共享这个引用类型值,可能会导致意外的修改。
  3. 不适合大规模继承:对于需要创建大量对象的情况,寄生式继承可能不是最佳选择,因为它在每次创建对象时都会进行操作,效率较低。

寄生式继承通常不单独使用,而是与其他继承模式(如寄生组合式继承)结合使用,以弥补其不足。在现代JavaScript开发中,ES6的类和继承语法已经提供了更清晰、更高效的继承方式。

寄生式组合继承

寄生式组合继承是一种结合了构造函数继承和原型链继承的继承模式。它通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype); // 创建原型副本
  prototype.constructor = subType;                    // 修正构造函数指向
  subType.prototype = prototype;                      // 赋值给子类原型
}

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

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

function SubType(name, age) {
  SuperType.call(this, name); // 构造函数继承,继承属性
  this.age = age;
}

inheritPrototype(SubType, SuperType); // 寄生式继承原型

SubType.prototype.sayAge = function() {
  console.log(this.age);
};

寄生式组合继承的优缺点:

优点:(- 解决了组合继承调用两次父类构造函数的问题;几乎是最理想的继承方式,避免了原型链和构造函数继承的缺陷;)

  1. 高效:只调用了一次SuperType构造函数,避免了在SubType.prototype上创建不必要的、多余的属性。
  2. 解决了原型链继承和构造函数继承的问题:通过构造函数继承属性,避免了引用类型的属性被所有实例共享的问题;通过原型链继承方法,实现了函数复用。

缺点:(实现较为复杂,理解起来有一定难度;)

  1. 理解复杂:寄生式组合继承的实现相对复杂,理解起来不如ES6的类继承直观。
  2. 需要额外的函数:为了实现这种继承模式,我们需要编写额外的函数(如inheritPrototype),增加了代码的复杂性。

寄生式组合继承是JavaScript中实现继承的一种非常普遍且高效的方式,尽管在现代JavaScript中,我们通常会使用ES6的类语法来简化继承的实现。