懂王系列(四)之彻底搞懂JavaScript类

518 阅读4分钟

作为一名前端小白,不知道大家是否遇到和我一样的问题。看了一道面试题的解析,当时觉得会了,可是过两天以后再看又不会了;盲目追求各种新技术,感觉什么都会点,但是一上手就不行了... 痛定思痛后,我终于认识到了问题所在,开始专注于基本功的修炼。近半年来通读了(其实是囫囵吞枣)《JavaScript高级程序设计》、《你不知道的JavaScript上、中、下》等书籍,本系列文章是我读书过程中对知识点的一些总结。喜欢的同学记得帮我点个赞😁。

懂王系列(一)之彻底搞懂JavaScript函数执行机制
懂王系列(二)之彻底搞懂JavaScript作用域
懂王系列(三)之彻底搞懂JavaScript对象
懂王系列(四)之彻底搞懂JavaScript类
懂王系列(五)之彻底搞懂JavaScript原型
懂王系列(六)之彻底搞懂JavaScript中的this
懂王系列(七)之彻底搞懂JavaScript数据类型
懂王系列(八)之彻底搞懂JavaScript语句
懂王系列(九)之彻底搞定JavaScript类型转换

1. JavaScript中的“类”

由于类是一种设计模式,所以 JavaScript 用一些方法)近似实现了类的功能法。

2. 类的机制

2.1 构造函数

类实例是由一个特殊的类方法构造的,这个方法名通常和类名相同,被称为构造函数。这个方法的任务就是初始化实例需要的所有信息

class CoolGuy { 
    specialTrick = nothing 
    CoolGuy( trick ) { 
        specialTrick = trick 
    } 
    showOff() { 
        output( "Here's my trick: ", specialTrick ) 
    } 
}

CoolGuy 类有一个 CoolGuy() 构造函数,执行 new CoolGuy() 时实际上调用的就是它。构造函数会返回一个对象(也就是类的一个实例),之后我们可以在这个对象上调用showOff() 方法,来输出指定 CoolGuy 的特长

类构造函数属于类,而且通常和类同名。此外,构造函数大多需要用 new 来调,这样语言引擎才知道你想要构造一个新的类实例

2.2 继承

在面向类的语言中,你可以先定义一个类,然后定义一个继承前者的类。后者通常被称为“子类”,前者通常被称为“父类”。

子类是一个单独的个体。虽然子类会继承父类许多特性,但是他是一个独一无二的存在。二者之间没有直接的联系。
定义好一个子类之后,相对于父类来说它就是一个独立并且完全不同的类。子类会包含父类行为的原始副本,但是也可以重写所有继承的行为甚至定义新行为。

3. 多态

在继承链的不同层次中一个方法名可以被多次定义,当调用方法时会自动选择合适的定义

3.1 多重继承

有些面向类的语言允许你继承多个“父类”。多重继承意味着所有父类的定义都会被复制到子类中。

从表面上来,对于类来说这似乎是一个非常有用的功能,可以把许多功能组合在一起。然而,这个机制同时也会带来很多复杂的问题。如果两个父类中都定义了 drive() 方法的话,子类引用的是哪个呢?难道每次都需要手动指定具体父类的 drive() 方法吗?这样多态继承的很多优点就不存在了

JavaScript 要简单得多:它本身并不提供“多重继承”功能。许多人认为这是件好事,因为使用多重继承的代价太高。

但是我们可以使用混入来实现多重继承。

4. 混入

4.1 显示混入

// 非常简单的 mixin(..) 例子 : 
function mixin( sourceObj, targetObj ) { 
    for (var key in sourceObj) { 
        // 只会在不存在的情况下复制
        if (!(key in targetObj)) { 
            targetObj[key] = sourceObj[key]; 
        } 
    } 
    return targetObj; 
}

使用混入

var Vehicle = { 
    engines: 1, 
    ignition: function() { 
        console.log( "Turning on my engine." ); 
    }, 
    drive: function() { 
        this.ignition(); 
        console.log( "Steering and moving forward!" ); 
    } 
}; 
var Car = mixin( Vehicle, { 
    wheels: 4, 
    drive: function() { 
        Vehicle.drive.call( this ); 
        console.log( 
            "Rolling on all " + this.wheels + " wheels!" 
        ); 
    } 
} );

从技术角度来说,函数实际上没有被复制,复制的是函数引用。所以,Car 中的属性 ignition 只是从 Vehicle 中复制过来的对于 ignition() 函数的引用。相反,属性 engines 就是直接从 Vehicle 中复制了值 1

Car 已经有了 drive 属性(函数),所以这个属性引用并没有被 mixin 重写,从而保留了Car 中定义的同名属性,实现了“子类”对“父类”属性的重写

4.2 隐式混入

var Something = { 
    cool: function() { 
        this.greeting = "Hello World"; 
        this.count = this.count ? this.count + 1 : 1; 
    } 
};
Something.cool(); 
Something.greeting; // "Hello World" 
Something.count; // 1 
var Another = { 
    cool: function() { 
        // 隐式把 Something 混入 Another 
        Something.cool.call( this ); 
    } 
}; 
Another.cool(); 
Another.greeting; // "Hello World" 
Another.count; // 1 (count 不是共享状态)

在构造函数调用或者方法调用中使用 Something.cool.call( this )把 Something 的行为“混入”到了 Another 中

虽然这类技术利用了 this 的重新绑定功能,但是 Something.cool.call( this ) 仍然无法变成相对(而且更灵活的)引用,所以使用时千万要小心。通常来说,尽量避免使用这样的结构,以保证代码的整洁和可维护性。