类与继承

124 阅读12分钟

主讲:麓一

面向过程(POP)和面向对象(OOP)

什么是面向对象编程

面向对象是一种编程思想,经常被拿来和面向过程比较。

其实说的简单点:

  • 面向过程 关注的重点是动词,是分析出解决问题需要的步骤,然后编写函数实现每个步骤,最后依次调用函数。
    • 例如:获取商品信息 => 下单 => 调起支付
  • 面向对象 关注的重点是主谓,是把构成问题的事物拆解为各个对象,而拆解出对象的目的也不是为了实现某个步骤,而是为了描述这个事物在当前问题中的各种行为。

面向对象的特点是什么?

  • 封装:让使用对象的人不考虑内部实现,只考虑功能使用,把内部的代码保护起来,只留出一些 api 接口供用户使用;
  • 继承:就是为了代码的复用,从父类上继承出一些方法和属性,子类也有自己的一些属性;
  • 多态:是不同对象作用于同一操作产生不同的效果。多态的思想实际上是把“想做什么”和“谁去做“分开;

比如下棋的过程:

面向过程是这样的:开局 => 白方下棋 => 棋盘展示 => 检查胜负 => 黑方下棋 => 棋盘展示 => 检查 => 胜负 => 循环

用代码表示可能是一连串函数的调用:

  • init();
  • whitePlay(); // 里面实现一遍下棋的操作
  • repaint(); // 棋盘展示
  • check();
  • blackPlay(); // 再单独实现一遍下棋的操作
  • repaint(); // 棋盘展示
  • check();

面向对象是这样的:棋盘.开局 => 选手.下棋 => 棋盘.重新展示 => 棋盘.检查胜负 => 选手.下棋 => 棋盘.重新展示 => 棋盘.检查胜负

用代码表示可能是这样的:

  • const checkerBoard = new CheckerBoard(); // CheckerBoard 类内部封账了棋盘的操作,比如初始化棋盘,检查胜负关系等
  • const whitePlayer = new Player(‘white’); // Player 类内部封装了各种玩家的操作,比如等待,落棋,悔棋
  • const blackPlayer = new Player(‘black’);
  • whitePlayer.start(); // start 方法的结束,内部封装了或者通过事件发布触发
  • checkerBoard.repaint(), checkerBoard.check() 的调用
  • blackPlayer.start();

你只需要调用 new 一个 player,然后调用 start 方法,也就是说我们只需要关注行为,而不需要知道内部到底做了什么。

而且如果要加一些新功能,比如悔棋,比如再加一个玩家,面向对象都很好扩展。

在上面的例子中,面向对象的特性是怎么表现出来的呢?

  • 封装:PlayerCheckerBoard 类,使用的时候并不需要知道内部实现了什么,只需要考虑暴露出的 api 的使用;
  • 继承:whitePlayerblackPlayer 都继承自 Player,都可以直接使用 Player 的各种方法和属性;
  • 多态:whitePlayer.start()blackPlayer.start() 下棋的颜色分别是白色和黑色;

什么时候适合使用面向对象

  • 面向过程:简单的场景下,协同人员较少;
  • 面向对象:中型或者大型项目中,协同人员较多,迭代频繁;
// 面向对象例子
class Food {
  cooked() {
    if (this.type) {
      console.log(`make the ${this.type} can be eat`);
    } else {
      console.error('should ensure food');
    }
  }
  eat() {
    console.log('eating food');
  }
}
class Pork extends Food {
  constructor() {
    super();
    this.type = 'pork';
  }
}

class Vegetable extends Food {}

class Person {}
class Chief extends Person {
  cook(meat) {
    meat.cooked();
  }
}
class Consumer extends Person {
  eat(food) {
    // todo
    food.eat();
  }
}

const chief = new Chief();
const consumer = new Consumer();
const recookMeat = new Pork();
chief.cook(recookMeat);
consumer.eat(recookMeat);

// 类和对象

class Car extends Object {
  // speed, monkey, 排量
  // drive, bug sell
}
class MockCar extends Object {}
// 动物。为什么动物,

let myCar = new Car();

JS 对象的创建

