第二部分 类

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)仍然无法变成相对引用,所以使用时千万小心。应避免使用这样的结构。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容