this 和对象原型
对象
1 类理论
类/继承描述了一种代码的组织结构形式------一种在软件中对真实世界中问题领域的建模方法。
面向对象编程强调的是数据和操作数据的行为本质上是互相关联的(当然,不同的数据有不同的行为),因为好的设计就是把数据以及和它相关的行为打包(或者说封装)起来。这在正式的计算机科学中有时被称为数据结构。
1.1 "类"设计模式
我们可能从来没把类作为设计模式来看待,讨论得最多的是面向对象设计模式,比如迭代器 模式、观察者模式、工厂模式、单例模式,等等。从这个角度来说,我们似乎是在(低级)面向对象类的基础上实现了所有(高级)设计模式,似乎面向对象是优秀代码的基础。
1.2 JavaScript中的"类"
在相当长一段时间里,JavaScript只有一些近似类的语法元素(比如new和instanceof),不过在ES6中新增了一些元素,比如class关键字。
但不意味着JS实际上有类。
但由于类是一种设计模式,我们可以通过一些方法近似实现类的功能。JS中也提供了近似类的语法。但JS的机制视乎在阻止我们使用类。
2 类的机制
在许多面向类的语言中,"标准库"会提供Stack类,它是一种"栈"数据结构(支持压入、弹出,等等)。Stack 类内部会有一些变量来存储数据,同时会提供一些公有的可访问行为("方法"),从而让我们的代码可以和(隐藏的)数据进行交互(比如添加、删除数据)。
但是在这些语言中,我们实际上并不是直接操作Stack(除非创建一个静态类成员引用,这超出了我们的讨论范围)。Stack 类仅仅是一个抽象的表示,它描述了所有"栈"需要做的事,但是它本身并不是一个"栈"。我们必须先实例化Stack 类然后才能对它进行操作。
2.1 建造
"类"和"实例"的概念来源于房屋建造。
类相当于建筑蓝图,蓝图中描述建筑所有的特性:宽、高、房屋结构、建造材料等等。
实例就是工人根据蓝图建造出来的物理建筑。
2.2 构造函数
类实例是由一个特殊的类方法构造的,这个方法名通常和类名相同,被称为构造函数。这个方法的任务就是初始化实例需要的所有信息(状态)。
参考下面关于类的伪代码(编造出来的语法):
js
class CoolGuy {
specialTrick = nothing;
CoolGuy(trick) {
specialTrick = trick;
}
showOff() {
output("Here's my trick: ", specialTrick);
}
}
通过调用类构造函数生成一个CoolGuy实例:
js
Joe = new CoolGuy("jumping rope");
Joe.showOff(); // Here's my trick: jumping rope
注意,CoolGuy类有一个CoolGuy()构造函数,执行new CoolGuy()时实际上调用的就是它。构造函数会返回一个对象(也就是类的一个实例),之后我们可以在这个对象上调用showOff()方法,来输出指定CoolGuy的特长。
类构造函数属于类,而且通常和类同名。此外,构造函数大多需要用new来调用。
3 类的继承
在面向类的语言中,我们可以先定义一个类,然后再定义一个继承前者的类。后者通常被称为"子类" ,前者通常被称为"父类"。
定义好一个子类之后,相对于父类来说它就是一个独立并且完全不同的类。子类会包含父类行为的原始副本,但是也可以重写所有继承的行为甚至定义新的行为。
参考下面关于类继承的伪代码:
js
class Vehicle {
engines = 1;
ignition() {
output("Turning on my engine.")
}
drive() {
ignition();
outPut("Steering and moving forward!")
}
}
class Car inherits Vehicle {
wheels = 4
drive(){
inherits: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重写了继承自父类的driver()方法,但是之后Car调用了inherited:driver()方法,这表明Car可以引用继承来的原始drive()方法。
这个技术被称为多态或者虚拟多态。在本例中,更恰当的说法是相对多态。
多态是一个非常广泛的话题,我们现在所说的"相对"只是多态的一个方面:任何方法都可以引用继承层次中高层的方法(无论高层的方法名和当前方法名是否相同)。之所以说"相对"是因为我们并不会定义想要访问的绝对继承层次(或者说类),而是使用相对引用"查找上一层"。
在许多语言中可以使用super来代替本例中的inherited:,它的含义是"超类"(superclass),表示当前类的父类/祖先类。
多态的另一方面是,在继承链的不同层次中一个方法名可以被多次定义,当调用方法时会自动选择合适的定义。
在之前的代码中就有两个这样的例子:drive() 被定义在Vehicle 和Car 中,ignition() 被定义在Vehicle 和SpeedBoat 中。
我们可以在ignition() 中看到多态非常有趣的一点。在pilot() 中通过相对多态引用了(继承来的)Vehicle 中的drive()。但是那个drive() 方法直接通过名字(而不是相对引用)引用了ignotion() 方法。
语言引擎会使用哪个ignition()呢?实际上ignition() 方法定义的多态性取决于你是在哪个类的实例中引用它。
在子类(而不是它们创建的实例对象!)中也可以相对引用它继承的父类,这种相对引用通常被称为super。
多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制。
3.2 多重继承
有些面向类的语言允许继承多个"父类"。多重继承意味着所有父类的定义都会被复制到子类中。
将多个父类的功能组合到一起,看似是一个非常有用的功能,但当父类中都定义了相同的方法名的话,子类就很难分辨引用哪一个了。
JS本身不提供"多重继承"功能,但由许多热情的开发者们尝试各种办法来实现多重继承。
4 混入
在继承或者实例化时,JS的对象机制并不会自动执行复制行为。简单来说,JS中只有对象,并不存在可以被实例化的"类"。一个对象并不会复制到其他对象,它们会被关联起来。
由于在其他语言中类变现出来的都是复制行为,因此JS开发者引入想出了一个方法来模拟类的复制行为,这个方法就是混入。
4.1 显示混入
手动实现复制功能
js
// 非常简单的mixin(..)例子:
function mixin(sourceObj, targetObj) {
for (var key in sourceObj) {
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Vehicle = {
engined: 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 中就有了一份Vehicle 属性和函数的副本了。从技术角度来说,函数实际上没有被复制,复制的是函数引用。所以,Car 中的属性ignition 只是从Vehicle 中复制过来的对于ignition() 函数的引用。相反,属性engines 就是直接从Vehicle 中复制了值1。
Car 已经有了drive 属性(函数),所以这个属性引用并没有被mixin 重写,从而保留了 Car 中定义的同名属性,实现了"子类"对"父类"属性的重写(参见mixin(..) 例子中的if 语句)
- 再说多态
Vehicle.drive.call(this)就是我们所说的显示多态,之前的伪代码语句inherited:drive(),我们称之为相对多态。
JS(ES6前)没有想对多态的机制。所以,由于Car和Vehicle中都有driver()函数,为了指明调用对象,我们必须使用绝对引用。
在支持相对多态的面向类的语言中,Car和Vehicle之间的联系只在类定义的开头被创建,从而只需要在这一个地方维护两个类的联系。
但是在JavaScript 中(由于屏蔽)使用显式伪多态会在所有需要使用(伪)多态引用的地方创建一个函数关联,这会极大地增加维护成本。此外,由于显式伪多态可以模拟多重继承,所以它会进一步增加代码的复杂度和维护难度。
- 混合复制
回顾之前提到的mixin(..)函数:
js
// 非常简单的mixin(..)例子:
function mixin(sourceObj, targetObj) {
for (var key in sourceObj) {
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
mixin(..)会遍历sourceObj的属性,如果在targetObj没有这个属性就会进行复制。由于我们是在目标对初始化后才进行复制,因此一定要小心不要覆盖目标对象的原有属性。
复制操作完成后,两个对象就分离了,对其中一个对象中添加属性进行操作不会影响另外一个。
由于Car和Vehicle两个对象引用的是同一个函数,因此这种复制(或者是混入)实际上并不能完全模拟面向的语言中的复制。
- 寄生继承
显示混入模式的一种变体被称为"寄生继承",它既是显示的又是隐式的。
下面是它的工作原理:
js
// "传统的JS类"Vehicle
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();
// Turning on my engine.
// Steering and moving forward!
// Rolling on all 4 wheels!
4.2 隐式混入
隐式混入和之前提到的显示伪多态很像,因此也具备同样的问题。
js
var Something = {
cool: function () {
this.greething = "Hello World";
this.count = this.count ? this.count + 1 : 1;
},
};
Something.cool();
Something.greething; // "Hello World"
Something.count; // 1
var Anoyher = {
cool: function () {
// 隐式把Something混入Another
Something.cool.call(this);
},
};
Another.cool();
Another.greething; // "Hello World"
Another.count; // 1
通过this的重新绑定功能,调用Something.cool.call(this),将Something的行为"混入"到Another,但Something.cool.call(this)任然无法变为相对(而且更加灵活的)引用,使用时应尽量避免这样的结构,以保证代码的整洁和可维护性。