likes
comments
collection
share

ES5、ES6常见的7种继承

作者站长头像
站长
· 阅读数 59

继承

继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

构造函数

一个函数可以作为创建对象实例的构造函数,也可以当做普通函数使用,区别就是在于是否使用了 new 关键字。

构造函数在创建对象的时候,会执行如下的操作:

  1. 在内存中创建一个对象
  2. 在新对象内部的 [[Prototype]] 属性指向构造函数的 prototype 属性
  3. 构造函数内部的 this 指向这个创建的新对象
  4. 执行构造函数内部的代码,比如添加一些属性、方法等
  5. 如果构造函数有返回值,该返回值为非空对象,则返回该对象,否则返回创建的新对象。

继承方式(ES5)

下面是一些 ES5 中常见的继承。

1、原型链继承

核心:

将父类的实例作为子类的原型。

SubType.prototype = new SuperType();
// 要修改子类构造函数的指向,否则子类实例的构造函数会指向SuperType。
SubType.prototype.constructor = SubType;

// 不能通过对象字面量的方式添加新方法
SubType.prototype.getSubValue = function () {
    return this.subproperty; 
};

优点:

  • 简单。
  • 父类方法可以复用。

缺点:

  • 子类之间会共享引用类型属性。
  • 创建子类时,无法向父类构造函数传参, 这样就会使子类实例没法自定义自己的属性。

举例🌰:

// 创建子类Child,使用原型和构造函数的方式继承父类People的方法,并调用say函数说出姓名和年龄。

// 父类:
function People(name, age) {
  this.name = name;
  this.age = age;
  this.arr = [1];
  this.say = function () {
    console.log('我的名字是:' + this.name + ',我今年' + this.age + '岁。');
  };
}
People.prototype.sayHi = function () {
  console.log('Hi');
};

// 子类:
function Child(name, age) {
  this.name = name;
  this.age = age;
}

// 原型链继承:
Child.prototype = new People();
// 所有涉及到原型链继承的继承方式都要修改子类构造函数的指向,否则子类实例的构造函数会指向People
Child.prototype.constructor = Child;

let child = new Child('Rainy', 20);
child.say(); // 我的名字是:Rainy,我今年20岁。
child.arr.push(2);
console.log(child.arr); // [ 1, 2 ]

let child1 = new Child('Jack', 23);
child1.say(); // 我的名字是:Jack,我今年23岁。
console.log(child1.arr); // [ 1, 2 ]
child1.sayHi(); // Hi

// instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
// instanceof 可以在继承关系中用来判断一个实例是否属于它的父类型
console.log(child instanceof Child); // true
console.log(child instanceof People); // true

2、构造函数(经典继承)

核心:

将父类构造函数的内容复制给了子类的构造函数。这是所有继承中唯一一个不涉及到 prototype 的继承。

优点:

  • 避免了引用类型的属性被所有实例共享。
  • 子类可以向父类传参。

缺点:

  • 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
  • 无法复用父类的公共函数。
  • 每次子类构造实例都得执行一次父类函数。

举例🌰:

// 父类:
function People(name, age) {
  this.name = name;
  this.age = age;
  this.arr = [1];
  this.say = function () {
    console.log('我的名字是:' + this.name + ',我今年' + this.age + '岁。');
  };
}

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

// 子类:
function Child(name, age) {
  // 构造函数继承(经典继承):
  People.call(this, name, age);
}

let child = new Child('Rainy', 20);
child.say(); // 我的名字是:Rainy,我今年20岁。
child.arr.push(2);
console.log(child.arr); // [ 1, 2 ]

let child1 = new Child('Jack', 23);
child1.say(); // 我的名字是:Jack,我今年23岁。
console.log(child1.arr); // [ 1 ]

// 父类的方法(构造函数外定义的方法)不能复用
child.sayName(); // Error: child.sayName is not a function

3、组合继承(伪经典继承)

核心:

