继承
function Parent(name) {  this.name = name;  this.hobbies = [];}
Parent.prototype.printName = function () {  console.log("name: ", this.name);};
function Son(name) {}Son.prototype = new Parent();
// 这里传的参数是无效的const son1 = new Son("xxx");son1.printName(); // name:  undefinedson1.hobbies.push("son1-football");console.log("son1.hobbies: ", son1.hobbies); // ['son1-football']
// 只有在获取不存在的属性时,才会去原型链上查找,设置不存在的属性并不会这样做// 这里修改的不是原型链上的属性,而是给 son1 自己新添加了一个 name 属性// 因此这里并不会影响到另外的以 new Parent() 为原型的对象对 name 属性的获取son1.name = "xxx";son1.printName(); // name: xxx
const son2 = new Son("xxx");son2.hobbies.push("son2-basketball");console.log("son2.hobbies: ", son2.hobbies); // ['son1-football', 'son2-basketball']
son2.printName(); // name: undefined缺点:
- 
Son无法复用Parent的构造器;
- 
Son的实例对象由于没有独属于自身的属性,访问的都是原型上的属性,导致修改属性(特指引用类型的变量不改变引用的情况,比如:const arr = []; arr.push('xxx'))时会修改掉原型上的值,这样就会对其他共享此原型的对象造成影响;
借用构造函数继承
Section titled “借用构造函数继承”function Parent(name, hobbies) {  this.name = name;  this.hobbies = hobbies;}
Parent.prototype.printName = function () {  console.log("name: ", this.name);};
function Son(name, hobbies) {  Parent.call(this, name, hobbies);}
const son1 = new Son("xxx", []);son1.hobbies.push("son1-football");console.log("son1.hobbies: ", son1.hobbies); // ['son1-football']
// son1.printName() // error, 无法通过原型链去访问 printName 方法
const son2 = new Son("xxx", []);son2.hobbies.push("son2-basketball");console.log("son2.hobbies: ", son2.hobbies); // ['son2-basketball']优点:
- Son可以复用- Parent的构造器;
缺点:
- 由于没有使用原型链,Son只能继承Parent构造器中含有的属性或者方法,无法通过原型链去访问Parent.prototype...的方法;
一般来说借用构造函数继承这种方式很少单独使用。
它是结合 原型链继承 + 借用构造函数继承 两种方式的一种新方式。
// 组合式继承function Parent(name, hobbies) {  this.name = name;  this.hobbies = hobbies;}
Parent.prototype.printName = function () {  console.log("name: ", this.name);};
function Son(name, hobbies) {  Parent.call(this, name, hobbies);}
Son.prototype = new Parent();
const son1 = new Son("son1", []);son1.printName(); // son1son1.hobbies.push("son1-football");console.log("son1.hobbies: ", son1.hobbies); // ['son1-football']
console.log("son1: ", son1);
const son2 = new Son("son2", []);son2.printName(); // son2son2.hobbies.push("son2-basketball");console.log("son2.hobbies: ", son2.hobbies); // ['son2-basketball']优点:
缺点:
- 
son的实例和son.__proto__上具有重复的属性名和方法名,这是由于Son.prototype = new Parent()和Son中调用了Parent.call(this, xxx)造成的; 
这和ES5中Object.create()很类似。
Object.create(proto, [propertiesObject])方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
第一个参数proto指的就是这个新对象的__proto__需要指向的对象。
第二个参数是[propertiesObject],它是可选的,该参数和Object.defineProperties()的第二个参数相同。
同时注意,如果proto参数是原始值类型(null除外),则抛出一个 TypeError异常。
比如:
// Uncaught TypeError: Object prototype may only be an Object or null: 0Object.create(0);
// successObject.create(new Number(0));Object.create({});// successObject.create(null);接下来我们通过原型式继承来实现一下Object.create(proto)方法,它的思路就是创建一个临时对象,让临时对象的构造函数的prototype指向我们传入的proto。
function createObject(proto) {  function F() {}  F.prototype = proto;  return new F();}接下来应用一下:
function Person(name, hobbies) {  this.name = name;  this.hobbies = hobbies;}
Person.prototype.printName = function () {  console.log("name: ", this.name);};
const person = new Person("xxx", []);
const subPerson1 = createObject(person);subPerson1.hobbies.push("subPerson1-basketball");console.log(subPerson1.hobbies); // ['subPerson1-basketball']
// 只有在获取不存在的属性时,才会去原型链上查找,设置不存在的属性并不会这样做// 这里修改的不是原型链上的属性,而是给 subPerson1 自己新添加了一个 name 属性// 因此这里并不会影响到另外的以 person 为原型的对象对 name 属性的获取subPerson1.name = "xzq";subPerson1.printName(); // name:  xzq
const subPerson2 = createObject(person);subPerson2.hobbies.push("subPerson2-football");console.log(subPerson2.hobbies); // ['subPerson1-basketball', 'subPerson2-football']
// 这里并没有被影响到, 因为这里获取的是原型上的 name 属性subPerson2.printName(); // name:  xxx缺点:
- 和原型链继承有同样的问题,const obj = createObject(proto),obj没有独属于自身的属性,访问的都是proto的属性,导致修改属性(特指引用类型的变量不改变引用的情况,比如:const arr = []; arr.push('xxx'))时会修改掉proto的属性,这样就会对其他共享此proto的对象造成影响;
- 无法createObject的过程中对生成的对象进行自定义;
寄生式继承是与原型式继承差不多的思路,只不过再寄生式继承中会对对对象进行某种增强,比如增加某个方法,比如增加某个属性。
// 原型式继承function createObject(proto) {  function F() {}  F.prototype = proto;  return new F();}
// 寄生式继承function createAnotherObject(proto) {  const obj = createObject(proto);
  // 对生成的对象进行某种增强  obj.run = function () {};  obj.age = 18;  //...
  return obj;}其实这里我们就看的出来,寄生式继承只是将原型式继承进行了一层封装而已。
