承继概念的探究

说到承继的概念,首要要说一个经典的例子。

先界说一个类(Class)叫轿车,轿车的特点包括颜色、轮胎、品牌、速度、排气量等,由轿车这个类能够派生出“轿车”和“卡车”两个类,那么能够在轿车的根底特点上,为轿车增加一个后备厢、给卡车增加一个大货箱。这样轿车和卡车便是不相同的,可是二者都属于轿车这个类,这样从这个例子中就能具体说明轿车、轿车以及卡车之间的承继联系。

承继能够使得子类别具有父类的各种办法和特点,比方上面的例子中“轿车” 和 “卡车” 分别承继了轿车的特点,而不需要再次在“轿车”中界说轿车已经有的特点。在“轿车”承继“轿车”的一起,也能够重新界说轿车的某些特点,并重写或覆盖某些特点和办法,使其获得与“轿车”这个父类不同的特点和办法。

JS 完成承继的几种办法

1、原生链承继

原型链承继是比较常见的承继办法之一,其间触及的结构函数、原型和实例,三者之间存在着必定的联系,即每一个结构函数都有一个原型目标,原型目标又包括一个指向结构函数的指针,而实例则包括一个原型目标的指针。

  function Parent1() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
  }
  function Child1() {
    this.type = 'child2';
  }
  Child1.prototype = new Parent1();
  console.log(new Child1());

原型链承继是比较常见的承继办法之一,其间触及的结构函数、原型和实例,三者之间存在着必定的联系,即每一个结构函数都有一个原型目标,原型目标又包括一个指向结构函数的指针,而实例则包括一个原型目标的指针。尽管父类的办法和特点都能够拜访,但其实有一个潜在的问题,分明我只改变了 s1 的 play 特点,为什么 s2 也跟着变了呢?原因很简略,因为两个实例运用的是同一个原型目标。它们的内存空间是同享的,当一个发生变化的时分,别的一个也随之进行了变化,这便是运用原型链承继办法的一个缺陷。

  var s1 = new Child1();
  var s2 = new Child2();
  s1.play.push(4);
  console.log(s1.play, s2.play);

2、结构函数承继

  function Parent1(){
    this.name = 'parent1';
  }
  Parent1.prototype.getName = function () {
    return this.name;
  }
  function Child1(){
    Parent1.call(this);
    this.type = 'child1'
  }
  let child = new Child1();
  console.log(child);  // 没问题
  console.log(child.getName());  // 会报错

能够看到最终打印的 child 在控制台显示,除了 Child1 的特点 type 之外,也承继了 Parent1 的特点 name。这样写的时分子类尽管能够拿到父类的特点值,处理了第一种承继办法的坏处。

问题:父类原型目标中一旦存在父类之前自己界说的办法,那么子类将无法承继这些办法。

3、组合承继

这种办法结合了前两种承继办法的优缺陷,结合起来的承继。

  function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
  }
  Parent3.prototype.getName = function () {
    return this.name;
  }
  function Child3() {
    // 第2次调用 Parent3()
    Parent3.call(this);
    this.type = 'child3';
  }
  // 第一次调用 Parent3()
  Child3.prototype = new Parent3();
  // 手动挂上结构器,指向自己的结构函数
  Child3.prototype.constructor = Child3;
  var s3 = new Child3();
  var s4 = new Child3();
  s3.play.push(4);
  console.log(s3.play, s4.play);  // 不互相影响
  console.log(s3.getName()); // 正常输出'parent3'
  console.log(s4.getName()); // 正常输出'parent3'

问题:经过注释咱们能够看到 Parent3 执行了两次,第一次是改变Child3 的 prototype 的时分,第2次是经过 call 办法调用 Parent3 的时分,那么 Parent3 多结构一次就多进行了一次性能开销,这是咱们不愿看到的。

4、原型式承继

