关于前端:JS继承面试的时候怎么说答应我不要再死记硬背了好吗

43次阅读

共计 9544 个字符,预计需要花费 24 分钟才能阅读完成。

前言

JS继承这块,ES6曾经有 class 很香的语法糖实现了,ES6之前那些实现继承的办法真的又多又长,说句心里话,能不学真的不想再学,然而没方法,面试还是要搞你呀,所以这两天看回 ES6 之前的继承,发现还是蛮有意思的。写这篇文章也是对本人的一个梳理总结,也心愿能帮忙到大家弄懂继承这块,这样就不须要再死记硬背八股文,面试自由发挥就好。
JS的继承,外围就是靠 原型链 实现。如果大家对原型链还不是很分明,能够先读读我写的这篇对于原型链的文章——带你搞明确到底什么是原型、原型链。

文章蛮长,大家能够分成两局部来看。原型链继承、盗用构造函数继承、组合继承为一部分,原型式继承、寄生式继承、寄生式组合继承为一部分。

为了让大家更好的了解,前面的例子,咱们都用:

  • Animal作为父类
  • Cat为子类
  • cat为子类 Cat 实例一,small_cat为子类 Cat 实例二

JS继承最常见的 六种 形式

  • 原型链继承
  • 盗用构造函数继承
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生式组合继承

原型链继承

原理:为什么叫原型链继承,咱们能够这样记,因为外围就是咱们会重写某个构造函数的原型(prototype),使其指向父类的一个实例,以此让它们的原型链一直串联起来,从而实现继承。

将子类 Cat.prototype 指向父类 Animal 的一个实例(Cat.prototype = new Animal()),这样咱们就实现了一个原型链继承。来看看具体例子:

// 定义一个父类
function Animal() {this.like = ['eat', 'drink', 'sleep'];
}

// 为父类的原型增加一个 run 办法
Animal.prototype.run = function() {console.log('跑步');
}

// 定义一个子类
function Cat() {this.name = 'limingcan';}

// 外围:将 Cat 的原型指向父类 Animal 的一个实例
Cat.prototype = new Animal();

// 实例 cat.constructor 是来自 Cat.prototype.constructor
// 不改正的 cat.constructor 话,以后的 cat.constructor 指向的是 Animal
// 因为 Cat.prototype 被重写,constructor 被指向了 new Animal().__proto__.constructor,相当于 Animal.prototype.constructor
Cat.prototype.constructor = Cat;

// 实例一个由子类 new 进去的对象
const cat = new Cat();

cat.run();

console.log(cat);

打印:

解析:

当咱们执行 Cat.prototype = new Animal(); 这句时,产生了什么:

它把 Cat.prototype 整个重写了,并将两者通过原型链分割起来,从而实现继承。因为咱们将 Cat.prototype 指向了父类 Animal 的一个实例,咱们临时把这个实例叫做 中介实例 X ,这个 中介实例 X 本人也有一个 __proto__,它又指向了Animal.prototype。所以当实例cat 在本身找不到属性办法时,它会去 cat.__proto__(相当于Cat.prototype,然而Cat.prototype 被重写成了 中介实例 X ,所以也是去 中介实例 X 外面找)找。如果 中介实例 X 也找不到,就会去 中介实例 X.__proto__(相当于 Animal.prototype)找。有值的话,则返回值;没有值的话又会去Animal.prototype.__proto__(相当于Object.prototype)找。有值的话,则返回值;没有值的话又会去Object.prototype.__proto__ 找,然而 Object.prototype.__proto__ 返回 null,原型链到顶,一条条原型链搜寻结束,都没有,则返回undefined。所以这就是为什么实例cat 本身 没有 like 属性跟 run 办法,然而还是能够拜访。上述的大抵过程,咱们能够这样看:

这条链有点绕,所以这也是为什么大家对原型链继承总是那么昏头昏脑的起因。倡议读的时候想一下这条链是什么样的,怎么来的。然而外围还是要分明原型链,所以对原型链不了解的同学,倡议还是先把原型链弄清楚,这样才好了解继承。

