首页 > 其他分享 >【面试题】JS 常见的 6 种继承方式(常见)

【面试题】JS 常见的 6 种继承方式(常见)

时间:2022-11-05 17:36:34浏览次数:42  
标签:function 面试题 name 继承 常见 JS 对象 原型 构造函数


【面试题】JS 常见的 6 种继承方式(常见)_javascript


继承概念的探究

说到继承的概念,首先要说一个经典的例子。

先定义一个类(Class)叫汽车,汽车的属性包括颜色、轮胎、品牌、速度、排气量等,由汽车这个类可以派生出“轿车”和“货车”两个类,那么可以在汽车的基础属性上,为轿车添加一个后备厢、给货车添加一个大货箱。这样轿车和货车就是不一样的,但是二者都属于汽车这个类,这样从这个例子中就能详细说明汽车、轿车以及卡车之间的继承关系。

继承可以使得子类别具有父类的各种方法和属性,比如上面的例子中“轿车” 和 “货车” 分别继承了汽车的属性,而不需要再次在“轿车”中定义汽车已经有的属性。在“轿车”继承“汽车”的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与“汽车”这个父类不同的属性和方法。

JS 实现继承的几种方式

1、原生链继承

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

function Parent1() {

this.name = 'parent1';

this.play = [1, 2, 3]

}

function Child1() {

this.type = 'child2';

}

Child1.prototype = new Parent1();

console.log(new Child1());
复制代码

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。虽然父类的方法和属性都能够访问,但其实有一个潜在的问题,明明我只改变了 s1 的 play 属性,为什么 s2 也跟着变了呢?原因很简单,因为两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点。

var s1 = new Child1();

var s2 = new Child2();

s1.play.push(4);

console.log(s1.play, s2.play);
复制代码

2、构造函数继承

function Parent1(){

this.name = 'parent1';

}



Parent1.prototype.getName = function () {

return this.name;

}



function Child1(){

Parent1.call(this);

this.type = 'child1'

}



let child = new Child1();

console.log(child); // 没问题

console.log(child.getName()); // 会报错
复制代码

可以看到最后打印的 child 在控制台显示,除了 Child1 的属性 type 之外,也继承了 Parent1 的属性 name。这样写的时候子类虽然能够拿到父类的属性值,解决了第一种继承方式的弊端。

问题:父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法。

3、组合继承

这种方式结合了前两种继承方式的优缺点,结合起来的继承。

function Parent3 () {

this.name = 'parent3';

this.play = [1, 2, 3];

}



Parent3.prototype.getName = function () {

return this.name;

}

function Child3() {

// 第二次调用 Parent3()

Parent3.call(this);

this.type = 'child3';

}



// 第一次调用 Parent3()

Child3.prototype = new Parent3();

// 手动挂上构造器,指向自己的构造函数

Child3.prototype.constructor = Child3;

var s3 = new Child3();

var s4 = new Child3();

s3.play.push(4);

console.log(s3.play, s4.play); // 不互相影响

console.log(s3.getName()); // 正常输出'parent3'

console.log(s4.getName()); // 正常输出'parent3'
复制代码

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

4、原型式继承

ES5 里面的 Object.create 方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。

let parent4 = {

name: "parent4",

friends: ["p1", "p2", "p3"],

getName: function() {

return this.name;

}

};



let person4 = Object.create(parent4);

person4.name = "tom";

person4.friends.push("jerry");



let person5 = Object.create(parent4);

person5.friends.push("lucy");



console.log(person4.name);//tom

console.log(person4.name === person4.getName());//true

console.log(person5.name);//parent4

console.log(person4.friends);//["p1","p2","p3","jerry","lucy"]

console.log(person5.friends);//["p1","p2","p3","jerry","lucy"]
复制代码

通过 Object.create 这个方法可以实现普通对象的继承,不仅仅能继承属性,同样也可以继承 getName 的方法。

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

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

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

最后两个输出结果是一样的,其实 Object.create 方法是可以为一些对象实现浅拷贝的。

问题:多个实例的引用类型属性指向相同的内存,存在篡改的可能。

5、寄生继承

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

虽然其优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。

let parent5 = {

name: "parent5",

friends: ["p1", "p2", "p3"],

getName: function() {

return this.name;

}

};



function clone(original) {

let clone = Object.create(original);

clone.getFriends = function() {

return this.friends;

};

return clone;

}



let person5 = clone(parent5);



console.log(person5.getName());

console.log(person5.getFriends());
复制代码

 person5 是通过寄生式继承生成的实例,它不仅仅有 getName 的方法,而且可以看到它最后也拥有了 getFriends 的方法。person5 通过 clone 的方法,增加了 getFriends 的方法,从而使 person5 这个普通对象在继承过程中又增加了一个方法,这样的继承方式就是寄生式继承。

