原文地址:https://github.com/YvetteLau/Blog/issues/37

关于【Step-By-Step】

Step-By-Step (点击进入项目) 是我于 2019-05-20 开始的一个项目,每个工作日发布一道面试题。
每个周末我会仔细阅读大家的答案,整理最一份较优答案出来,因本人水平有限,有误的地方,大家及时指正。


本周面试题一览:

  • 原型链继承的基本思路是什么?有什么优缺点?
  • 借用构造函数和组合继承基本思路是什么?有什么优缺点?
  • 原型式继承的基本思路是什么?有什么优缺点?
  • 寄生式继承的基本思路是什么?有什么优缺点?
  • 寄生组合式继承的基本思路是什么?有什么优缺点?

本周是继承专题,在开始之前,需要先了解构造函数、原型和原型链的相关知识。

构造函数

构造函数和普通函数的区别仅在于调用它们的方式不同,任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;任何函数,如果不通过 new 操作符来调用,那么它就是一个普通函数。

实例拥有 constructor(构造函数) 属性,该属性返回创建实例对象的构造函数。

functionPerson(name, age) {
    this.name= name;
    this.age= age;
}

var Yvette =newPerson('刘小夕', 20);
console.log(Yvette.constructor=== Person); //true

有一点需要说明的是,除了基本数据类型的 constructor 外( nullundefinedconstructor 属性),constructor 属性是可以被重写的。因此检测对象类型时,instanceof 操作符比 contsrutor 更可靠一些。

functionPerson(name) {
    this.name= name;
}
functionSuperType() { }
var Yvette =newPerson('刘小夕');
console.log(Yvette.constructor); //[Function: Person]Yvette.constructor= SuperType;
console.log(Yvette.constructor); //[Function: SuperType]

原型

我们创建的每个函数都有 prototype 属性,这个属性指向函数的原型对象。原型对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

在默认情况下,所有原型对象都会自动获得一个 constructor 属性,这个属性包含一个指向 prototype 属性所在函数的指针。

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象(可以通过实例的 __proto__ 来访问构造函数的原型对象)。

functionPerson(name) {
    this.name= name;
}
Person.prototype.sayName=function() {
    console.log(this.name);
}
var person1 =newPerson('刘小夕');
var person2 =newPerson('前端小姐姐');
//构造函数原型对象上的方法和属性被实例共享person1.sayName();
person1.sayName(); 

xuehuayu.cn

实例.__proto__ === 构造函数.prototype

console.log(Object.prototype.__proto__===null)  //trueconsole.log(Object.__proto__===Function.prototype) //trueconsole.log(Function.prototype.__proto__===Object.prototype) //true

原型链

简单回顾一下构造函数、原型和实例的关系:

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个可以执行原型对象的内部指针(可以通过 __proto 访问)。

假如我们让原型对象等于另一个类型的实例,那么此时原型对象包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。加入另一个原型又是另一个类型的实例,那么上述关系仍然成立,如此层层递进,就构成了实例与原型的链条,这就是原型链的基本概念。

functionSuperType() {
    this.type='animal';
}
SuperType.prototype.getType=function() {
    console.log(this.type);
}
functionSubType() {

}
SubType.prototype=newSuperType();
SubType.prototype.sayHello=function() {
    console.log('hello');
}
functionSimType(name) {
    this.name= name;
}
SimType.prototype=newSubType();
SimType.prototype.sayHi=function() {
    console.log('hi');
}
var instance =newSimType('刘小夕');
instance.getType();

一图胜万言:

xuehuayu.cn

调用 instance.getType() 会调用以下的搜索步骤:

  1. 搜索 instance 实例
  2. 搜索 SimType.prototype
  3. 搜索 SubType.prototype
  4. 搜索 SuperType.prototype,找到了 getType 方法

在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链的末端才会停下来。

所有引用类型都继承了 Object,这个继承也是通过原型链实现的。如果在 SuperType.prototype 还没有找到 getType,就会到 Object.prototype中找(图中少画了一环)。

原型链继承

原型链继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

SubType.prototype = new SuperType();

functionSuperType() {
    this.name='Yvette';
    this.colors= ['pink', 'blue', 'green'];
}
SuperType.prototype.getName=function () {
    returnthis.name;
}
functionSubType() {
    this.age=22;
}
SubType.prototype=newSuperType();
SubType.prototype.getAge=function() {
    returnthis.age;
}
SubType.prototype.constructor= SubType;
let instance1 =newSubType();
instance1.colors.push('yellow');
console.log(instance1.getName()); //'Yvette'console.log(instance1.colors);//[ 'pink', 'blue', 'green', 'yellow' ]let instance2 =newSubType();
console.log(instance2.colors);//[ 'pink', 'blue', 'green', 'yellow' ]

可以看出 colors 属性会被所有的实例共享(instance1、instance2、…)。

缺点:

  1. 通过原型来实现继承时,原型会变成另一个类型的实例,原先的实例属性变成了现在的原型属性,该原型的引用类型属性会被所有的实例共享。
  2. 在创建子类型的实例时,没有办法在不影响所有对象实例的情况下给超类型的构造函数中传递参数。

借用构造函数

借用构造函数的技术,其基本思想为:

在子类型的构造函数中调用超类型构造函数。

functionSuperType(name) {
    this.name= name;
    this.colors= ['pink', 'blue', 'green'];
}
functionSubType(name) {
    SuperType.call(this, name);
}
let instance1 =newSubType('Yvette');
instance1.colors.push('yellow');
console.log(instance1.colors);//['pink', 'blue', 'green', yellow]let instance2 =newSubType('Jack');
console.log(instance2.colors); //['pink', 'blue', 'green']