创建一个对象有哪些方法?

  • Object.create();
  • var obj = {};
  • new Object(); / new Function();

1、Object.create()

Object.create 创建了一个对象;

let p = Object.create(q); -> p.__proto__ = q; 就是 p 的原型指向了 q

当我们需要调用 p 对象的一个方法或者属性的时候,如果 p 上面没有,就会去 q 上找;

let q = {};

let p = Object.create(q);

console.log(p.__proto__ === q); // true
console.log(p.__proto__.__proto__ === Object.prototype); // true

2、var obj = {}

obj.__proto__ = Object.prototype;

Object.create() 对比:

let p = Object.create({}); 相当于:let p = Object.create(obj); 相当于:p.__proto__ = obj;

所以:p.__proto__.__proto__ = Object.prototype;

var obj = {};
// 相当于:obj.__proto__ = Object.prototype;
console.log(obj.__proto__ === Object.prototype); // true

let p = Object.create({});
// 相当于:
let obj1 = {};
let p1 = Object.create(obj1);
// 相当于:p1.__proto__ = obj1;
console.log(p1.__proto__.__proto__ === Object.prototype); // true

3、new Object(); / new Function();

  • 创建了一个对象;
  • 该对象的原型,指向了这个 Function(构造函数)的 prototype
  • 该对象实现了这个构造函数的方法;
  • 根据一些特定情况返回对象;
    • 如果没有返回值,则返回创建的对象;
    • 如果有返回值,是一个对象(除了 null),则返回该对象;
    • 如果有返回值,不是一个对象,则返回创建的对象;

Object.create 的参数,如何和 {} 的结果保持一致?

  • 使用 Object.create(Object.prototype) 相当于 直接定义一个 {} 对象;

Object.create(null) 有何问题?

  • 没有 Object 的原型方法;

new 的实现

function Person(name) {
  this.name = name;
  return {};
}

function newObj(Father) {
  if (typeof Father !== 'function') {
    throw new Error('new operator function the first param must be a function!');
  }

  var obj = Object.create(Father.prototype);
  var result = Father.apply(obj, Array.prototype.slice.call(arguments, 1));

  return result && typeof result === 'object' && typeof result !== null ? result : obj;
}

// 使用
newObj(Person, 'tom');
function Person(name, age) {
  this.name = name;
  this.age = age;
}

var p = new Person();
// Person 是构造函数
p.__proto__ = Person.prototype; // = { ..., constructor: Person };
Person.prototype.constructor = Person;
// p 本身没有 constructor,p.constructor 找到的是它的原型,原型上有 Person,所以 p.constructor => Person
p.constructor = Person;

// console.log(p.__proto__ === Person.prototype); // true
// console.log(Person.prototype.constructor === Person); // true
// console.log(p.constructor === Person); // true

继承

实现一个继承,主要就是两部分:

  • 使用父类构造函数的方法;
  • 让对象的原型链指向父类;

原型链继承

function Parent() {
  this.name = 'father';
}

Parent.prototype.getName = function () {
  console.log(this.name);
};

function Child() {}

// Child.prototype.__proto__ = Parent.prototype;
Child.prototype = new Parent();
Child.prototype.constructor = Child;

存在的问题:

  • 如果有属性是引用类型,一旦某个实例修改了这个属性,所有的都会被改;
  • 创建 Child 的时候不能传参;

构造函数继承

想办法把 Parent 上的属性和方法,添加到 Child 上面去,而不是都存在在原型对象上,防止被实例共享;

function Parent(actions, name) {
  this.actions = actions || ['eat', 'work', 'sleep'];
  this.name = name || 'parentName';
}

function Child(play) {
  Parent.apply(this, Array.prototype.slice.call(arguments, 1));

  this.play = play;
}

存在的问题:

  • 属性或者方法被继承的话,只能在构造函数中定义;
  • 如果方法在构造函数中定义了,那么每次创建实例都会创建一遍方法;

组合继承

function Parent(actions, name) {
  this.actions = actions || ['eat', 'work', 'sleep'];
  this.name = name || 'parentName';
}

Parent.prototype.work = function () {
  console.log(`${this.name} coding everyday.`);
};