问题:优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。

6、寄生组合式继承

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

function clone (parent, child) {

// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程

child.prototype = Object.create(parent.prototype);

child.prototype.constructor = child;

}



function Parent6() {

this.name = 'parent6';

this.play = [1, 2, 3];

}

Parent6.prototype.getName = function () {

return this.name;

}

function Child6() {

Parent6.call(this);

this.friends = 'child5';

}



clone(Parent6, Child6);



Child6.prototype.getFriends = function () {

return this.friends;

}



let person6 = new Child6();

console.log(person6);

console.log(person6.getName());

console.log(person6.getFriends());
复制代码

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

总结:

(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在 的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享, 容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传 递参数。

(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子 类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不 能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函 数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。

(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数 组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属 性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继 承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我 们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构 造函数,造成了子类型的原型中多了很多不必要的属性

(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已 有的对象来创建新的对象,实现的原理是,向函数中传入一个对象, 然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为 了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原 型链方式相同。

(5)第五种方式是寄生式继承,寄生式继承的思路是创建一个用于 封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本, 然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解 是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这 个对象不是自定义类型时。缺点是没有办法实现函数的复用。

(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类 型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式 组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这 样就避免了创建不必要的属性。 

上面的继承看见就想吐,而且还有人拷问这些东西,很烦!还好ES6出了class和extends ,我们可以通过class创造类,使用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,让一些不支持新语法的浏览器也能运行。

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));
复制代码

ES5/ES6 的继承除了写法以外还有什么区别?

1. ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加 到 this 上(Parent.apply(this))

2. ES6 的继承机制完全不同,实质上是先创建父类的实例对象 this(所以必 须先调用父类的 super()法),然后再用子类的构造函数修改 this。

3. ES5 的继承时通过原型或构造函数机制来实现。

4. ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关 键字实现继承。

5. 子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因为子类没有自己的 this 对象,而是继承了父类的 this 对象,然后对其进行加工。 如果不调用 super 方法,子类得不到 this 对象。

6. 注意 super 关键字指代父类的实例,即父类的 this 对象。

7. 注意:在子类构造函数中,调用 super 后,才可使用 this 关键字,否则 报错。

 


标签:function,面试题,name,继承,常见,JS,对象,原型,构造函数
From: https://blog.51cto.com/u_14627797/5826029

相关文章

  • 【面试题】 面试手写JS 十六题(必看)
     1、手写实现防抖和节流1.1实现防抖函数防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函......
  • 【面试题】面试小技巧:如果有人问你 xxx 技术是什么?
    背景在前几天,有一个朋友突然问了我一个问题,说如果有人问你“React是什么?你怎么回答。 ”,我当时脱口而出“React是一个网页UI框架一个,它的特点声明式、组件化、组件化、......
  • 【面试题】说说JS中的this指向问题
    JS中的this指向问题this的指向问题全局作用域在JS中,全局的变量和函数附着在​​global​​​对象上,全局对象在浏览器环境下是​​window​​对象。在全局作用域中,​​this​......
  • 野花--input获取焦点,改变父元素,改变兄弟元素,不使用js来实现
    :focus-within:focus-within是一个CSS伪类,表示一个元素获得焦点,或该元素的后代元素获得焦点。换句话说,元素自身或者它的某个后代匹配:focus伪类。(shadowDOM树(en-U......
  • Unity中的常见单例模式
    在Unity中我们用到的最多的设计模式之一就是单例模式单例模式顾名思义就是只允许单个此实例物体存在因此单例类通常挂载在各种Manager物体上下面介绍几种常用单例模式的......
  • 常见的设计模式
    单例模式:单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在......
  • js的bind 的原理
    js的bind方法主要绑定this的指向bind方法也会返回是个bind后的函数。知道它功能我们就可以自定义bind功能letobject={name:'jeff'}functionfn(){console.log(thi......
  • CSS & JS Effect – Textarea Autoresize
    前言这是一个很普遍的体验,而且实现起来也很简单哦 参考YouTube– HowtoAutoResizeTextareausingHTMLCSS&JavaScript 效果我故意加了border和pad......
  • Jmeter断言之Json Assertion
    现在大部分的程序都是通过json格式返回数据,所以JsonAssertion也是非常重要的一个组件添加JsonAssertion组件使用方法 ......
  • JS中的变量声明
    一、引入1.定义:在JavaScript中创建变量被称为“声明”变量。JavaScript中变量声明又分为显示声明和隐式声明。其中显示声明中,被“声明”变量之前的关键词有var、let、cons......