优点:

  1. 可以向超类传递参数
  2. 解决了原型中包含引用类型值被所有实例共享的问题

缺点:

  1. 方法都在构造函数中定义,函数复用无从谈起,另外超类型原型中定义的方法对于子类型而言都是不可见的。

组合继承

组合继承指的是将原型链和借用构造函数技术组合到一块,从而发挥二者之长的一种继承模式。基本思路:

使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,既通过在原型上定义方法来实现了函数复用,又保证了每个实例都有自己的属性。

functionSuperType(name) {
    this.name= name;
    this.colors= ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName=function () {
    console.log(this.name);
}
functionSuberType(name, age) {
    SuperType.call(this, name);
    this.age= age;
}
SuberType.prototype=newSuperType();
SuberType.prototype.constructor= SuberType;
SuberType.prototype.sayAge=function () {
    console.log(this.age);
}
let instance1 =newSuberType('Yvette', 20);
instance1.colors.push('yellow');
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]instance1.sayName(); //Yvettelet instance2 =newSuberType('Jack', 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]instance2.sayName();//Jack

缺点:

  • 无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

优点:

  • 可以向超类传递参数
  • 每个实例都有自己的属性
  • 实现了函数复用

原型式继承

原型继承的基本思想:

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

functionobject(o) {
    functionF() { }
    F.prototype= o;
    returnnewF();
}

object() 函数内部,先穿甲一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例,从本质上讲,object() 对传入的对象执行了一次浅拷贝。

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

var person = {
    name:'Yvette',
    hobbies: ['reading', 'photography']
}
var person1 =Object.create(person);
person1.name='Jack';
person1.hobbies.push('coding');
var person2 =Object.create(person);
person2.name='Echo';
person2.hobbies.push('running');
console.log(person.hobbies);//[ 'reading', 'photography', 'coding', 'running' ]console.log(person1.hobbies);//[ 'reading', 'photography', 'coding', 'running' ]

在没有必要创建构造函数,仅让一个对象与另一个对象保持相似的情况下,原型式继承是可以胜任的。

缺点:

同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部已某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

functioncreateAnother(original) {
    var clone =object(original);//通过调用函数创建一个新对象clone.sayHi=function () {//以某种方式增强这个对象console.log('hi');
    };
    return clone;//返回这个对象
}
var person = {
    name:'Yvette',
    hobbies: ['reading', 'photography']
};

var person2 =createAnother(person);
person2.sayHi(); //hi

基于 person 返回了一个新对象 -—— person2,新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi() 方法。在考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

缺点:

  • 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而效率低下。
  • 同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,基本思路:

不必为了指定子类型的原型而调用超类型的构造函数,我们需要的仅是超类型原型的一个副本,本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示:

functioninheritPrototype(subType, superType) {
    var prototype =object(superType.prototype); //创建对象prototype.constructor= subType;//增强对象subType.prototype= prototype;//指定对象
}
  • 第一步:创建超类型原型的一个副本
  • 第二步:为创建的副本添加 constructor 属性
  • 第三步:将新创建的对象赋值给子类型的原型

至此,我们就可以通过调用 inheritPrototype 来替换为子类型原型赋值的语句:

functionSuperType(name) {
    this.name= name;
    this.colors= ['pink', 'blue', 'green'];
}
//...codefunctionSuberType(name, age) {
    SuperType.call(this, name);
    this.age= age;
}
inheritPrototype(SuberType, SuperType);
//...code

优点:

只调用了一次超类构造函数,效率更高。避免在SuberType.prototype上面创建不必要的、多余的属性,与其同时,原型链还能保持不变。

因此寄生组合继承是引用类型最理性的继承范式。

ES6 继承

Class 可以通过extends关键字实现继承,如:

classSuperType {
    constructor(age) {
        this.age= age;
    }

    getAge() {
        console.log(this.age);
    }
}

classSubTypeextendsSuperType {
    constructor(age, name) {
        super(age); // 调用父类的constructor(x, y)this.name= name;
    }

    getName() {
        console.log(this.name);
    }
}

let instance =newSubType(22, '刘小夕');
instance.getAge(); //22

对于ES6的 class 需要做以下几点说明:

  1. 类的数据类型就是函数,类本身就指向构造函数。

    console.log(typeof SuperType);//functionconsole.log(SuperType ===SuperType.prototype.constructor); //true

  2. 类的内部所有定义的方法,都是不可枚举的。(ES5原型上的方法默认是可枚举的)

    Object.keys(SuperType.prototype);

  3. constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。

  4. Class 不能像构造函数那样直接调用,会抛出错误。

使用 extends 关键字实现继承,有一点需要特别说明:

  • 子类必须在 constructor 中调用 super 方法,否则新建实例时会报错。如果没有子类没有定义 constructor 方法,那么这个方法会被默认添加。在子类的构造函数中,只有调用 super 之后,才能使用 this关键字,否则报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

    classSubTypeextendsSuperType {
    constructor(…args) {
    super(…args);
    }
    }

参考文章:

[1] 珠峰架构课(墙裂推荐)

[2] CSS-清除浮动

[3] 详解JS函数柯里化

[4] JavaScript数组去重

谢谢各位小伙伴愿意花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的肯定是我前进的最大动力。 https://github.com/YvetteLau/Blog