原型式继承和构造函数继承的组合,兼具了二者的优点。

基本的思路是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

优点:

  • 可以在创建子类实例时向父类构造函数传参。
  • 父类的方法可以被复用。
  • 父类的引用属性不会被共享。

缺点:

  • 调用了两次父类的构造函数,第一次是 Child.prototype = new Parent(),第二次是 Parent.call(this, name),从而覆盖了子类原型中的同名参数。这种被覆盖的情况造成了性能上的浪费。

举例🌰:

// 父类:
function People(name, age) {
  this.name = name;
  this.age = age;
  this.arr = [1];
  this.say = function () {
    console.log('我的名字是:' + this.name + ',我今年' + this.age + '岁。');
  };
}

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

// 组合继承(伪经典继承):
// 子类:
function Child(name, age) {
  // 第二次调用父构造函数People
  People.call(this, name, age); 
}

// 第一次调用父构造函数People
Child.prototype = new People(); 
Child.prototype.constructor = Child;

let child = new Child('Rainy', 20);
child.say(); // 我的名字是:Rainy,我今年20岁。
child.arr.push(2);
console.log(child.arr); // [ 1, 2 ]

let child1 = new Child('Jack', 23);
child1.say(); // 我的名字是:Jack,我今年23岁。
console.log(child1.arr); // [ 1 ]

delete child.arr;
console.log(child.arr); // [ 1 ]

// 子类可以调用父构造函数外定义的方法
child.sayName(); // Rainy

4、原型式继承:

核心:

原型式继承的 object 方法本质上是对参数对象的一个浅复制。

原型式继承并没有使用严格意义上的构造函数,是通过借助原型基于已有的对象创建新对象,同时还不必创建自定义类型。本质上就是对传入的对象进行了一次浅复制。

优点:

  • 父类方法可以复用。

缺点:

  • 父类的引用属性会被所有子类实例共享。
  • 子类构建实例时不能向父类传递参数。

举例🌰:

// 核心代码
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

let person = {
  name: 'Tom',
  list: [1, 2, 3]
};

let p1 = object(person);

// ES5通过新增 Object.create() 方法规范化了原型式继承
// let p1 = Object.create(person);

p1.name = 'Jack';
p1.list.push(4);
console.log(person.name); // Tom
console.log(p1.name); // Jack

let p2 = Object.create(person, {
  name: { value: 'Helen' }
});
p2.list.push(5);
console.log(p2.name); // Helen
console.log(person.list); // [ 1, 2, 3, 4, 5 ]

delete p2.name;
console.log(p2.name); // Helen

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

var p1 = object(person); 等同于 var p1 = Object.create(person);

5、寄生式继承:

核心:

使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。

优点:

  • 寄生式继承在主要考虑对象而不是创建自定义类型和构造函数时,是十分有用的。

缺点:

  • 跟构造函数继承模式一样,每次创建对象都会创建一遍方法。(使用寄生式继承来为对象添加函数,会由于不能做到函数复用而效率低下。)
  • 同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

举例🌰:

// 寄生式继承
function createPerson(o) {
  // 核心代码
  var clone = Object.create(o);
  clone.sayHi = function () {
    console.log('Hi');
  };
  return clone;
}

var person = {
  name: 'Tom',
  age: [1, 2, 3]
};

var p1 = createPerson(person);
p1.sayHi(); // Hi
p1.name = 'Jack';
console.log(p1.name); // Jack
console.log(person.name); // Tom
p1.age.push(4);
console.log(person.age); // [ 1, 2, 3, 4 ]

6、寄生组合式继承

核心:

最佳的的继承方式,组合继承会调用两次父类构造函数,存在效率问题。其实本质上子类原型最终是要包含父类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

优点:

  • 不必为了指定子类型的原型而调用父类型的构造函数。
  • 这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。
  • 与此同时,原型链还能保持不变;因此,还能够正常使用 instanceofisPrototypeOf
  • 开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