如果咱们这时候给实例 catlike属性 push 一个值,看看上面例子:

// 定义一个父类
function Animal() {this.like = ['eat', 'drink', 'sleep'];
}

// 为父类的原型增加一个 run 办法
Animal.prototype.run = function() {console.log('跑步');
}

// 定义一个子类
function Cat() {this.name = 'limingcan';}

// 外围:将 Cat 的原型指向父类 Animal 的一个实例
Cat.prototype = new Animal();

// 实例 cat.constructor 是来自 Cat.prototype.constructor
// 不改正的 cat.constructor 话,以后的 cat.constructor 指向的是 Animal
// 因为 Cat.prototype 被重写,constructor 被指向了 new Animal().__proto__.constructor,相当于 Animal.prototype.constructor
Cat.prototype.constructor = Cat;

// 实例一个由子类 new 进去的对象
const cat = new Cat();

// 给 like 属性 push 一个 play 值
cat.like.push('play');

// 实例第二个对象
const small_cat = new Cat();

console.log(cat.like);

console.log(small_cat.like);

console.log(cat.like === small_cat.like);

打印:

咱们会发现,如果咱们批改实例 cat 的属性,并且该属性是援用类型的话,后续实例化进去的对象,都会被影响到 。因为catsmall_cat本身 没有 like 属性,它们的 like 都继承自Cat.prototype,指向的是的同一份地址。

如果想要两个实例批改 like 互不影响,只能给他们本身减少一个 like 属性(cat.like = ['eat', 'drink', 'sleep', 'play'];cat_small.like = ['food']。如果本身有属性,是不会去 prototype 查找的,它们是两个实例本人独有的属性,指向不同地址),但这样就失去了继承的意义了。

总结:

  • 长处:

    • 实现绝对简略
    • 子类实例能够间接拜访到父类实例或父类原型上的属性办法
  • 毛病:

    • 父类所有的援用类型属性都会被实例进去的对象共享,所以批改一个实例对象的援用类型属性,会导致所有实例对象受到影响
    • 实例化时,不能传参数

因而为了解决原型链继承的毛病,又搞了个盗用构造函数继承的形式。

盗用构造函数继承

盗用构造函数继承,也叫借用构造函数继承,它能够解决原型链继承带来的毛病。

原理:在子类构造函数中,调用父类构造函数办法,但通过 call 或者 apply 办法扭转了父类构造函数内 this 的指向,使得子类实例进去的对象,本身 领有来自父类构造函数的办法跟属性,且分别独立,互不影响。

来看看具体例子:

// 定义一个父类
function Animal(name) {
  this.name = name;
  this.like = ['eat', 'drink', 'sleep'];
  this.play = function() {console.log('到处玩');
  }
}

// 为父类的原型增加一个 run 办法
Animal.prototype.run = function() {console.log('跑步');
}

// 定义一个子类
function Cat(name, age) {Animal.call(this, name);
  this.age = age;
}

// 实例一个由子类 new 进去的对象
const cat = new Cat('limingcan', 27);

// 给实例 cat 的 like 属性 push 一个 toys 值
cat.like.push('toys');

// 实例第二个对象
const small_cat = new Cat('mimi', 100);

console.log(cat);

console.log(small_cat);

console.log(cat.run);

console.log(small_cat.run);

打印:

