《你不知道的JavaScript》--精读(九)

知识点

混合对象“类”

1.类理论

类/继承描述了一种代码的组织结构形式--一种在软件中对真实世界中问题领域的建模方法。编程

面向对象编程强调的是数据和操做数据的行为本质上是互相关联的(固然,不一样的数据有不一样的行为),所以好的设计就是把数据以及和它相关的行为打包(或者说封装)起来,这在正式的计算机科学中有时被称为数据结构。设计模式

举例来讲,用来表示一个单词或者短语的一串字符一般被称为字符串。字符就是数据。可是你关心的每每不是数据是什么,而是能够对数据作什么,因此能够应用在这种数据上的行为(计算长度、添加数据、搜索等等)都被设计成String类的方法。数据结构

全部字符串都是String类的一个实例,也就是说它是一个包裹,包含字符数据和咱们能够应用在数据上的函数。框架

咱们来看一个常见的例子,“汽车”能够被看做“交通工具”的一种特例,后者是更普遍的类。函数

咱们能够在软件中定义一个Vehicle类和一个Car类来对这种关系进行建模。工具

Vehicle的定义可能包含推动器(好比引擎)、载人能力等,这些都是Vehicle的行为。咱们在Vehicle中定义的是(几乎)全部类型的交通工具(飞机、火车和汽车)都包含的东西。ui

在咱们的软件中,对不一样的交通工具重复定义“载人能力”是没有意义的。相反咱们只在Vehicle中定义一次,定义Car时,只要声明它继承(或者扩展)了Vehicle的这个基础定义就行。Car的定义就是对通用Vehicle定义的特殊化。this

虽然Vehicle和Car会定义相同的方法,可是实例中的数据多是不一样的,好比每辆车独一无二的VIN(车辆识别号码),等待。spa

这就是类、继承和实例化。设计

类的另外一个核心概念是多态,这个概念是说父类的通用行为能够被子类用更特殊的行为重写。实际上,相对多态性容许咱们从重写行为中引用基础行为。

类理论强烈建议父类和子类使用相同的方法名来表示特定的行为,从而让子类重写父类。咱们以后会看到,在JavaScript代码中这样作会下降代码的可读性和健壮性。

1.1 “类”设计模式

1.2 JavaScript中的“类”

JavaScript属于哪一类呢?在至关长的一段时间里,JavaScript只有一些近似类的语法元素,(好比new和instanceof),不过在后来的ES6中新增了一些元素,好比class关键字。

这是否是意味着JavaScript中实际上有类呢?简单来讲,不是。

因为类是一种设计模式,因此你能够用一些方法近似实现类的功能。为了知足对于类设计模式的最广泛需求,JavaScript提供了一些近似类的语法。

虽然有近似类的语法,可是JavaScript的机制彷佛一直在阻止你使用类设计模式。在近似类的表象之下,JavaScript的机制其实和类彻底不一样。语法糖和JavaScript“类”库试图掩盖这个现实,可是你早晚会面对它:其余语言中的类和JavaScript中的“类”并不同。

总结一下,在软件设计中类是一种可选的模式,你须要本身决定是否在JavaScript中使用它。因为许多开发者都很是喜欢面向类的软件设计,以后咱们会介绍在JavaScript中实现类以及存在的一些问题。

2.类的机制

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

可是在这些语言中,你实际上并非直接操做Stack(除非建立一个静态类成员引用),Stack类仅仅是一个抽象的表示,它描述了全部“栈”须要作的事,可是它自己并非一个“栈”。你必须实例化Stack类而后才能对它进行操做。

2.1 建造

“类”和“实例”的概念来源于房屋建造。

建筑师会规划出一个建筑的全部特性:多宽、多高、多少个窗户以及窗户的位置,甚至连建造墙和房顶须要的材料都要计划好。在这个阶段他并不须要关心建筑会被建在哪,也不须要关心会建造多少个这样的建筑。

建筑师也不太关心建筑里的内容--家具、壁纸、吊扇等--他只关心须要用什么结构来容纳它们。

建筑蓝图只是建筑计划,它们并非真正的建筑,咱们还须要一个建筑工人来建造建筑。建筑工人会按照蓝图建造建筑。实际上,他会把规划好的特性从蓝图中复制到现实世界的建筑中。

完成后,建筑就成为了蓝图的物理实例,本质上就是对蓝图的复制。以后建筑工人就能够到下一个地方,把全部工做都重复一遍,再建立一份副本。

建筑和蓝图之间的关系是间接的。你能够经过蓝图了解建筑的结构,只观察建筑自己是没法得到这些信息的。可是若是你想打开一扇门,那就必须接触真实的建筑才行--蓝图只能表示门应该在哪,但并非真正的门。

一个类就是一张蓝图。为了得到真正能够交互的对象,咱们必须按照类来建造(也能够说实例化)一个东西,这个东西一般被称为实例,有须要的话,咱们能够直接在实例上调用方法并访问其全部公有数据属性。

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

你走进一栋建筑时,它的蓝图不太可能挂在墙上(尽管这个蓝图可能会保存在公共档案馆中)。相似地,你一般也不会是要一个实例对象来直接访问并操做它的类,不过只是能够判断出这个实例对象来自哪一个类。

把类和实例对象之间的关系看做是直接关系而不是间接关系一般更有助于理解。类经过复制操做被实例化为对象形式。

2.2 构造函数

类实例是由一个特殊的类方法构造的,这个方法名一般和类名相同,被称为构造函数。这个方法的任务就是初始化实例须要的全部信息(状态)。

举例来讲,思考下面这个关于类的伪代码(编造出来的语法):