缺点:

  • 太复杂了。

举例🌰:

// 寄生组合式继承
function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype); // 创建了父类原型的浅复制
  prototype.constructor = subType; // 修正原型的构造函数
  subType.prototype = prototype; // 将子类的原型替换为这个原型
}

// 父类:
function Animal(name) {
  this.name = name;
  this.arr = [1, 2, 3];
}
Animal.prototype.sayName = function () {
  console.log(this.name);
};

// 子类:
function Cat(name, age) {
  Animal.call(this, name);
  this.age = age;
}

// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(Cat, Animal);

Cat.prototype.sayAge = function () {
  console.log(this.age);
};
Cat.prototype.sayName = function () {
  console.log('catName');
};

var cats = new Cat('Tom', 3);
var animals = new Animal('Boss');
console.log(typeof animals.sayName); // function
console.log(typeof animals.sayAge); // undefined(Cat独有的方法)
animals.sayName(); // Boss
cats.sayName(); // catName
cats.sayAge(); // 3
console.log(cats.__proto__.prototype); // undefined

继承方式(ES6)

ES6 中的继承。

ES6 Class 继承

核心:

ES6 继承的结果和寄生组合继承相似,本质上,ES6 继承是一种语法糖。但是,寄生组合继承是先创建子类实例 this 对象,然后再对其增强;而 ES6 先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this

class A {}

class B extends A {
  constructor() {
    super();
  }
}

// 核心代码:
class A {}
class B {}

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
};

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

总结:

  • ES6 Class extendsES5 继承的语法糖。
  • JS 的继承除了构造函数继承之外都基于原型链构建的。
  • 可以用寄生组合继承实现 ES6 Class extends,但是还是会有细微的差别。

ES6 继承与 ES5 继承的异同:

  • 相同点:本质上 ES6 继承是 ES5 继承的语法糖。

  • 不同点:

    • ES6 继承中子类的构造函数的原型链指向父类的构造函数,ES5 中使用的是构造函数复制,没有原型链指向。

    • ES6 子类实例的构建,基于父类实例,ES5 中不是。

举例🌰:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return this.x + '-' + this.y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的 constructor(x, y)
    this.color = color;
  }

  // 调用父类的 toString()
  toString() {
    return this.color + ' ' + super.toString();
  }
}

let point = new Point(100, 100);
let colorPoint = new ColorPoint('1', '2', 'red');

console.log(point); // Point { x: 100, y: 100 }
console.log(colorPoint); // ColorPoint { x: '1', y: '2', color: 'red' }
point.toString(); // 100-100
colorPoint.toString(); // red 1-2

扩展

new()做了什么:

  • 创建一个新的空对象。
  • 将该对象的原型链链接到构造函数的原型对象上,使其继承构造函数的属性和方法。
  • 将构造函数中的 this 指向新创建的对象。
  • 执行构造函数内部的代码,给新对象添加属性和方法。
  • 如果构造函数没有返回其他对象,则返回新创建的对象;如果构造函数返回了一个非基本类型的值(对象),则返回这个对象,否则还是返回新创建的对象。
function myNew(fn, ...args) {
  // 创建一个新的空对象
  let target = {};

  // 将这个空对象的__proto__指向构造函数的原型
  target.__proto__ = fn.prototype;

  // 将this指向空对象
  let res = fn.apply(target, args);

  // 对构造函数返回值做判断,然后返回对应的值
  return res instanceof Object ? res : target;
}

super关键字:

  • 子类中存在 constructor 方法的时候,需要调用 super 方法,并且需要在使用 this 关键字之前调用
  • super 关键字可以用来调用父对象上的方法
  • 可以使用 super 来调用父对象上的静态方法
  • 不可以使用 delete 来删除 super 上的属性
  • 不可以复写 super 对象上的只读属性
转载自:https://juejin.cn/post/7273014178930131000
评论
请登录