JavaScript实现继承

JavaScript 中的继承有以下六种

原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针

function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function() {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

// inherit from SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function() {
  return this.subproperty;
};

let instance = new SubType();
console.log(instance.getSuperValue()); // true

console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true

这种继承的问题

function SuperType() {
  this.colors = ["red", "blue", "green"];
}

function SubType() {}

// inherit from 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"

明明我只改变了 instance1 的 colors 属性,为什么 instance2 也跟着变了呢?原因很简单,因为两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点

子类有时候需要覆盖父类的方法,或者增加父类没有的方法,为此,这些方法必须在原型赋值之后再添加到原型上

function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function() {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

// inherit from SuperType
SubType.prototype = new SuperType();

// new method
SubType.prototype.getSubValue = function() {
  return this.subproperty;
};

// override existing method
SubType.prototype.getSuperValue = function() {
  return false;
};

let instance = new SubType();
console.log(instance.getSuperValue()); // false

盗用构造函数

function SuperType() {
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.getColors = function() {
  return this.colors;
};

function SubType() {
  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"

instance2.getColors(); // instance2.colors is not a function

这种方式解决了第一种继承方式的弊端,但问题是,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法

因此,从上面的结果就可以看到构造函数实现继承的优缺点,它使父类的引用属性不会被共享,优化了第一种继承方式的弊端;但是随之而来的缺点也比较明显——只能继承父类的实例属性和方法,不能继承原型属性或者方法。

上面的两种继承方式各有优缺点,那么结合二者的优点,于是就产生了下面这种组合的继承方式

组合继承

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.constructor = SubType;

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

但是这里又增加了一个新问题:通过注释我们可以看到 SuperType 执行了两次,第一次是改变 SubType 的 prototype 的时候,第二次是通过 call 方法调用 SuperType 的时候,那么 SuperType 多构造一次就多进行了一次性能开销,这是我们不愿看到的。

那么是否有更好的办法解决这个问题呢?

上面介绍的更多是围绕着构造函数的方式,那么对于 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 = SuperType.prototype;
// 手动挂上构造器,指向自己的构造函数
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function() {
  console.log(this.age);
};

let instance1 = new SubType("Nicholas", 29);

let instance2 = new SubType("Greg", 27);

console.log(instance1);

这时你会发现子类实例的构造函数是 SuperType,显然这是不对的,应该是 SubType

原型式继承

普通对象使用 ES5 里面的 Object.create 方法实现继承:

let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
  getName: function() {
    return this.name;
  },
};

let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = Object.create(person);
yetAnotherPerson.friends.push("Barbie");

console.log(anotherPerson.name); //"Greg"
console.log(anotherPerson.name === anotherPerson.getName()); //true
console.log(yetAnotherPerson.name); // "Nicholas"
console.log(anotherPerson.friends); //["Shelby", "Court", "Van", "Rob", "Barbie"]
console.log(yetAnotherPerson.friends); //["Shelby", "Court", "Van", "Rob", "Barbie"]

第一个结果“Linda”,比较容易理解,anotherPerson 继承了 person 的 name 属性,但是在这个基础上又进行了自定义。

第二个是继承过来的 getName 方法检查自己的 name 是否和属性里面的值一样,答案是 true。

第三个结果“Nicholas”也比较容易理解,yetAnotherPerson 继承了 person 的 name 属性,没有进行覆盖,因此输出父对象的属性。

最后两个输出结果是一样的,这里涉及浅拷贝的知识点,关于引用数据类型“共享”的问题,其实 Object.create 方法是可以为一些对象实现浅拷贝的。

那么关于这种继承方式的缺点也很明显,多个实例的引用类型属性指向相同的内存,存在篡改的可能,接下来我们看一下在这个继承基础上进行优化之后的另一种继承方式——寄生式继承

寄生式继承

使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。

let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
  getName: function() {
    return this.name;
  },
};
function createAnother(original) {
  let clone = Object.create(original); // create a new object by calling a function
  clone.sayHi = function() {
    // augment the object in some way
    console.log("hi");
  };
  clone.getFriends = function() {
    console.log(this.friends);
  };
  return clone; // return the object
}
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
anotherPerson.getFriends(); // ["Shelby", "Court", "Van"]

通过上面这段代码,我们可以看到 anotherPerson 是通过寄生式继承生成的实例,它不仅仅有 sayHi 的方法,而且可以看到它最后也拥有了 getFriends 的方法,结果如下图所示

anotherPerson 通过 createAnother 的方法,增加了 getFriends 的方法,从而使 anotherPerson 这个普通对象在继承过程中又增加了一个方法,这样的继承方式就是寄生式继承。

上面第三种组合继承方式中提到了一些弊端,即两次调用父类的构造函数造成浪费,下面要介绍的寄生组合继承就可以解决这个问题。

寄生式组合继承

结合第四种中提及的继承方式,解决普通对象的继承问题的 Object.create 方法,我们在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式,代码如下

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); // second call to SuperType()

  this.age = age;
}

function inheritPrototype(subType, superType) {
  let prototype = Object.create(superType.prototype); // create object
  prototype.constructor = subType; // augment object
  subType.prototype = prototype; // assign object
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
  console.log(this.age);
};

let instance = new SubType(ygj, 20);
console.log(instance);
instance.sayName(); //"ygj"
instance.sayAge(); //20

通过这段代码可以看出来,这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销

ES6 中的 extends 实现继承

上面六种继承方式中,寄生组合式继承是这六种里面最优的继承方式。另外,ES6 还提供了继承的关键字 extends

利用 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,让一些不支持新语法的浏览器也能运行

最后 extends 编译成的样子

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);

从上面编译完成的源码中可以看到,它采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式

最后

使用继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。

引用小黄书中关于上面的原型继承和类继承

在软件架构中你可以选择是否使用类和继承设计模式。大多数开发者理所当然地认为类是唯一(合适)的代码组织方式,但是本章中我们看到了另一种更少见但是更强大的设计模式:行为委托。行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的[[Prototype]] 机制本质上就是行为委托机制。也就是说,我们可以选择在 JavaScript 中努力实现类机制(参见第 4 和第 5 章),也可以拥抱更自然的 [[Prototype]] 委托机制。

当你只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。

对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把它们抽象成类。对象关联可以用基于 [[Prototype]] 的行为委托非常自然地实现。

委托机制是指通过 Object.create()来创建委托对象

原文链接:https://juejin.cn/post/7337867671096852518 作者:跌倒的小黄瓜

(0)
上一篇 2024年2月22日 上午10:41
下一篇 2024年2月22日 上午10:53

相关推荐

发表回复

登录后才能评论