【Step-By-Step】高频面试题深刻解析 / 周刊06

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

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

更多优质文章可戳: https://github.com/YvetteLau/...前端

构造函数

构造函数和普通函数的区别仅在于调用它们的方式不一样,任何函数,只要经过 new 操做符来调用,那它就能够做为构造函数;任何函数,若是不经过 new 操做符来调用,那么它就是一个普通函数。java

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

function Person(name, age) {
    this.name = name;
    this.age = age;
}

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

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

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

原型

咱们建立的每一个函数都有 prototype 属性,这个属性指向函数的原型对象。原型对象的用途是包含能够由特定类型的全部实例共享的属性和方法。面试

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

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

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

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

原型链

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

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

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

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

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

一图胜万言:

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

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

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

全部引用类型都继承了 Object,这个继承也是经过原型链实现的。若是在 SuperType.prototype 尚未找到 getType,就会到 Object.prototype中找(图中少画了一环)。

25. 原型链继承

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

SubType.prototype = new SuperType();

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

let instance2 = new SubType();
console.log(instance2.colors);//[ 'pink', 'blue', 'green', 'yellow' ]

能够看出 colors 属性会被全部的实例共享(instance一、instance二、...)。

缺点:
  1. 经过原型来实现继承时,原型会变成另外一个类型的实例,原先的实例属性变成了如今的原型属性,该原型的引用类型属性会被全部的实例共享。
  2. 在建立子类型的实例时,没有办法在不影响全部对象实例的状况下给超类型的构造函数中传递参数。

26. 借用构造函数

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

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

function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
function SubType(name) {
    SuperType.call(this, name);
}
let instance1 = new SubType('Yvette');
instance1.colors.push('yellow');
console.log(instance1.colors);//['pink', 'blue', 'green', yellow]

let instance2 = new SubType('Jack');
console.log(instance2.colors); //['pink', 'blue', 'green']
优势:
  1. 能够向超类传递参数
  2. 解决了原型中包含引用类型值被全部实例共享的问题
缺点:
  1. 方法都在构造函数中定义,函数复用无从谈起,另外超类型原型中定义的方法对于子类型而言都是不可见的。

27. 组合继承

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

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

function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}
function SuberType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SuberType.prototype = new SuperType();
SuberType.prototype.constructor = SuberType;
SuberType.prototype.sayAge = function () {
    console.log(this.age);
}
let instance1 = new SuberType('Yvette', 20);
instance1.colors.push('yellow');
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); //Yvette

let instance2 = new SuberType('Jack', 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]
instance2.sayName();//Jack
缺点:
  • 不管什么状况下,都会调用两次超类型构造函数:一次是在建立子类型原型的时候,另外一次是在子类型构造函数内部。
优势:
  • 能够向超类传递参数
  • 每一个实例都有本身的属性
  • 实现了函数复用

28. 原型式继承

原型继承的基本思想:

借助原型能够基于已有的对象建立新对象,同时还没必要所以建立自定义类型。

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

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' ]

在没有必要建立构造函数,仅让一个对象与另外一个对象保持类似的状况下,原型式继承是能够胜任的。

缺点:

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

29. 寄生式继承

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

function createAnother(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() 方法。在考虑对象而不是自定义类型和构造函数的状况下,寄生式继承也是一种有用的模式。

缺点:
  • 使用寄生式继承来为对象添加函数,会因为不能作到函数复用而效率低下。
  • 同原型链实现继承同样,包含引用类型值的属性会被全部实例共享。

30. 寄生组合式继承

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

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

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

至此,咱们就能够经过调用 inheritPrototype 来替换为子类型原型赋值的语句:

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

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

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

ES6 继承

Class 能够经过extends关键字实现继承,如:

class SuperType {
    constructor(age) {
        this.age = age;
    }

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

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

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

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

对于ES6的 class 须要作如下几点说明:

  1. 类的数据类型就是函数,类自己就指向构造函数。
console.log(typeof SuperType);//function
console.log(SuperType === SuperType.prototype.constructor); //true
  1. 类的内部全部定义的方法,都是不可枚举的。(ES5原型上的方法默认是可枚举的)
Object.keys(SuperType.prototype);
  1. constructor 方法是类的默认方法,经过 new 命令生成对象实例时,自动调用该方法。一个类必须有constructor 方法,若是没有显式定义,一个空的 constructor 方法会被默认添加。
  2. Class 不能像构造函数那样直接调用,会抛出错误。

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

  • 子类必须在 constructor 中调用 super 方法,不然新建实例时会报错。若是没有子类没有定义 constructor 方法,那么这个方法会被默认添加。在子类的构造函数中,只有调用 super 以后,才能使用 this关键字,不然报错。这是由于子类实例的构建,基于父类实例,只有super方法才能调用父类实例。
class SubType extends SuperType {
    constructor(...args) {
        super(...args);
    }
}

参考文章:

[1] CSS-清除浮动

[2] 详解JS函数柯里化

[3] JavaScript数组去重

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

推荐关注本人公众号:

clipboard.png

相关文章
相关标签/搜索