class CoolGuy {
    specialTrick = nothing;
    CoolGuy(trick) {
        specialTrick = trick;
    }
    showOff() {
        output("Here's my trick: ",specialTrick);
    }
}
复制代码

咱们能够调用类构造函数来生成一个CoolGuy实例:

Joe = new CoolGuy('jumping rope');
Joe.showOff(); // Here's my trick: specialTrick
复制代码

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

显然,跳绳让乔成为了一个很是酷的家伙。

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

3.类的继承

在面向类的语言中,你能够先定义一个类,而后定义一个继承前者的类。

后者一般被称为“子类”,前者一般被称为“父类”。

定义好一个子类以后,相对于父类来讲它就是一个独立而且彻底不一样的类。子类会包含父类行为的原始副本,可是也能够重写全部继承的行为甚至定义新行为。

接下来,讲解一个稍有不一样的例子:不一样类型的交通工具。

首先回顾一下本章前面部分提出的Vehicle和Car类。思考下面关于继承的伪代码:

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

class Car inherits Vehicle {
    wheels = 4
    dirve() {
        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继承了通用的特性并根据自身类别修改了某些特性。汽车须要四个轮子,快艇须要两个发动机,所以它必须启动两个发动机的点火装置。

3.1 多态

Car重写了继承自父类drive()方法,可是以后Car调用了inherited:drive()方法,这代表Car能够引用继承来的原始drive()方法。快艇的pilot()方法一样引用了原始的drive()方法。

这个技术被称为多态或者虚拟多态。在本例中,更恰当的说法是相对多态。

多态是一个很是普遍的话题,咱们如今所说的“相对”只是多态的一个方面:任何方法均可以引用继承层次中高层的方法(不管高层的方法名和当前方法名是否相同)。之因此说“相对”是由于咱们并不会定义想要访问的绝对继承层次(或者说类),而是使用相对引用“查找上一层”。

在许多语言中可使用super来代替本例中的inherited:,它的含义是“超类”(superclass),表示当前类的父类/祖先类。

多态的另外一个方面是,在继承链的不一样层次中一个方法名能够被屡次定义,当调用方法时会自动选择合适的定义。

在子类(而不是它们建立的实例对象!)中也能够相对引用它继承的父类,这种相对引用一般被称为super。

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

3.2 多重继承

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

相比之下,JavaScript要简单得多:它自己并不提供“多重继承”功能。许多人认为这是件好事,由于使用多重继承的代价过高。然而这没法阻挡开发者们的热情,他们会尝试各类各样的方法来实现多重继承,咱们立刻就会看到。

4.混入

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

因为在其余语言中类表现出来的都是复制行为,所以JavaScript开发者也想出了一个方法来模拟类的复制行为,这个方法就是混入。接下来,咱们会看到两种类型的混入:显式和隐式。

4.1 显式混入

首先咱们来回顾一下以前提到的Vehicle和Car。因为JavaScript不会自动实现Vehicle到Car的复制行为,因此咱们须要手动实现复制功能。这个功能在许多库和框架中被称为extend(...),可是为了方便理解咱们称之为mixin(...)。

// 很是简单的mixin(...)例子:
Function mixin(sourceObj, targetObj) {
    for(var key in sourceObj) {
        // 只会在key不存在的状况下复制
        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!");
    }
})
复制代码

注意:咱们处理的已经再也不是类了,由于在JavaScript中不存在类,Vehicle和Car都是对象,供咱们分别进行复制和粘贴。

如今Car中就有了一份Vehicle属性和函数的副本了。从技术角度来讲,函数实际上没有被复制,复制的是函数的引用。因此,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.cool()并在Another的上下文中调用了它。最终的结果是Something.cool()中的赋值操做都会应用在Another对象上而不是Something对象上。

所以,咱们把Something的行为“混入”到了Another中。

虽然这类技术利用了this的从新绑定功能,可是Something.cool.call(this)仍然没法变成相对(并且更灵活的)引用,因此使用时千万要当心。一般来讲,尽可能避免使用这样的结构,以保证代码的整洁和可维护性。

总结

类是一种设计模式。许多语言提供了对于面向类软件设计的原生语法。JavaScript也有相似的语法,可是和其余语言中的类彻底不一样。

类意味着复制。

传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。

多态(在继承链的不一样层次名称相同可是功能不一样的函数)看起来彷佛是从子类引用父类,可是本质上引用的实际上是复制的结果。

JavaScript并不会(像类那样)自动建立对象的副本。

混入模式(不管显式仍是隐式)能够用来模拟类的复制行为,可是一般会产生丑陋而且脆弱的语法,好比显式伪多态(OtherObj.methodName.call(this,...)),这会让代码更加难懂而且难以维护。

此外,显式混入实际上没法彻底模拟类的复制行为,由于对象(和函数!别忘了函数也是对象)只能复制引用,没法复制被引用的对象或者函数自己。忽视这一点会致使许多问题。

总地来讲,在JavaScript中模拟类是得不偿失的,虽然能解决当前的问题,可是可能会埋下更多的隐患。

巴拉巴拉

最近懒癌犯了,感受又很久没有更新,一开始写这个的初衷也是但愿聚沙成塔,量变引发质变,但是总由于各类各样的缘由耽搁了,我慢慢的开始发现,不少时候难以坚持,实际上是由于原本的做息被打断,我本身是这样的缘由,好比说,我通常九点开始写这个,若是我下班晚了,就职性的不写了,若是我下班早了,极可能也不写了,由于躺着躺着就睡着了,仍是但愿本身能养成习惯吧,多看书,多写代码。

相关文章
相关标签/搜索