第二部分 类

2018-05-23  本文已影响0人  将军肚

4.1 类理论

面向对象编程强调的是数据和操作数据的行为本质上是互相关联的,因此好的设计就是把数据以及和它相关的行为打包起来。有说这被称为数据结构。

举例,用来表示一个单词或者短语的一串字符通常被称为字符串。字符就是数据。但是你关心的往往不是数据是什么,而是可以对数据做什么,所以可应用在这种数据上的行为都被设计成String类的方法。

类理论强烈建议父类和子类使用相同的方法名来表示特定的行为,从而让子类重写父类。在JS中这样做会降低代码的可读性和健壮性。

4.1.2 JS中的 “类”

虽然有近似类的语法,但是JS的机制似乎一直在阻止你使用类设计模式。在近似的表象之下,JS的机制其实和类完全不同。语法糖和JS类库试图掩盖这个现实,但是你迟早会面对它:其他语言中的类和JS中的“类”并不一样。

4.2 类的机制

在许多面向类的语言中,“标准库”会提供Stack类,它是一种“栈”数据结构(支持压入,弹出,等等)。Stack类内部会有一些变量来存储数据,同时会提供一些公有的可访问行为(方法),从而让你的代码可以和(隐藏的)数据进行交互(比如添加,删除数据)。

4.2.1 建造

“类”和“实例”的概念来源于房屋建造。
建筑和蓝图之间的关系是间接的。一个类就是一张蓝图。为了获得真正可交互的对象,我们必须按照类来建造(实例化)一个东西,这个东西通常被称为实例,有需要的话,可直接在实例上调用方法并访问其所有公有数据属性。

这个对象就是类中描述的所有特性的一份副本。

把类和实例对象之间的关系看作是直接关系而不是间接关系通常更好理解,类通过复制操作被实例化为对象形式

4.2.2 构造函数

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

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

4.3 类的继承

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

定义好一个子类后,相对于父类来说它就是一个独立且完全不同的类。子类会包含父类行为的原始副本,但是也可重写所有继承的行为甚至定义新行为。

非常重要的一点是,我们讨论的父类和子类并不是实例。
我们可把父类和子类称为父类DNA和子类DNA。我们需要根据这些DNA来创建(或实例化)一个人,然后才能和他进行沟通。

class Vehicle {
    engines = 1
    ignition() {
        output("Turning on my engine.");
    }
    drive() {
        ignition();
        output("Steering and moving forward!")
    }
}

class Car inherits Vehicle {
    wheels = 4;
    drive() {
        inherited: drive()
        output( "Rolling on all", wheels, "wheels!" )
    }
}

class SpeedBoat inherits Vehicle {
    engines = 2
    ignition() {
        output( "Turning on my", engines, " engines" )
    }
    pilot() {
        inherited: drive()
        output ("Speeding through the water with ease!")
    }
}

我们定义Vehicle类来假设一种发动机,一种点火方式,一种驾驶方法。但你不可能制造一个通用的“交通工具”,因为这个类只是一个抽象的概念。

接下来定义了两类具体的交通工具:Car 和 SpeedBoat 。它们都从 Vehicle 继承了通用的特性并根据自身类别修改了某些特性。汽车需要四个轮子,快艇需要两个发动机

4.3.1 多态

Car 重写了继承自父类的drive() 方法,但之后Car调用了继承的inherited:drive() 方法,这表明Car 可引用继承来的原始drive()方法。快艇的pilot() 方法同样引用了原始drive() 方法。

这个技术被称为多态或者虚拟多态,或相对多态。

多态并不表示子类和分类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制。

4.3.2 多重继承

211526478759_.pic.jpg

这个机制带来了复杂的问题,上例中如果A中有drive() 并且B 和 C 都重写了这个方法(多态),那D应当选择哪个版本呢?

相比之下,JS要简单得多:它本身并不提供“多重继承”。但开发者尝试各种办法来实现多重继承,后面会看到。

4.4 混入

在继承或者实例化时,JS的对象机制并不会自动执行复制行为。简单来说,JS中只有对象,并不存在可被实例化的“类”。一个对象并不会被复制到其他对象,它们会被关联起来。

由于在其他语言中类表现为复制行为,因此JS开发者模拟出了复制行为,就是混入。后面有两种类型的混入:显式和隐式。

4.4.1 显式混入