ES5 里面的 Object.create 办法,这个办法接纳两个参数:一是用作新目标原型的目标、二是为新目标界说额定特点的目标(可选参数)。

  let parent4 = {
    name: "parent4",
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };
  let person4 = Object.create(parent4);
  person4.name = "tom";
  person4.friends.push("jerry");
  let person5 = Object.create(parent4);
  person5.friends.push("lucy");
  console.log(person4.name);//tom
  console.log(person4.name === person4.getName());//true
  console.log(person5.name);//parent4
  console.log(person4.friends);//["p1","p2","p3","jerry","lucy"]
  console.log(person5.friends);//["p1","p2","p3","jerry","lucy"]

经过 Object.create 这个办法能够完成一般目标的承继,不只仅能承继特点,相同也能够承继 getName 的办法。

第一个结果“tom”,比较简略了解,person4 承继了 parent4 的 name 特点,可是在这个根底上又进行了自界说。

第二个是承继过来的 getName 办法检查自己的 name 是否和特点里面的值相同,答案是 true。

第三个结果“parent4”也比较简略了解,person5 承继了 parent4 的 name 特点,没有进行覆盖,因此输出父目标的特点。

最终两个输出结果是相同的,其实 Object.create 办法是可认为一些目标完成浅复制的。

问题:多个实例的引证类型特点指向相同的内存,存在篡改的或许。

5、寄生承继

运用原型式承继能够获得一份目标目标的浅复制,然后运用这个浅复制的才能再进行增强,增加一些办法,这样的承继办法就叫作寄生式承继。

尽管其优缺陷和原型式承继相同,可是关于一般目标的承继办法来说,寄生式承继比较于原型式承继,还是在父类根底上增加了更多的办法。

   let parent5 = {
    name: "parent5",
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };
  function clone(original) {
    let clone = Object.create(original);
    clone.getFriends = function() {
      return this.friends;
    };
    return clone;
  }
  let person5 = clone(parent5);
  console.log(person5.getName());
  console.log(person5.getFriends());

person5 是经过寄生式承继生成的实例,它不只仅有 getName 的办法,并且能够看到它最终也拥有了 getFriends 的办法。person5 经过 clone 的办法,增加了 getFriends 的办法,从而使 person5 这个一般目标在承继进程中又增加了一个办法,这样的承继办法便是寄生式承继。

问题:优缺陷和原型式承继相同,可是关于一般目标的承继办法来说,寄生式承继比较于原型式承继,还是在父类根底上增加了更多的办法。

6、寄生组合式承继

结合第四种中提及的承继办法,处理一般目标的承继问题的 Object.create 办法,咱们在前面这几种承继办法的优缺陷根底上进行改造,得出了寄生组合式的承继办法,这也是所有承继办法里面相对最优的承继办法

  function clone (parent, child) {
    // 这儿改用 Object.create 就能够减少组合承继中多进行一次结构的进程
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
  }
  function Parent6() {
    this.name = 'parent6';
    this.play = [1, 2, 3];
  }
   Parent6.prototype.getName = function () {
    return this.name;
  }
  function Child6() {
    Parent6.call(this);
    this.friends = 'child5';
  }
  clone(Parent6, Child6);
  Child6.prototype.getFriends = function () {
    return this.friends;
  }
  let person6 = new Child6();
  console.log(person6);
  console.log(person6.getName());
  console.log(person6.getFriends());

寄生组合式承继办法,根本能够处理前几种承继办法的缺陷,较好地完成了承继想要的结果,一起也减少了结构次数,减少了性能的开销。

总结:

(1)第一种是以原型链的办法来完成承继,可是这种完成办法存在 的缺陷是,在包括有引证类型的数据时,会被所有的实例目标所同享, 简略形成修正的紊乱。还有便是在创立子类型的时分不能向超类型传 递参数。

(2)第二种办法是运用借用结构函数的办法,这种办法是经过在子 类型的函数中调用超类型的结构函数来完成的,这一种办法处理了不 能向超类型传递参数的缺陷,可是它存在的一个问题便是无法完成函 数办法的复用,并且超类型原型界说的办法子类型也没有办法拜访到。

