前端面试 JavaScript— JS如何实现继承?

Posted aiguangyuan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端面试 JavaScript— JS如何实现继承?相关的知识,希望对你有一定的参考价值。

第一种:借助call

function Parent()
    this.name='parent';  

function Child()
    Parent.call(this);
    this.type='child'  
;
console.log(new Child);
// name: "parent",type: "child"

 这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。

第二种:借助原型链

function Parent() 
    this.name='parent';
    this.play= [1, 2, 3]  

function Child() 
    this.type='child';  

Child.prototype=new Parent();

console.log(new Child());
// type: "child",__proto__:Parent

看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。

var s1=new Child();
var s2=new Child();
s1.play.push(4);

// console.log(s1.play);
// [1, 2, 3, 4]
// console.log(s2.play);
// [1, 2, 3, 4]

明明我只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象。那么还有更好的方式么?

第三种:将前两种组合

function Parent()  
    this.name = 'parent'; 
    this.play = [1, 2, 3]; 
;
function Child()  
    Parent.call(this); 
    this.type = 'child'; 
;

Child.prototype = new Parent(); 

var s1 = new Child();
var s2 = new Child(); 
s1.play.push(4); 

// console.log(s1.play);
// [1, 2, 3, 4]
// console.log(s2.play);
// [1, 2, 3]

之前的问题都得以解决,但是这里又徒增了一个新问题,那就是Parent的构造函数会多执行了一次,这是我们不愿看到的,那么如何解决这个问题?

第四种:组合继承方式一

function Parent () 
    this.name='parent';
    this.play= [1, 2, 3];  
;
function Child() 
    Parent.call(this);
    this.type='child';  
;

Child.prototype=Parent.prototype;

这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下:

var s1=new Child();
var s2=new Child();

console.log(s1);

打印的结果如下图所示:

子类实例的构造函数是Parent,显然这是不对的,应该是Child。

第五种:组合继承方式二

function Parent () 
    this.name='parent';
    this.play= [1, 2, 3];  
;

function Child() 
    Parent.call(this);
    this.type='child';  
;

console.log(new Child());

Child.prototype=Object.create(Parent.prototype);

console.log(new Child());

Child.prototype.constructor=Child;

console.log(new Child());

这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承。

ES6的extends被编译后的JavaScript代码

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));

核心是_inherits函数,可以看到它采用的依然也是第五种方式,即寄生组合继承方式,同时证明了这种方式的成功。不过这里加了一个Object.setPrototypeOf(subClass, superClass),这是用来干啥的呢?

答案是用来继承父类的静态方法,这也是原来的继承方式疏忽掉的地方。

追问:面向对象的设计一定是好的设计吗?

从设计思想上谈谈继承本身的问题

假如现在有不同品牌的车,每辆车都有drive、music、addOil这三个方法。

class Car
    constructor(id) 
        this.id=id;  
    
    drive()
        console.log("开车");  
    
    music()
        console.log("听音乐")  
    
    addOil()
        console.log("加油")  
    

class otherCar extends Car

现在可以实现车的功能,并且以此去扩展不同的车。

但是问题来了,新能源汽车也是车,但是它并不需要addOil()。

如果让新能源汽车的类继承Car的话,也是有问题的,俗称"大猩猩和香蕉"的问题。大猩猩手里有香蕉,但是我现在明明只需要香蕉,却拿到了一只大猩猩。也就是说加油这个方法,我现在是不需要的,但是由于继承的原因,也给到子类了。

继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。

当然你可能会说,可以再创建一个父类啊,把加油的方法给去掉,但是这也是有问题的,一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好。

那如何来解决继承的诸多问题呢?

用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式。

顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。

function drive()
    console.log("开车");

function music()
    console.log("听音乐")

function addOil()
    console.log("加油")
 
let car =compose(drive, music, addOil);
let newEnergyCar= compose(drive, music);

代码干净,复用性也很好。这就是面向组合的设计方式。

以上是关于前端面试 JavaScript— JS如何实现继承?的主要内容,如果未能解决你的问题,请参考以下文章

前端面试回顾---javascript的面向对象

前端javascript面试题目录汇总

前端面试基础回顾之深入JS继承

前端面试

js的5种继承方式——前端面试

前端面试题总结二(js原型继承)