由于JS不会自动实现前例Vehicle到Car的复制行为,所有我们需要手动实现复制功能。这个功能在许多库和框架中被称为extend(),但是为了方便理解我们称为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() {
        console.log("Turning on my engine");
    },
    drive(){
        this.ignition();
        console.log("Steering and moving forward!");
    }
};
var Car = mixin( Vehicle, {
    wheels: 4,
    drive(){
        Vehicle.drive.call(this);
        console.log("Rolling on all " + this.wheels + " wheels!");
    }
});

有一点需要注意,我们处理的已经不再是类了,因为在JS中不存在类,Vehicle 和 Car 都是对象,供我们分别进行复制和粘贴。

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

1.再说多态

分析这条语句Vehicle.drive.call(this)。这就是显式多态。在之前的伪代码中对应的语句是inherited:drive(),我们称之为相对多态。
ES6之前,没有想对多态的机制。由于Car和Vehicle中都有drive() 函数,为了指明调用对象,我们必须使用绝对引用。通过名称显式指定Vehicle对象并调用它的drive() 函数。

但是如果直接执行Vehicle.drive(),函数调用中的this会被绑定到Vehicle对象而不是Car对象,这并不是我们想要的。因此,使用.call(this)

如果函数Car.drive() 的名称标识符并没有和Vehicle.drive()重叠(屏蔽)的话,我们就不需要实现方法多态,因为调用mixin()时会把函数Vehicle.drive()的引用复制到Car中,因此我们可直接访问this.drive()。正是由于存在标识符重叠,所有必须使用更加复杂的显式多态方法。

在支持相对多态的面向类的语言中,Car和Vehicle之间的联系只在类定义的开头被创建,从而只需要在这一个地方维护两个类的联系。

但是在JS中(由于屏蔽)使用显式伪多态会在所有需要使用(伪)多态引用的地方创建一个函数关联,这会极大地增加维护成本。

使用伪多态通常会导致代码变得更加复杂,难以阅读并且难以维护,因此应当尽量避免使用显式伪多态,因为这样做往往得不偿失。

2.混合复制

回顾mixin() 函数,它会遍历sourceObj的属性,如果在targetObj(本例是Car)没没有这个属性就会进行复制。由于我们是在目标对象初始化之后才进行复制,因此一定要小心不要覆盖目标对象的原有属性。

如果我们先进行复制然后对Car进行特殊化的话。就可跳过存在性检查。不过并不好用且效率低:

// 另一种混入函数,可能有重写风险
function mixin(sourceObj, targetObj) {
    for(var key in sourceObj) {
        targetObj[key] = sourceObj[key];
    }
    return targetObj;
}
var Vehicle = {
    //...
}

// 首先创建一个空对象并把Vehicle的内容复制进去
var Car = mixin(Vehicle, {});

//然后把新内容复制到Car中
mixin({
    wheels: 4,
    drive(){
        // ...
    }
}, Car)

这两种方法都可把不重叠的内容从Vehicle中显性复制到Car中。由于两个对象引用的是同一个函数,因此这种复制实际上并不能完全模拟面向类的语言中的复制。
JS中的函数无法真正的复制,所以你只能复制对共享函数对象的引用。

一定要注意,只在能提高代码可读性的前提下使用显式混入,避免使用增加代码理解难度或让对象关系更复杂的模式。

如果使用混入时感觉越来越困难,那或许你应该停止使用它了。

3.寄生继承

显式混入的一种变种被称为“寄生继承”。即是显式又是隐式的。

function Vehicle() {
    this.engines = 1;
}
Vehicle.prototype.ignition = function() {
    console.log("Turning on my engine.");
};
Vehicle.prototype.drive = function () {
    this.ignition();
    console.log("Steering and moving forward!");
}

// 寄生类 Car
function Car() {
    // 首先,car是一个Vehicle
    var car = new Vehicle();
    // 接着我们对car 进行定制
    car.wheels = 4;

    // 保存到Vehicle::drive() 的特殊引用
    var vehDrive = car.drive;
    
    // 重写 Vehicle::drive();
    car.drive = function() {
        vehDrive.call(this);
        console.log("Rolling on all " + this.wheels + " wheels!");
    }
    return car;
}
var myCar = new Car();

myCar.drive();

4.4.2 隐式混入

隐式混入和之前的显式伪多态很像,所有也具备相同的问题。

var Something = {
    cool(){
        this.greeting = "Hello World!"=;
        this.count = this.count ? this.count + 1 : 1;
    }
}

var Another = {
    cool(){
        //隐式把Someing混入Another
        Something.cool.call(this);
    }
}

虽然这类技术利用了this的重新绑定功能,但是Something.cool.call(this)仍然无法变成相对引用,所以使用时千万小心。应避免使用这样的结构。

上一篇 下一篇

猜你喜欢

热点阅读