一文详解 JS 中常见继承方式

原型链继承

原型链

首先每个构造函数(其实是每个函数都有)都有一个原型对象,同时原型有一个属性回指构造函数,而实例内部有一个指针 [[prototype]]__ptoto__) 指向原型;

原型又可以看做是另一个构造函数的实例,那么对于原型来说,它也有一个内部指针 [[prototype]](即 __ptoto__)指向它的原型;

那么这个原型链什么时候到头呢?

实际上,任何函数的默认原型都是一个Object的实例,所以原型链的尽头就是 Object 原型的原型,即 Object.prototype.__ptoto__,我们可以用 Object.getPrototypeOf(Object.prototype) 获取,它的值为 null

原型链继承代码

原型链继承就是基于这个思想:通过把自己的原型指向另外一个构造函数的实例对象,从而"继承"这个对象上的属性、方法

  function SuperType() {
    this.property = true;
  }
  SuperType.prototype.getSuperValue = function() {
    return this.property;
  };
  function SubType() {
    this.subproperty = false;
  }
  // 让 SubType 这个构造函数的原型指向 SuperType 的实例
  SubType.prototype = new SuperType();
  SubType.prototype.getSubValue = function () {
    return this.subproperty;
  };
  let instance = new SubType();
  console.log(instance.getSuperValue()); // true

这个例子中实现继承的关键,是 SubType 没有使用默认原型,而是将其替换成了一个新的对象。这个新的对象恰好是 SuperType 的实例。这样一来,SubType 的实例不仅能从 SuperType 的实例中继承属性和方法,而且还与 SuperType 的原型挂上了钩。

问题

  • 原型中包含的引用值会在所有实例间共享;
  function SuperType() {
    this.colors = ["red", "blue", "green"];
  }
  function SubType() {}
  // 继承SuperType
  SubType.prototype = new SuperType();
  let instance1 = new SubType();
  instance1.colors.push("black");
  console.log(instance1.colors); // "red, blue, green, black"
  let instance2 = new SubType();
  console.log(instance2.colors); // "red, blue, green, black"
  • 子类型在实例化时不能给父类型的构造函数传参,所有类型的默认值都是相同的,不能够定制化;

借用构造函数继承

就是在子类构造函数中调用父类构造函数,使得子类的实例上也能拥有父类的相关属性。

来看下面的例子:

  function SuperType() {
    this.colors = ["red", "blue", "green"];
  }
  function SubType() {
    //继承SuperType
    SuperType.call(this);
  }
  let instance1 = new SubType();
  instance1.colors.push("black");
  console.log(instance1.colors); // "red, blue, green, black"
  let instance2 = new SubType();
  console.log(instance2.colors); // "red, blue, green"

子类可以向父类传递参数:

  function SuperType(name){
    this.name = name;
    this.sayName = function() {
      console.log(this.name);
    };
  }
  function SubType() {
    // 继承SuperType并传参
    SuperType.call(this, "Nicholas");
    // 实例属性
    this.age = 29;
  }
  let instance = new SubType();
  console.log(instance.name); // "Nicholas";
  console.log(instance.age);   // 29

问题

  • 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
  • 因为没有使用原型链,所以子类不能访问父类原型上定义的方法。

组合继承

组合模式综合了原型链模式和借用构造函数的模式;是 JavaScript 中最常用的继承模式。

总的来说就是:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。

  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;
  }
  // 调用父类构造函数 通过原型链 继承方法
  SubType.prototype = new SuperType();
  SubType.prototype.sayAge = function() {
    console.log(this.age);
  };
  let instance1 = new SubType("Nicholas", 29);
  instance1.colors.push("black");
  console.log(instance1.colors);   // "red, blue, green, black"
  instance1.sayName();               // "Nicholas";
  instance1.sayAge();                // 29
  let instance2 = new SubType("Greg", 27);
  console.log(instance2.colors);   // "red, blue, green"
  instance2.sayName();               // "Greg";
  instance2.sayAge();                // 27

问题

父类构造函数始终会被调用两次:一次在是创建子类原型时调用(用来继承方法),另一次是在子类构造函数中调用(用来继承属性)。

原型式继承

其实也就是 Object.create() 的模拟实现:

function createObj(o) {
  // 创建一个临时构造函数
  function F(){}
  // 将这个构造函数的原型指向传入的对象
  F.prototype = o;
  // 返回这个构造函数的实例
  return new F();
}
let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
// yetAnotherPerson 修改引用类型的值,会导致 person 的值也被修改
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends);   // "Shelby, Court, Van, Rob, Barbie"

原型式继承非常适合:你需要在两个对象之间共享属性,但是又不想创建一个构造函数,通过实例化这个构造函数来实现共享属性的时候。

问题

属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

  function createAnother(original){
    let clone = object(original);   // 通过调用函数创建一个新对象
    clone.sayHi = function() {      // 以某种方式增强这个对象
      console.log("hi");
    };
    return clone;              // 返回这个对象
  }
  let person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
  };
  let anotherPerson = createAnother(person);
  anotherPerson.sayHi();   // "hi"

问题

跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

寄生式组合继承

组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。

function Parent (name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child (name, age) {
  Parent.call(this, name);
  this.age = age;
}
// 关键的三步
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
var child1 = new Child('kevin', '18');
console.log(child1);

封装后:

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}
function prototype(child, parent) {
  var prototype = object(parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;
}
// 当我们使用的时候:
prototype(Child, Parent);

ES6 继承

ES6 中的继承使用 extends 关键字可以创建一个类,该类继承自另一个类;子类可以继承父类的属性和方法,并且还可以添加自己的属性和方法:

class Parent {
  constructor(name) {
    this.name = name;
  }
  
  sayHello() {
    console.log("Hello, " + this.name);
  }
}
class Child extends Parent {
  constructor(name, age) {
    super(name); // 调用父类的构造函数
    this.age = age;
  }
  
  sayAge() {
    console.log("I am " + this.age + " years old.");
  }
}
const child = new Child("Alice", 10);
child.sayHello(); // 输出:Hello, Alice
child.sayAge(); // 输出:I am 10 years old.

在上面的例子中,Parent 是一个父类,Child 是一个子类。Child 通过 extends 关键字继承了 Parent 的属性和方法。

Childconstructor 中,通过 super 关键字调用了父类的构造函数,以便初始化父类的属性。然后,Child 可以使用父类的方法 sayHello,并且还可以添加自己的方法 sayAge

总的来说,ES6 引入了 classextends 关键字,使得继承更加简洁和易于理解。

以上就是 JS 中常见的几种继承方式,小伙伴们都学会了吗~

全部评论

相关推荐

不愿透露姓名的神秘牛友
昨天 10:52
点赞 评论 收藏
分享
Java抽象带篮子:难蚌,点进图片上面就是我的大头😆
点赞 评论 收藏
分享
牛客717484937号:双飞硕没实习挺要命的
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务