从打印咱们能够看出:

  1. 实例化子类 Cat 时,能够传入参数
  2. 父类 Animal 里的属性办法,都被增加到实例 cat 跟实例 small_cat本身 里了(因为子类 Cat 调用了 call 办法,某种程度来说继承了父类 Animal 里的属性办法)
  3. 批改实例 cat 不会影响到实例 small_cat(因为实例进去的对象,所有的属性、办法都是增加到实例对象 本身,而不是增加到实例对象的原型上,它们是齐全独立,指向的都是不同的地址)
  4. 打印 run 办法,输入都是 undefined,阐明实例没有继承父类Animal 原型上的办法(实例的原型链没有跟父类 Animal 原型链买通,因而原型链上搜寻不到 run 办法,能够跟原型链继承比照想想)
  5. 子类的原型 Cat.prototype 与父类原型 Animal.prototype 没有买通,因为 Cat.prototype.__proto__ 间接指向了 Object.prototype,如果买通了的话,应该是Cat.prototype.__proto__ 指向 Animal.prototype,这也是为什么实例cat 没有继承父类 run 办法的起因,因为拜访不到。

总结:

  • 长处:

    • 实例化时,能够传参
    • 子类通过 callapply办法,将父类里的所有属性、办法复制到实例对象的 本身,而不是共享原型链上同一个属性,所以批改一个实例对象的援用类型属性时,不会导致所有实例对象受到影响
  • 毛病:

    • 无奈继承父类原型上的属性与办法

咱们通过借用构造函数继承的办法,解决了原型链继承的毛病。然而又产生了一个新的问题——子类无奈继承父类原型(Animal.prototype)上的属性与办法,如果咱们把这两种形式联合一下,会不会好点呢,于是有了组合继承这个继承形式。

组合继承

组合继承顾名思义就是,利用原型链继承跟借用构造函数继承相结合,而发明进去的一种新的继承形式,是不是很好记。

原理:利用原型链继承,实现实例对父类原型(Animal.protoytype)上的办法与属性继承;利用借用构造函数继承,实现实例对父类构造函数(function Animal() {})里的属性的继承,并且解决原型链继承的缺点。

来看看具体例子:

// 定义一个父类
function Animal(name, sex) {
  this.name = name;
  this.sex = sex;
  this.like = ['eat', 'drink', 'sleep'];
}

// 为父类的原型增加一个 run 办法
Animal.prototype.run = function() {console.log('跑步');
}

// 定义一个子类
function Cat(name, sex, age) {
  // 第一次调用 Animal 构造函数
  Animal.call(this, name, sex);
  this.age = age;
}

// 外围:将 Cat 的原型指向父类 Animal 的一个实例(第二次调用 Animal 构造函数)Cat.prototype = new Animal();

// 实例 cat.constructor 是来自 Cat.prototype.constructor
// 不改正的 cat.constructor 话,以后的 cat.constructor 指向的是 Animal
// 因为 Cat.prototype 被重写,constructor 被指向了 new Animal().__proto__.constructor,相当于 Animal.prototype.constructor
Cat.prototype.constructor = Cat;

// 实例一个由子类 new 进去的对象
const cat = new Cat('limingcan', 'man', 27);
console.log(cat);

打印:

由上图咱们能得出总结:

  • 长处:

    • 利用原型链继承,将实例 cat、子类Cat、父类Animal 三者的原型链串联起来,让实例对象继承父类原型 Animal.prototype 的办法与属性
    • 利用借用构造函数继承,将父类构造函数 function Animal() {} 的属性、办法增加到实例 本身 上,解决原型链继承,实例批改援用类型属性时对后续实例影响问题
    • 利用构造函数继承,实例化对象时,可传参
  • 毛病:

    • 两次调用父类构造函数 function Animal() {}(第一次在子类Cat 构造函数内调用,第二次在 new Animal() 时候调用)
    • 实例本身领有的属性,子类 Cat.prototype 里也会有,造成不必要的节约(因为 Cat.prototype 被重写为 new Animal() 了,new Animal()是父类的一个实例,也有 namesexlike 属性)

看来组合继承也不是最完满的继承形式。咱们先把组合继承放一边,先看看什么是原型式继承。

原型式继承

原理:用于创立一个新对象,应用现有的对象来作为新创建对象的原型(prototype)。个别应用 Object.create() 办法实现,具体用法能够看看这里。

来看看具体例子:

// 定义一个父类(新建进去的对象的__proto__会指向它)const Animal = {
  name: 'nobody',
  like: ['eat', 'drink', 'sleep'],
  run() {console.log('跑步');
  }
};

// 新建以 Animal 为原型的实例
const cat = Object.create(
  Animal,
  // 这里定义的是实例本身的办法或属性
  {
    name: {value: 'limingcan'}
  }
);

// 给实例 cat 属性 like 增加一个 play 值
cat.like.push('play');

const small_cat = Object.create(
  Animal,
  // 这里定义的是实例本身的办法或属性
  {
    name: {value: 'mimi'}
  }
);

console.log(cat);
console.log(small_cat);
console.log(cat.__proto__ === Animal);

打印:

由上图咱们能够得出总结:

  • 长处:

    • 实现比原型链继承更简洁(不须要写什么构造函数了,也不须要写子类Cat,间接父类继承Animal
    • 子类实例能够间接拜访到父类实例或父类原型上的属性办法
  • 毛病:

    • 父类所有的援用类型属性都会实例进去的对象共享,所以批改一个实例对象的援用类型属性,会导致所有实例对象受到影响
    • 实例化时,不能传参数

咱们能够比照原型链继承形式,其实这两种形式差不多,所以它要跟原型链继承存在一样的毛病,然而实现起来比原型式继承更加简洁不便一些。如果咱们只是想让一个对象跟另一个对象放弃相似,原型式继承可能更加难受,因为它不须要像原型链继承那样大费周章。接下来咱们再看看另一种继承形式——寄生式继承。

寄生式继承

原理:它其实就是对原型式继承进行一个小封装,加强了一下实例进去的对象

来看看具体例子:


// 定义一个父类(新建进去的对象的__proto__会指向它)const Animal = {
  name: 'nobody',
  like: ['eat', 'drink', 'sleep'],
  run() {console.log('跑步');
  }
};

// 定义一个封装 Object.create()办法的函数
const createObj = (parentPropety, ownProperty) => {
  // 生成一个以 parentPropety 为原型的对象 obj
  // ownProperty 是新建进去的实例,领有本身的属性跟办法配置
  const obj = Object.create(parentPropety, ownProperty);

  // 加强性能
  obj.catwalk = function() {console.log('走猫步');
  };

  return obj;
}

// 新建以 Animal 为原型的实例一
const cat = createObj(Animal, {
  name: {value: 'limingcan'}
})

// 给实例 cat 属性 like 增加一个 play 值
cat.like.push('play');

// 新建以 Animal 为原型的实例二
const small_cat = createObj(Animal, {
  name: {value: 'mimi'}
})

console.log(cat);
console.log(small_cat);
console.log(cat.__proto__ === Animal);

打印:

总结:

  • 长处:

    • 实现比原型链继承更简洁
    • 子类实例能够间接拜访到父类实例或父类原型上的属性办法
  • 毛病:

    • 父类所有的援用类型属性都会实例进去的对象共享,所以批改一个实例对象的援用类型属性,会导致所有实例对象受到影响
    • 实例化时,不能传参数

寄生式继承优缺点跟原型式继承一样,但最重要的是它提供了一个相似 工厂的思维,是对原型式继承的一个封装。后面咱们说到组合继承还是会有一些缺点,通过原型式继承跟寄生式继承,咱们能够利用这两个继承的思维,来解决组合继承的缺点,它就是寄生组合式继承。

寄生式组合继承

原理:利用原型链继承,实现实例对父类原型(Animal.prototype)办法与属性的继承;利用借用构造函数继承,实现实例对父类构造函数(function Animal() {})里的属性的继承,并且解决了组合继承带来的缺点

后面咱们说到,组合继承会有以下两个毛病:

  • 会两次调用父类构造函数 function Animal() {}。(第一次在子类构造函数内应用call 或者 apply 办法时调用;第二次在 Cat.prototype = new Animal() 时候调用了)
  • 实例本身领有的属性,子类构造函数的 prototype 里也会有,造成不必要的节约(因为子类构造函数的 protptype 被重写为父类的一个实例了,所以 Cat.prototype 也会领有父类实例里的属性跟办法)