它相比于原型式继承多了一个优点,那就是能够对生成的对象进行自定义。
它同样具有原型式继承的缺点:
- 和原型链继承有同样的问题,const obj = createObject(proto),obj没有独属于自身的属性,访问的都是proto的属性,导致修改属性(特指引用类型的变量不改变引用的情况,比如:const arr = []; arr.push('xxx'))时会修改掉proto的属性,这样就会对其他共享此proto的对象造成影响;
寄生组合式继承
Section titled “寄生组合式继承”就是将 寄生式继承 和 组合继承 两种方式进行合并,摒弃它们的缺点,获得它们的优点。
function Parent(name, hobbies) {  this.name = name;  this.hobbies = hobbies;}
Parent.prototype.printName = function () {  console.log("name: ", this.name);};
function Son(name, hobbies) {  Parent.call(this, name, hobbies);}
// 只要关联上了父类的实例,那么都会出现`Son`的不同实例共享父类这个实例的属性的问题// Son.prototype = new Parent()
//  因此这里采用寄生式继承的思想
function F() {}// 如果直接将原型进行共享,会导致两个构造函数的原型对象相互影响// Son.prototype = Parent.prototypeF.prototype = Parent.prototype;// 这样就避免了需要额外将 Parent 实例化一次Son.prototype = new F();
const son1 = new Son("son1", []);son1.printName(); // son1son1.hobbies.push("son1-football");console.log("son1.hobbies: ", son1.hobbies); // ['son1-football']
console.log("son1: ", son1);
const son2 = new Son("son2", []);son2.printName(); // son2son2.hobbies.push("son2-basketball");console.log("son2.hobbies: ", son2.hobbies); // ['son2-basketball']优点:
- Son.prototype = new F()相比于- Son.prototype = new Parent():一方面,不用为了实现继承还需要额外去实例化- Parent,另一方面就是会让- Son.prototype上不会多出冗余的属性(重复的属性);
- Son可以复用- Parent构造函数;
- Son的实例可以通过- Parent的原型去访问对应的属性或者方法;
这里我们在对其进行一些封装:
function _extends(Son, Parent) {  // 1. 通过一个暂时的中间对象将 Son.prototype 和 Parent.prototype 关联起来
  // function F() {}
  /*    我们回忆一下 new 的过程:      1. 创建新对象 obj: {}      2. 将 obj.__proto__ 赋值为构造函数 F 的 prototype      3. 执行 const res = F.call(obj), 如果 res 的类型为引用类型, 那么直接返回 res, 否则返回 obj
    关键就是第二步, 这里我们的目的是要将 f 和 Parent 关联起来    如果你这么写了:      const f = new F()  那么它创建的实例对象 obj.__proto__ 指向的是原来 F.prototype 指向的那块空间      F.prototype = Parent.prototype 然后在这里我们将 F.prototype 指向另外的地方并不会改变 obj.__proto__ 的错误指向了  */  // F.prototype = Parent.prototype  // const f = new F()  // Son.prototype = f
  // 简写成如下
  Son.prototype = Object.create(Parent.prototype);
  // -----------------------------------
  // 2. 由于第一步 Son.prototype 指向了别人变的面目全非了, 因此这里我们需要将其进行整容, 让其保留一些原来的属性
  Son.prototype.constructor = Son;
  // 3. 通过 super 属性保存父类的引用  Son.prototype.super = Parent;
  // 4. 让构造函数也完成继承(继承静态属性)  Son.__proto__ = Parent;}
Parent.staticName = "static parent";
function Parent(name, age) {  this.name = name;  this.age = age;}
function Son(name, age, hobbies) {  this.super(name, age);  this.hobbies = hobbies;}
_extends(Son, Parent);
console.log("staticName from Parent: ", Son.staticName); // staticName from Parent:  static parent
const son = new Son("xzq", 18, ["足球"]);console.log("son: ", son); // Son { name: 'xzq', age: 18, hobbies: [ '足球' ] }ES6 Class 继承
Section titled “ES6 Class 继承”ES6中继承达到的效果其实和我们前面讲的寄生组合式继承类似,同样都是可以继承静态属性方法和实例属性方法,但是他们有本质性的区别。
ES6 规定,子类必须在constructor()方法中调用super(),否则就会报错。
这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。
如果不调用super()方法,子类就得不到自己的this对象。
为什么子类的构造函数,一定要调用super()?原因就在于 ES6 的继承机制,与 ES5 完全不同。
比如我们前面封装的_extends函数(寄生组合式继承),它的本质就是将Son.prototype和Parent.prototype关联起来,然后将Son.__proto__和Parent关联起来,前者达成实例属性的继承,后者达成静态属性的继承,然后将Parent保留Son.prototype.super上,方便我们在Son中复用父类的构造函数,然后Son在添加自己的属性或者方法。
然后结合new的原理,其实这里只创建了一个this,那就是专属于Son的this,这里你会发现,其实调用super的原因就是为了复用父类的构造函数,简化代码。
因此super实际上你调与不调都可以,只不过如果不调用super的话你想拥有父类构造函数中的属性要自己手动写罢了;
但是在ES6中super是必须要调用的,而且必须在第一行调用,不然会报错:
class Parent {  constructor(name, age) {    this.name = name;    this.age = age;  }}
class Son extends Parent {  constructor(hobbie) {    // success    // super()    this.hobbie = hobbie;    // error    // super()  }}
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructorconst son = new Son("踢足球");这是因为ES6 的继承机制,本质上先实例化一个父类的实例对象,然后再将该对象作为子类的实例,也就是说在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,必须依赖父类实例的产生。
// 伪代码function _extends(Son, Parent) {  // 省略其他继承逻辑  // ...
  // ES6 的继承方式中先调用了 Parent  const parent = new Parent(...)  Son.call(parent, ...)}这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类。
这意味着新建子类实例时,父类的构造函数必定会先运行一次。
这里发现原型链继承,原型式继承,寄生式继承,都会带来同一个问题:
那就是Son的实例由于基本没有属于自身的属性,因此Son的实例获取到的属性值基本都是从其原型上拿到的,因此一般对属性的修改都会修改原型上的属性,导致其他共享这一原型的对象同样遭受影响。
以原型链继承的例子为例:
function Parent(name, hobbies) {  this.name = name;  this.hobbies = hobbies;}
Parent.prototype.printName = function () {  console.log("name: ", this.name);};
function Son(name) {}Son.prototype = new Parent("parent", []);
// 这里传的参数是无效的const son1 = new Son("xxx");son1.printName(); // name:  parentson1.hobbies.push("son1-football");console.log("son1.hobbies: ", son1.hobbies); // ['son1-football']
// 只有在获取不存在的属性时,才会去原型链上查找,设置不存在的属性并不会这样做// 这里修改的不是原型链上的属性,而是给 son1 自己新添加了一个 name 属性// 因此这里并不会影响到另外的以 new Parent() 为原型的对象对 name 属性的获取son1.name = "xxx";son1.printName(); // name: xxx
const son2 = new Son("xxx");son2.hobbies.push("son2-basketball");console.log("son2.hobbies: ", son2.hobbies); // ['son1-football', 'son2-basketball']
son2.printName(); // name: parent由于这里的Son的实例是没有对应的自身的属性,不同的实例获取值时,获取的都是new Parent()或者Parent.prototype上的属性或者方法。
然后在设置值时,如果当前实例对象上没有该属性,那么就会在当前实例对象上新增一个属性,而不是去原型链寻找,然后去设置:
son1.printName(); // name:  parentson1.name = "xxx";son1.printName(); // name: xxx
son2.printName(); // name: parent请看下面的例子,你是否觉得很奇怪,不是说设置值时不会去寻找对应的原型上的值而后进行设置吗?那下面的例子又作何解释:
son1.hobbies.push("son1-football");console.log("son1.hobbies: ", son1.hobbies); // ['son1-football']
son2.hobbies.push("son2-basketball");console.log("son2.hobbies: ", son2.hobbies); // ['son1-football', 'son2-basketball']这是因为在hobbies.push方法内部会先通过获取的方式去获取hobbies的值,而hobbies在当前实例上并没有,因此获取的就是对应的原型上的值,然后对这个值进行操作。
再结合hobbies是一个引用类型的值(数组),而push操作并不会改变这个数组的引用,因此就会影响到另外的实例对hobbies的访问。
用下面的例子来验证我们的说法:
// 直接通过赋值运算符来赋值// 此时 son1 对象上会新增一个 hobbies 属性, 值为: ['son1-football']son1.hobbies = ["son1-football"];console.log("son1.hobbies: ", son1.hobbies); // ['son1-football']
// son2 访问的依旧是原型对象上的 hobbiesconsole.log("son2.hobbies: ", son2.hobbies); // []https://es6.ruanyifeng.com/#docs/class#new-target-%E5%B1%9E%E6%80%A7