function Child(play) {
  Parent.apply(this, Array.prototype.slice.call(arguments, 1));

  this.play = play;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

存在的问题:

  • 调用了两次构造函数,做了重复的操作;
    • 第一次是在构造函数继承的时候;
    • 第二次是在原型链继承的时候;

寄生组合式继承

// 方法1
function Parent(actions, name) {
  this.actions = actions || ['eat', 'work', 'sleep'];
  this.name = name || 'parentName';
}

Parent.prototype.work = function () {
  console.log(`${this.name} coding everyday.`);
};

function Child(play) {
  Parent.apply(this, Array.prototype.slice.call(arguments, 1));

  this.play = play;
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 方法2
function inherit(p) {
  // p 是一个对象,但不能是 null
  if (p === null) throw TypeError();

  // 如果 Object.create 存在,直接使用
  if (Object.create) {
    return Object.create(p);
  }

  var t = typeof p;
  if (t !== 'object' && t !== 'function') throw TypeError();

  // 定义一个空构造函数
  function f() {}

  // 将其原型属性设置为 p
  f.prototype = p;

  // 使用 f() 创建 p 的继承对象
  return new f();
}

function Parent(actions, name) {
  this.actions = actions || ['eat', 'work', 'sleep'];
  this.name = name || 'parentName';
}

Parent.prototype.work = function () {
  console.log(`${this.name} coding everyday.`);
};

function Child(play) {
  Parent.apply(this, Array.prototype.slice.call(arguments, 1));

  this.play = play;
}

// Child.prototype = Object.create(Parent.prototype);
Child.prototype = inherit(Parent.prototype);
Child.prototype.constructor = Child;

ES6 class 继承

class Parent {}

class Child extends Parent {
  constructor() {
    super();
    // this.xxx = xxx;
  }
}
  • super() 作为函数调用时,要求子类必须执行一次;
  • 因为子类自己的 this 对象,必须通过父类的构造函数完成;
  • es6 继承会继承静态的属性和方法;

补充

一、JS 中怎么创建对象

1、普通方式/工厂模式

  • 缺点:每一个新对象都要重新写一遍 colorstart 的赋值;
const Player = new Object();
Player.color = 'white';
Player.start = function () {
  console.log('white 下棋');
};

console.log(Player); // { color: 'white', start: [Function (anonymous)] }
Player.start(); // white 下棋
console.log(Player.constructor); // [Function: Object],不能判断类型

或者工厂模式这两种方式都无法识别对象类型,比如 Player 的类型只是 Object

function createObject(color) {
  const Player = new Object();
  Player.color = color;
  Player.start = function () {
    console.log('下棋');
  };
  return Player;
}

const red = createObject('red');
console.log(red);
red.start(); // 下棋
console.log(red.constructor); // [Function: Object]

缺点:无法判断类型。

2、构造函数/实例

通过 this 添加的属性和方法总是指向当前对象的,所以在实例化的时候,通过 this 添加的属性和方法都会在内存中复制一份,这样就会造成内存的浪费。

但是这样创建的好处是即使改变了某一个对象的属性或方法,不会影响其他的对象(因为每一个对象都是复制的一份)。

function Player() {
  this.color = 'red';
  this.start = function () {
    console.log(this.color);
  };
}
const p1 = new Player();
const p2 = new Player();

console.log(p1); // Player {color: "red", start: ƒ}
console.log(p1.constructor); // [Function: Player]
p1.start(); // red

console.log(p1.start === p2.start); // false,说明每生成一个实例,构造函数内部的方法都会重新开辟一块内存

优点:相比于普通模式和工厂模式,可以知道类型。

缺点:每生成一个实例,构造函数内部的方法都会重新开辟一块内存。

3、原型

通过原型继承的方法并不是自身的,我们要在原型链上一层一层的查找,这样创建的 好处是只在内存中创建一次,实例化的对象都会指向这个 prototype 对象

function Player() {
  this.color = 'red';
}

Player.prototype.start = function () {
  console.log(this.color);
};

const p1 = new Player();
const p2 = new Player();

console.log(p1.start === p2.start); // true
console.log(p1); // Player { color: 'red' }
console.log(p1.constructor); // [Function: Player]

优点:是只在内存中创建一次,实例化的对象都会指向这个 prototype 对象。

缺点:共享同一个原型。

4、静态属性

静态属性就是绑定在构造函数上的属性方法,需要通过构造函数访问。

比如我们想看一下一共创建了多少个玩家的实例:

function Player() {
  this.color = 'red';
  // total 就是静态属性
  if (!Player.total) {
    Player.total = 0;
  }
  Player.total++;
}

const p1 = new Player();
console.log(Player.total); // 1
const p2 = new Player();
console.log(Player.total); // 2

二、原型及原型链

1、在原型上添加属性或者方法有什么好处?

在构造函数内通过 this 添加方法的话,每生成一个对象,都会重新开辟一块内存空间,当对象变多之后,性能会变得很差:

function Player() {
  this.color = 'red';
  this.start = function () {
    console.log(this.color);
  };
}

但是通过在原型上添加只在内存中创建一次:

Player.prototype.xxx = function () {};
Player.prototype.xxx = function () {};
Player.prototype.xxx = function () {};

这种方式向原型对象添加属性或者方法的话,又显得非常麻烦,所以我们可以这样写:

Player.prototype = {
  start: function () {
    console.log('下棋');
  },
  revert: function () {
    console.log('悔棋');
  }
};

2、怎么找到 Player 的原型对象?

  • xxx.__proto__

  • Object.getPrototypeOf(xxx)

function Player(color) {
  this.color = color;
}

Player.prototype.start = function () {
  console.log(color + '下棋');
};

const p1 = new Player('white');
const p2 = new Player('black');

// __proto__ 指向实例的原型对象
console.log(p2.__proto__); // Player {}
console.log(Object.getPrototypeOf(p2)); // Player {},可以通过 Object.getPrototypeOf 来获取 __proto__
console.log(Player.prototype); // Player {}
console.log(p2.__proto__ === Player.prototype); // true

console.log(Player.__proto__); // [Function (anonymous)]
console.log(Player.prototype.constructor); // [Function: Player]
console.log(Player.prototype.constructor === Player); // true

可以看一下 prototype.png 原型的流程图:

原型-1.png

构造函数和实例原型4.png

3、new 关键字到底做了什么?

  1. 一个继承自 Player.prototype 的新对象 p1 被创建;
  2. p1.__proto__ 指向 Player.prototype,即 p1.__proto__ = Player.prototype;(继承)
  3. this 指向新创建的对象 p1
  4. 返回这个新对象 p1
    1. 未显式 return,返回新对象 p1
    2. 显式 return this,返回新对象 p1
    3. 显式 return 基本数据类型,则 this 指向保持原来的规则,返回新对象 p1
    4. 显式 retuen 对象类型,this 指向返回的对象,比如 { a: 1 },则返回这个对象 { a: 1 }
    5. return nullnull 比较特殊,null 的数据类型为对象,但是 this 指向保持原来的规则;

3.1 new 的过程

new 的过程包括以下四个阶段:

  1. 创建一个新对象;
    1. let obj = new Object();
  2. 这个新对象的 __proto__ 属性指向原函数的 prototype 属性;(即继承原函数的原型)
    1. 获取原函数:let FunctionConstructor = [].shift.call(arguments);
    2. obj.__proto__ = [FunctionConstructor].prototype;
  3. 将这个新对象绑定到此函数的 this 上;
    1. let resultObj = [FunctionConstructor].apply(obj, ...args);
    2. let resultObj = [FunctionConstructor].apply(obj, arguments);
  4. 返回新对象;
    1. return typeoj resultObj === "object" && resultObj !== null ? resultObj : obj;
    2. return resultObj instanceof Object ? resultObj : obj;

后面看一下怎么手写实现 new 函数:

3.2 代码实现

/* 
  1. 用 new Object() 的方式新建了一个对象 obj
  2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
  3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
  4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
  5. 返回 obj
*/

/* 
  new 实现的过程:
  1、创建一个新对象
  2、这个新对象的 __proto__ 属性指向原函数的 prototype 属性;(即继承原函数的原型)
  3、将这个新对象绑定到此函数的 this 上;
  4、返回这个新对象 p1
*/
function objectFactory() {
  // 1、创建一个对象
  let obj = new Object();

  // arguments 是类数组对象
  /* 
    Javascript 函数中的参数对象 arguments 是个对象,而不是数组。
    但它可以类似数组那样通过数字下表访问其中的元素,而且它也有 length 属性标识它的元素的个数。
    通常我们把它转换成数组用 Array 的 slice 函数,示例代码如下:
    function fn() { 
      var arr = Array.prototype.slice.call(arguments,0); 
    }
  */

  // 2、这个新对象的 __proto__ 属性指向原函数的 prototype 属性;(即继承原函数的原型)
  // let FunctionConstructor = Array.prototype.shift.call(arguments); // 返回的就是 Player
  // shift 改变原数组
  let FunctionConstructor = [].shift.call(arguments); // 返回的就是 Player
  console.log(FunctionConstructor); // [Function: Player]

  obj.__proto__ = FunctionConstructor.prototype;

  // 3、将这个新对象绑定到此函数的 this 上;
  let resultObj = FunctionConstructor.apply(obj, arguments);

  // return typeof resultObj === "object" && resultObj !== null ? resultObj : obj;

  // null instanceof Object => false
  // typeof null => Object
  // 4、返回这个新对象 p1
  return resultObj instanceof Object ? resultObj : obj;
}

function Player(name) {
  this.name = name;
  // return {
  //   name: "tom"
  // };

  // return 123;

  // return null;
}

const p1 = objectFactory(Player, '秋裤');
console.log(p1); // Player { name: '秋裤' }
console.log(p1.name); // 秋裤
  • 去掉注释
/* 
  new 实现的过程:
  1、创建一个新对象
  2、这个新对象的 __proto__ 属性指向原函数的 prototype 属性;(即继承原函数的原型)
  3、将这个新对象绑定到此函数的 this 上;
  4、返回这个新对象 p1
*/
function objectFactory() {
  // 1、创建一个对象
  let obj = new Object();

  // 2、这个新对象的 __proto__ 属性指向原函数的 prototype 属性;(即继承原函数的原型)
  let FunctionConstructor = [].shift.call(arguments); // 返回的就是 Player
  obj.__proto__ = FunctionConstructor.prototype;

  // 3、将这个新对象绑定到此函数的 this 上;
  let resultObj = FunctionConstructor.apply(obj, arguments);

  // 4、返回这个新对象 p1
  return resultObj instanceof Object ? resultObj : obj;
}

function Player(name) {
  this.name = name;
  // return {
  //   name: "tom"
  // };

  // return 123;

  // return null;
}

const p1 = objectFactory(Player, '秋裤');
console.log(p1); // Player { name: '秋裤' }
console.log(p1.name); // 秋裤

4、原型链到底是什么?

我们都知道当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。

原型链.png

举个例子:

Object.prototype.name = 'Object';

function Player() {}

Player.prototype.name = 'Player';

var p1 = new Player();

p1.name = 'Daisy';

// 查找 p1 对象中的 name 属性,因为上面添加了 name,所以会输出 Daisy
console.log(p1.name); // Daisy

delete p1.name; // 删除自己的 name 属性

// 删除了 p1.name,然后查找 p1 发现没有 name 属性,
// 就会从 p1 的原型 p1.__proto__ 中去找,也就是 Player.prototype,然后找到了 name,输出 Player
console.log(p1.name); // Player

delete Player.prototype.name; // 删除原型上的 name 属性

console.log(p1.name); // Object

这样一条通过 __proto__prototype 去连接的对象的链条,就是原型链

三、继承

1、原型链继承

原型链继承.png

function Parent(name) {
  this.name = name || 'Parent';
  this.eat = function () {
    console.log(111);
  };
}

Parent.prototype.actions = ['sing'];

function Child() {}

// 这样写只能继承 Parent 原型上的属性和方法,不能继承实例上的
Child.prototype = Parent.prototype;

const c1 = new Child('我是 c1');
console.log(c1.name); // undefined
console.log(c1.actions); // [ 'sing' ]
c1.eat(); // 此处也找不到 eat 方法,报错:TypeError: c1.eat is not a function

1.1 代码实现

function Parent(name) {
  this.name = name || 'Parent';
  this.actions = ['sing', 'jump', 'rap'];
}

function Child() {}

Child.prototype = new Parent('我是 Parent');
// 为了保证类型正确,我们需要将 Child.prototype.constructor 指向它原本的构造函数 Child
// 如果不写此行代码,new 出来的实例的 constructor 是指向 [Function: Parent]
Child.prototype.constructor = Child;

const c1 = new Child('我是 c1');
const c2 = new Child();
c1.actions.push('basketball');
console.log(c1.constructor); // [Function: Parent]   [Function: Child]
console.log(c1.name); // 我是 Parent
console.log(c1.actions); // ["sing", "jump", "rap", "basketball"]
console.log(c2.actions); // ["sing", "jump", "rap", "basketball"]

1.2 缺点

  1. 父类如果存在【引用类型】,其中一个实例如果改变了此变量,那么所有的实例都会共享;
  2. 无法传参给 Parent

2、构造函数继承

看到上面的问题 1,我们想一下该怎么解决呢?

能不能想办法把 Parent 上的属性方法,添加到 Child 上呢?而不是都存在原型对象上,防止被所有实例共享。

1.1 代码实现

针对问题 1 我们可以使用 call 来复制一遍 Parent 上的操作。

function Parent() {
  this.name = 'Parent';
  this.actions = ['sing', 'jump', 'rap'];
}

function Child() {
  Parent.call(this);
  // ==> 相当于拷贝父元素
  // this.name = 'Parent';
  // this.actions = ['sing', 'jump', 'rap'];
}

const c1 = new Child();
c1.actions.push('basketball');
console.log(c1.constructor); // [Function: Child]
console.log(c1.actions); // [ 'sing', 'jump', 'rap', 'basketball' ]
const c2 = new Child();
console.log(c2.actions); // [ 'sing', 'jump', 'rap' ]

针对问题 2 我们应该怎么传参呢?

function Parent(name, color) {
  this.name = name;
  this.color = color;
  this.actions = ['sing', 'jump', 'rap'];
  this.eat = function () {
    console.log(`${name} - eat`);
  };
}

function Child() {
  // arguments 是类数组对象,得用 apply,不能用 call
  Parent.apply(this, arguments);
}

const c1 = new Child('c1', 'red');
const c2 = new Child('c2', 'white');
console.log(c1);
console.log(c1.constructor); // [Function: Child]
/* 
  Child {
    name: 'c1',
    color: 'red',
    actions: [ 'sing', 'jump', 'rap' ],
    eat: [Function (anonymous)]
  }
*/

console.log(c2);
/* 
  Child {
    name: 'c2',
    color: 'white',
    actions: [ 'sing', 'jump', 'rap' ],
    eat: [Function (anonymous)]
  }
*/

console.log(c1.actions === c2.actions); // false
console.log(c1.eat === c2.eat); // false

1.2 缺点

  • 浪费内存:如果属性或者方法想被继承,只能在构造函数中定义。而如果方法在构造函数内定义了,那么每次创建实例都会创建一遍方法,多占一块内存;

3、组合继承

通过原型链继承我们实现了基本的继承,方法存在 prototype 上,子类可以直接调用。但是引用类型的属性会被所有实例共享,并且不能传参。

通过构造函数继承,我们解决了上面的两个问题:使用 call/apply 在子构造函数内重复一遍属性和方法创建的操作,并且可以传参了。

但是构造函数继承同样带来了一个问题,就是构造函数内重复创建方法,导致内存占用过多。

是不是突然发现原型链继承是可以解决方法重复创建的问题? 所以我们将这两种方式结合起来,这就叫做组合继承

组合继承.png

1.1 代码实现

/* 
  组合继承:原型链继承 + 构造函数继承
*/
function Parent(name, actions) {
  this.name = name;
  this.actions = actions;
}

Parent.prototype.eat = function () {
  console.log(`${this.name} - eat`);
};

function Child() {
  // 构造函数继承
  Parent.apply(this, arguments); // 第一次调用构造函数
}

// 原型链继承
Child.prototype = new Parent(); // 第二次调用构造函数
Child.prototype.constructor = Child;

const c1 = new Child('c1', ['eat']);
c1.actions.push('play');
const c2 = new Child('c2', ['run']);

console.log(c1); // Child { name: 'c1', actions: [ 'eat', 'play' ] }
console.log(c2); // Child { name: 'c2', actions: [ 'run' ] }

c1.eat(); // c1 - eat
c2.eat(); // c2 - eat

console.log(c1.eat === c2.eat); // true

1.2 缺点

  • 调用了两次构造函数,做了重复的操作
    • 第一次是在构造函数继承的时候;
    • 第二次是在原型链继承的时候;

4、寄生组合式继承

上面重复调用了 2 次构造函数,想一下,我们可以精简掉哪一步?

我们可以考虑让 Child.prototype 间接访问到 Parent.prototype

寄生组合继承.png

1.1 代码实现

function Parent(name, actions) {
  this.name = name;
  this.actions = actions;
}

Parent.prototype.eat = function () {
  console.log(`${this.name} - eat`);
};

function Child() {
  Parent.apply(this, arguments);
}

// Child.prototype = new Parent(); // 优化
// Child.prototype = Parent.prototype; // 不可以这样写,因为:在给 Child.prototype 添加新的属性或者方法后,Parent.prototype 也会随之改变
/* 
  Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__。
  返回值:一个新对象,带着指定的原型对象和属性。
*/
Child.prototype = Object.create(Parent.prototype);

/* 
  ! Object.create 的内部实现原理:

  let TempFunction = function () {};
  TempFunction.prototype = Parent.prototype;
  Child.prototype = new TempFunction();
*/

Child.prototype.constructor = Child;

const c1 = new Child('c1', ['eat']);
c1.actions.push('play');
const c2 = new Child('c2', ['run']);

Child.prototype.run = function () {
  console.log('run');
};
console.log('Child ==== ', Child.prototype);
console.log('Parent ===== ', Parent.prototype);

也许有的同学会问,为什么一定要通过桥梁的方式让 Child.prototype 访问到 Parent.prototype

直接 Child.prototype = Parent.prototype 不行吗?

答:不行!!

咱们可以来看一下:

function Parent(name, actions) {
  this.name = name;
  this.actions = actions;
}

Parent.prototype.eat = function () {
  console.log(`${this.name} - eat`);
};

function Child() {
  Parent.apply(this, arguments);
}

// Child.prototype = new Parent(); // 优化
Child.prototype = Parent.prototype; // 在给 Child.prototype 添加新的属性或者方法后,Parent.prototype 也会随之改变

Child.prototype.constructor = Child;

const c1 = new Child('c1', ['eat']);
c1.actions.push('play');
const c2 = new Child('c2', ['run']);

Child.prototype.run = function () {
  console.log('run');
};
console.log(Child.prototype); // {eat: ƒ, run: ƒ, constructor: ƒ}
console.log(Parent.prototype); // {eat: ƒ, run: ƒ, constructor: ƒ}

可以看到,在给 Child.prototype 添加新的属性或者方法后,Parent.prototype 也会随之改变,这可不是我们想看到的。

5、class 继承

1.1 代码实现

class Parent {
  constructor() {
    this.name = 'parent';
    this.actions = ['sing'];
    this.eat = function () {
      console.log('this is eat func');
    };
  }

  getName() {
    console.log('this is getName func');
  }
}

// 继承使用 extends
class Child extends Parent {
  constructor(name) {
    // console.log(super()); // super() 执行后返回的就是继承自 Parent 的实例
    /* 
      Child {
        name: 'parent',
        actions: [ 'sing' ],
        eat: [Function (anonymous)]
      }
    */
    super();

    this.name = name;
  }
}

const c1 = new Child('c1');
const c2 = new Child('c2');
c1.actions.push('rap');
console.log(c1.name); // c1
console.log(c2.name); // c2
console.log(c1.actions); // [ 'sing', 'rap' ]
console.log(c2.actions); // [ 'sing' ]
console.log(c1.eat === c2.eat); // false
console.log(c1.getName === c2.getName); // true