通过下面原型式继承的形式,咱们能够把原型链继承里,Cat.prototype = new Animal()这一步,用寄生式继承的思维,用 Object.create() 办法实现并替换掉。来看看具体例子:

// 定义一个父类
function Animal(name, sex) {
  this.name = name;
  this.sex = sex;
  this.like = ['eat', 'drink', 'sleep'];
}

// 定义一个子类
function Cat(name, sex, age) {
  // 第一次调用 Animal 构造函数
  Animal.call(this, name, sex);
  this.age = age;
}

// 定义一个利用原型式继承形式,跟寄生式继承思维来实现寄生组合式继承的办法
function inheritObj(parentClass, childClass) {
  // parentClass 为传入的父类
  // childClass 为传入的子类
  // finalProperty 为最初继承的原型对象

  const finalProperty = Object.create(parentClass.prototype);

  finalProperty.constructor = childClass;

  childClass.prototype = finalProperty;
}

// 为父类的原型增加一个 run 办法
Animal.prototype.run = function() {console.log('跑步');
}

// 实现寄生组合继承
inheritObj(Animal, Cat);

// 给子类的原型增加一个办法
Cat.prototype.catwalk = function() {console.log('走猫步');
}

// 实例一个由子类 new 进去的对象
const cat = new Cat('limingcan', 'man', 27);

console.log(cat);

寄生式组合继承打印:

组合继承打印:

咱们能够比照一下组合继承那张图会发现:

  • 实例 cat 本身该有的属性都有
  • Cat.prototype也洁净了,没有把父类的属性都复制一遍,只有本人增加的 catwalk 办法
  • Animal.prototype也非常洁净,只有本人增加的 run 办法

这是根本咱们最想要的后果,也是最现实的继承形式。

解析:
<!–(咱们把 parentClass 称作父类,把 childClass 称作子类,把 finalProperty 称作最初继承的原型对象)–>
咱们想想为什么在组合继承时,咱们要 Cat.prototype = new Animal()?外围是因为咱们要 买通实例 cat、子类Cat、父类Animal 三者的原型链 ,从而实现继承。咱们顺着这个思路,解析一下下面inheritObj 这个办法,短短三行,然而为什么会产生那么神奇的事:

  • const finalProperty = Object.create(parentClass.prototype):浅拷贝一份 parentClass.prototype,并将其作为finalProperty 对象的原型,即 finalProperty.__proto__ === parentClass.prototype。此时finalProperty.constructor 指向的是parentClass.prototype.constructor
  • finalProperty.constructor = childClass:寄生式继承思维,加强对象。改正finalProperty.constructor,让其指向childClass
  • childClass.prototype = finalProperty:使得实例找不到办法属性,会去 childClass.prototype 找;再不到,会去 finalProperty 找;再找不到会去 finalProperty__proto__(parentClass.prototype) 找。买通了子类 childClass 与父类的 parentClass 原型链,实现了父子类的继承。

inheritObj办法,其实质就是上面的实现,这样可能能够更加直观的看出继承:

// 定义一个利用原型式继承形式,跟寄生式继承思维来实现寄生组合式继承的办法
function inheritObj(parentClass, childClass) {
  // parentClass 为传入的父类
  // childClass 为传入的子类
  
  childClass.prototype.__proto__ = parentClass.prototype;

  childClass.prototype.constructor = childClass;
}

最初

终于写完了!真的太累了!心愿这篇文章读完对大家有所帮忙,面试的时候不虚。只有了解透了各个继承形式的原理,各个继承形式的优缺点真的没有必要背,优缺点本人总结就好了呀,万变不离其宗~
如果大家有什么异同,欢送评论交换;如果感觉这篇文章好的话,欢送点赞分享,这篇文章真的花了我不少功夫。

正文完
 0