JavaScript的继承方案,再也不怕面试了

148 阅读7分钟

笔记:该篇文章读自《JavaScript高级程序-第四版》记录一下;

该书不愧为JavaScript经典书籍,需时常拜读;

在JavaScript中继承主要分为两大类,第一是原型链方案继承,第二是原型式方案继承;

一、原型链方案继承

所谓的原型链方案继承就是通过原型链中实例,原型对象,构造函数的相互关系催生出的一系列的JavaScript继承方案;

原型链继承

基本思想就是通过原型集成多个引用类型的属性和方法;

构造函数,原型和实例的关系:每个构造函数都有一个原型对象([[Prototype]]),原型有一个属性指回构造函数(constructor),而实例有一个内部指针(proto)指向原型对象。 那如果原型是另一个类型的实例,就意味着这个原型本身就有一个内部指针(proto)指向另一个原型对象([[Prototype]]),相应的另一个原型也有一个指针指向另一个构造函数,这样就在实例和原型之间构造了一条原型链;

// 原型链继承
function Parent() {
  this.parentName = 'parent';
}
Parent.prototype.getParentName = function () {
  return this.parentName;
};
function Children() {
  this.childName = 'children';
}
Children.prototype = new Parent();
Children.prototype.getChildName = function () {
  return this.childName;
};
const child = new Children();
console.log(child.getParentName()); //parent

原型与继承的关系

原型与继承的关系有两种方式可以确定

instanceof方法

console.log(child instanceof Object); //true
console.log(child instanceof Parent); //true
console.log(child instanceof Children); //true

isPrototypeOf()方法

console.log(Object.prototype.isPrototypeOf(child)); //true
console.log(Parent.prototype.isPrototypeOf(child)); //true
console.log(Children.prototype.isPrototypeOf(child)); //true

原型链继承的缺点

1、子类在实例化的时候不能给父类的构造函数传递参数;

2、原型中出现引用值的问题的时候,引用至会在所有实例之间共享,这也是为什么属性通畅会在构造函数中定义而不会定义在原型上的原因,例如:

function Parent() {
  this.parentName = 'parent';
  this.colors = ['red', 'green', 'yellow'];
}
Parent.prototype.getParentName = function () {
  return this.parentName;
};
function Children() {
  this.childName = 'children';
}
Children.prototype = new Parent();
Children.prototype.getChildName = function () {
  return this.childName;
};
const child1 = new Children();
const child2 = new Children();
child1.colors.push('blue');
console.log(child1.colors); // ['red', 'green', 'yellow', 'blue']
console.log(child2.colors); // ['red', 'green', 'yellow', 'blue']
//只在child1实例的colors中添加了blue,但是在child2中寻找colors发现同样也被添加上了blue

盗用构造函数

为了解决原型继承中包含引用值导致的问题,社区中开始流行了一种叫做“盗用构造函数”的继承技术;基本思想就是在子类的构造函数中调用父类构造函数;因为函数就是就是在特定上下文中执行代码的简单对象,所以可以使用call()和apply()方法为新创建的对象改变this指向为上下文执行构造函数,同时传参;

解决传参问题

function Parent(name) {
  this.name = name;
}
function Children(name) {
  // 继承Parent,并且传参
  Parent.call(this, name);
  // 实例属性
  this.source = '英语书';
}
let child1 = new Children('lilei');
let child2 = new Children('hanmeimei');
console.log(child1.name); //lilei
console.log(child1.source); //英语书
console.log(child2.name); //hanmeimei
console.log(child2.source); //英语书

解决引用问题

function Parent() {
  this.colors = ['red', 'green', 'yellow'];
}
function Children() {
  // 继承Parent
  Parent.call(this);
}
let child1 = new Children();
let child2 = new Children();
child1.colors.push('blue');
console.log(child1.colors); //['red', 'green', 'yellow', 'blue']
console.log(child2.colors); //['red', 'green', 'yellow']

盗用构造函数的缺点:必须在构造函数中定义方法,因此函数是不能重用的;

组合继承(经典继承)

组合继承将原型链继承和盗用构造函数继承两者的有点集中了起来;

基本思想就是使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性;

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'green', 'yellow'];
}
Parent.prototype.getName = function () {
  return this.name;
};
function Children(name, sex) {
  // 继承Parent属性
  Parent.call(this, name);
  this.sex = sex;
}
// 继承方法
Children.prototype = new Parent();
Children.prototype.construct = Children;

Children.prototype.getSex = function () {
  return this.sex;
};
const child1 = new Children('lilei', '男');
child1.colors.push('blue');
console.log(child1.colors); // ['red', 'green', 'yellow', 'blue']
console.log(child1.getName()); //lilei
console.log(child1.getSex()); //男

const child2 = new Children('hanmeimei', '女');
console.log(child2.colors); // ['red', 'green', 'yellow']
console.log(child2.getName()); //hanmeimei
console.log(child2.getSex()); //女

组合继承弥补了原型链继承和盗用构造函数继承的不足,是JavaScript中使用最多的继承模式,而且组合继承也保留了instanceof和isPrototypeO的方法识别能力;

// instanceof和isPrototypeOf()方法也可以正常使用。
console.log(child instanceof Object); //true
console.log(child instanceof Parent); //true
console.log(child instanceof Children); //true