(3)第三种办法是组合承继,组合承继是将原型链和借用结构函数 组合起来运用的一种办法。经过借用结构函数的办法来完成类型的属 性的承继,经过将子类型的原型设置为超类型的实例来完成办法的继 承。这种办法处理了上面的两种模式单独运用时的问题,可是因为我 们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构 造函数,形成了子类型的原型中多了很多不必要的特点

(4)第四种办法是原型式承继,原型式承继的首要思路便是根据已 有的目标来创立新的目标,完成的原理是,向函数中传入一个目标, 然后回来一个以这个目标为原型的目标。这种承继的思路首要不是为 了完成创造一种新的类型,只是对某个目标完成一种简略承继,ES5 中界说的 Object.create() 办法便是原型式承继的完成。缺陷与原 型链办法相同。

(5)第五种办法是寄生式承继,寄生式承继的思路是创立一个用于 封装承继进程的函数,经过传入一个目标,然后复制一个目标的副本, 然后目标进行扩展,最终回来这个目标。这个扩展的进程就能够了解 是一种承继。这种承继的优点便是对一个简略目标完成承继,假如这 个目标不是自界说类型时。缺陷是没有办法完成函数的复用。

(6)第六种办法是寄生式组合承继,组合承继的缺陷便是运用超类 型的实例做为子类型的原型,导致增加了不必要的原型特点。寄生式 组合承继的办法是运用超类型的原型的副本来作为子类型的原型,这 样就避免了创立不必要的特点。

上面的承继看见就想吐,并且还有人拷问这些东西,很烦!还好ES6出了class和extends ,咱们能够经过class创造类,运用extends完成类的承继。

class Person {
  constructor(name) {
    this.name = name
  }
  // 原型办法
  // 即 Person.prototype.getName = function() { }
  // 下面能够简写为 getName() {...}
  getName = function () {
    console.log('Person:', this.name)
  }
}
class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在结构函数,则需要在运用“this”之前首要调用 super()。
    super(name)
    this.age = age
  }
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功拜访到父类的办法

因为浏览器的兼容性问题,假如遇到不支持 ES6 的浏览器,那么就得运用 babel 这个编译东西,将 ES6 的代码编译成 ES5,让一些不支持新语法的浏览器也能运行。

function _possibleConstructorReturn (self, call) {
		// ...
		return call && (typeof call === 'object' || typeof call === 'function') ? call : self; 
}
function _inherits (subClass, superClass) { 
    // 这儿能够看到
	subClass.prototype = Object.create(superClass && superClass.prototype, { 
		constructor: { 
			value: subClass, 
			enumerable: false, 
			writable: true, 
			configurable: true 
		} 
	}); 
	if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 
}
var Parent = function Parent () {
	// 验证是否是 Parent 结构出来的 this
	_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
	_inherits(Child, _Parent);
	function Child () {
		_classCallCheck(this, Child);
		return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
}
	return Child;
}(Parent));

ES5/ES6 的承继除了写法以外还有什么区别?

1. ES5 的承继实质上是先创立子类的实例目标,然后再将父类的办法增加 到 this 上(Parent.apply(this))

2. ES6 的承继机制彻底不同,实质上是先创立父类的实例目标 this(所以必 须先调用父类的 super()法),然后再用子类的结构函数修正 this。

3. ES5 的承继时经过原型或结构函数机制来完成。

4. ES6 经过 class 关键字界说类,里面有结构办法,类之间经过 extends 关 键字完成承继。

5. 子类必须在 constructor 办法中调用 super 办法,否则新建实例报错。因为子类没有自己的 this 目标,而是承继了父类的 this 目标,然后对其进行加工。 假如不调用 super 办法,子类得不到 this 目标。

6. 留意 super 关键字指代父类的实例,即父类的 this 目标。

7. 留意:在子类结构函数中,调用 super 后,才可运用 this 关键字,否则 报错。