console.log(Object.prototype.isPrototypeOf(child)); //true
console.log(Parent.prototype.isPrototypeOf(child)); //true
console.log(Children.prototype.isPrototypeOf(child)); //true

二、原型式方案继承

原型式方案就是通过对原型的一种浅复制,将原型上的方法和属性复制一遍,然后将复制完成的对象给与一个变量,通过这种思想催生出一系列的JavaScript的继承方案;

原型式继承

2006年,Douglas Crockford写了一篇文章《JavaScript中的原型式继承》,这篇文章介绍了一种不涉及严格意义上的构造函数的继承方法。出发点是即使不定义类型也可以通过原型实现对象之间的信息共享。

文章给出了一个函数,也是Object.create()方法的实现方式;本质上是对传入的对象执行了一次浅复制;

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

接下来我们就不使用该方法,直接使用Object.create()方法直接进行代替;

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象之间可以共享信息的场景,但是本质上属性中包含的引用值始终会在相关对象之间进行共享,这个和使用原型模式是一样的。

const person = {
  name: 'lilei',
  friends: ['hanmeimei'],
};

const person1 = Object.create(person);
person1.name = 'dingxiang';
person1.friends.push('wanghuahua');
console.log(person1.name); //dingxiang
console.log(person1.friends); //['hanmeimei', 'wanghuahua']
const person2 = Object.create(person);
person1.name = 'zhanglili';
person1.friends.push('lisi');
console.log(person1.name); //zhanglili
console.log(person1.friends); //['hanmeimei', 'wanghuahua', 'lisi']Ï

缺点:和原型链继承缺点一致;

寄生式继承

在原型式继承延伸出的一种继承方式是寄生式继承;

基本思想类似于寄生构造函数和工厂模式,创建一个实现继承的函数,以某种方式增强对象,然后返回找个对象;

function create(o) {
  const clone = Object.create(o);
  clone.getName = function () {
    return this.name;
  };
  return clone;
}
const person = {
  name: 'lilei',
  friends: ['hanmeimei'],
};

const person1 = create(person);
console.log(person1.getName()); //lilei

缺点:寄生式继承同样适合不需要单独创建构造函数的场景,Object.create()并不是寄生式继承所需的,任何是返回新对象的函数都可以这样操作;

寄生组合式继承(继承终极解决方案)

组合继承其实是存在效率性能问题的,最主要的问题就是父类构造函数始终会被调用两次,一次是创建子类原型的时候调用,另一次是在子类构造函数中调用;将组合式继承代码拿过来可以看一下。

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'green', 'yellow'];
}
Parent.prototype.getName = function () {
  return this.name;
};
function Children(name, sex) {
  // 继承Parent属性
  第二次调用Parent(),设置子类实例原型
  Parent.call(this, name);
  this.sex = sex;
}
// 继承方法
// 第一次调用Parent()方法, 创建子类实例
Children.prototype = new Parent();
Children.prototype.construct = Children;

Children.prototype.getSex = function () {
  return this.sex;
};
const child1 = new Children('lilei', '男');

从代码中可以看到,是调用了两次Parent()父类的构造函数,这样是有效率问题的。所以从中又延伸出了一种寄生组合继承方案;

基本思想就是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。本质上就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型;

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'green', 'yellow'];
}
Parent.prototype.getName = function () {
  return this.name;
};
function Children(name, age) {
  // 只调用了一次Parent构造函数,,设置子类实例原型
  Parent.call(this, name);
  this.age = age;
}
const prototype = Object.create(Parent.prototype); //创建对象
prototype.constructor = Children; //增强对象
Children.prototype = prototype; //赋值对象

Children.prototype.getAge = function () {
  return this.age;
};
const child1 = new Children('lilei', '20');
child1.colors.push('blue');
console.log(child1.colors); // ['red', 'green', 'yellow', 'blue']
console.log(child1.name); // lilei
console.log(child1.getName()); // lilei
console.log(child1.age); // 20
console.log(child1.getAge()); // 20

const child2 = new Children('hanmeimei', '18');
console.log(child2.colors); // ['red', 'green', 'yellow']
console.log(child2.name); // hanmeimei
console.log(child2.getName()); // hanmeimei
console.log(child2.age); // 18
console.log(child2.getAge()); // 18

可以将创建对象,增强对象,赋值对象这一块逻辑封装起来

function inheritPrototype(Parent, child) {
  const prototype = Object.create(Parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;
}
inheritPrototype(Parent, Children);

可以看到这里只调用了一次Parent的构造函数,避免了Children.prototype上不必要也用不到的属性,所以可以说这个继承方案效率更高,而且原型链也可以保持不变,因此instanceof和isPrototypeOf()方法也可以正常使用。

// instanceof和isPrototypeOf()方法也可以正常使用。
console.log(child instanceof Object); //true
console.log(child instanceof Parent); //true
console.log(child instanceof Children); //true

console.log(Object.prototype.isPrototypeOf(child)); //true
console.log(Parent.prototype.isPrototypeOf(child)); //true
console.log(Children.prototype.isPrototypeOf(child)); //true

在JavaScript中,寄生式组合继承可以算是引用类型继承的最佳模式;