基于原型的面向对象
物体的世界
在一天中,开车去上班,坐在办公桌前执行任务,吃饭,在公园里散步,您通常可以操纵世界并与之互动,而不必了解控制世界的详细物理定律。 您可以将每天处理的各种系统视为单位或对象。 您理所当然地认为它们的复杂性,而专注于与它们的交互。
面向对象(OO)编程是一种创建功能相似的软件系统的尝试,它是一种功能强大且广受欢迎的软件开发建模工具。 它之所以受欢迎,是因为它反映了我们看待世界的方式:是可以相互交互并以各种方式进行操作的对象的集合。 OO编程的力量在于其两个核心原则:
-
封装形式
- 使开发人员可以隐藏其数据结构的内部工作原理,并揭示可用于创建模块化,适应性强的软件的可靠编程接口。 将其视为信息隐藏。 遗产
- 通过允许对象继承其他对象的封装行为来增强封装的能力。 将其视为信息共享。
这些原理对于大多数开发人员来说是众所周知的,因为每种主流编程语言都支持OO编程(并且在许多情况下是强制执行)。 尽管所有OO语言都以一种形式或另一种形式支持这两个核心原则,但多年来,至少有两种根本不同的定义对象的方式。
在本文中,了解原型OO编程和JavaScript对象模式的好处。
原型-什么? 类和原型
类提供对象的抽象定义,该定义为整个类或对象集合定义共享的数据结构和方法。 每个对象都定义为其类的实例。 还负责根据类的定义以及(可选)通过用户参数来构造类对象。
一个经典的例子是Point
类及其子Point3D
,分别用于定义二维和三维点。 清单1显示了这些类在Java代码中的外观。
清单1. Java Point
类
class Point {
private int x;
private int y;
static Point(int x, int y) {
this.x = x;
this.y = y;
}
int getX() {
return this.x;
}
int getY() {
return this.y;
}
void setX(int val) {
this.x = val;
}
void setY(int val) {
this.y = val;
}
}
Point p1 = new Point(0, 0);
p1.getX() // => 0;
p1.getY() // => 0;
// The Point3D class 'extends' Point, inheriting its behavior
class Point3D extends Point {
private int z;
static Point3D(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
int getZ() {
return Z;
}
void setZ(int val) {
this.z = val;
}
}
Point3D p2 = Point3D(0, 0, 0);
p2.getX() // => 0
p2.getY() // => 0
p2.getZ() // => 0
与按类定义对象相反,原型对象系统支持更直接的对象创建方法。 例如,在JavaScript中,对象是属性的简单列表。 每个对象都包含对另一个继承其行为的父对象或原型对象的特殊引用。 您可以模仿JavaScript中的Point
示例,如清单2所示。
清单2. JavaScript Point
类
var point = {
x : 0,
y : 0
};
point.x // => 0
point.y // => 0
// creates a new object with point as its prototype, inheriting point's behavior
point3D = Object.create(point);
point3D.z = 0;
point3D.x // => 0
point3D.y // => 0
point3D.z // => 0
经典对象系统和原型对象系统之间存在根本差异。 经典对象被抽象定义为概念组的一部分,并从其他类或对象组继承特征。 相反,原型对象被具体定义为特定对象,并从其他特定对象继承行为。
因此,基于类的OO语言具有双重性质,需要至少两个基本构造:类和对象。 由于这种双重性,随着基于类的软件的增长,复杂的类层次结构趋于发展。 通常无法预测将来将需要使用类的所有方式,因此需要不断重构类层次结构以促进更改。
基于原型的语言消除了对上述双重性的需求,并促进了对象的直接创建和操纵。 在没有对象受类约束的情况下,可以创建更松散地绑定的对象系统,这有助于维护模块性并减少重构的需求。
能够直接定义对象还为对象创建和操作增加了巨大的功能和简便性。 例如,在清单2中,您可以只用一行声明point
对象: var point = { x: 0, y: 0 };
。 有了这一行,您就有了一个完整的工作对象,该对象继承了JavaScript的Object.prototype
行为,例如toString
方法。 要扩展对象的行为,只需声明另一个对象,并将point
作为其原型。 相反,即使使用最简洁的经典OO语言,您也必须首先定义一个类,然后在拥有可操作对象之前实例化该类。 要继承,您必须定义另一个类以扩展第一个类。
原型模式在概念上更简单。 作为人类,我们经常考虑原型。 例如,在史蒂夫·耶格(Steve Yegge)的博客文章“通用设计模式”(请参阅参考资料 )中,他引用了一个美国足球运动员的例子,例如艾米特·史密斯(Emmitt Smith),他以其速度,敏捷性和剪切力成为所有人的原型。国家橄榄球联盟(NFL)的新球员。 然后,当一个特别出色的新跑者LT被拿起时,评论员说:
“ LT的腿像Emmitt。”
“他可以像艾米特一样在生产线上耕作。”
“但是他只差五分钟就跑了!”
评论员正在根据原型对象Emmitt Smith建模新对象LT。 在JavaScript中,这样的模型类似于清单3。
清单3. JavaScript模型
var emmitt = {
// ... properties go here
};
var lt = Object.create(emmitt);
// ... add other properties directly to lt
您可以将示例与经典建模进行对比,在经典建模中,您可以定义继承自FootballPlayer
类的RunningBack
类。 LT和emmitt将是RunningBack
实例。 这些类可能类似于Java代码中的清单4。
清单4.三个Java类
class FootballPlayer {
private string name;
private string team;
static void FootballPlayer() { }
string getName() {
return this.name;
}
string getTeam() {
return this.team;
}
void setName(string val) {
this.name = val;
}
void setTeam(string val) {
this.team = val;
}
}
class RunningBack extends FootballPlayer {
private bool offensiveTeam = true;
bool isOffesiveTeam() {
return this.offensiveTeam;
}
}
RunningBack emmitt = new RunningBack();
RunningBack lt = new RunningBack();
经典的模型带有相当多的概念开销,但没有细粒度控制类实例emmitt
和lt
你与原型模型得到的。 (公平地说, FootballPlayer
类不是100%必需的;可以在其中与下一个示例进行比较。)有时,这种开销可能会有帮助,但通常只是行李。
使用原型对象系统模拟经典建模非常容易。 (诚然,它也可以做相反的,虽然也许不容易。)例如,你可以创建一个对象footballPlayer
与其他runningBack
对象,从继承footballPlayer
为原型。 在JavaScript中,这些对象类似于清单5。
清单5. JavaScript建模
var footballPlayer = {
name : "";
team : "";
};
var runningBack = Object.create(footballPlayer);
runningBack.offensiveTeam = true;
您还可以创建另一个继承于footballPlayer
lineBacker
对象,如清单6所示。
清单6.对象继承
var lineBacker = Object.create(footballPlayer);
lineBacker.defensiveTeam = true;
如清单7所示,您可以通过添加到footballPlayer
对象来向lineBacker
和runningBack
对象添加行为。
清单7.添加行为
footballPlayer.run = function () { this.running = true };
lineBacker.run();
lineBacker.running; // => true
在这个例子中,你把footballPlayer
为一类。 您还可以为Emmitt和LT创建对象,如清单8所示。
清单8.创建对象
var emmitt = Object.create(runningBack);
emmitt.superbowlRings = 3;
var lt = Object.create(emmitt);
lt.mileRun = '5min';
因为lt
对象继承自emmitt
对象,所以您甚至可以将emmitt
对象视为一个类,如清单9所示。
清单9.继承和类
emmitt.height = "6ft";
lt.height // => "6ft";
如果要使用具有静态经典对象(例如Java代码)的语言来尝试上述示例,则必须使用装饰器模式,这需要更多概念上的开销,并且仍然无法直接从emmitt
对象继承作为一个实例。 相比之下,JavaScript等基于原型的语言中使用的属性模式可让您以更加自由的方式装饰对象。
JavaScript不是Java语言
JavaScript及其某些功能(例如原型对象)一直是不幸的历史错误和营销决策的受害者。 例如,Brendan Eich(JavaScript的父亲)在博客文章中讨论了为什么需要一种新语言:“高层工程管理人员的直言是该语言必须“看起来像Java”。 这排除了Perl,Python和Tcl以及Scheme。” 因此,JavaScript 看起来像Java代码,并且其名称链接到Java语言,这对于不熟悉这两者的人都感到困惑。 尽管JavaScript表面上看起来像Java语言,但从更深层次上讲,它与Java完全不同,这导致了人们的期望缺失。 从布伦丹·艾希(Brendan Eich):
我并不感到骄傲,但是我很高兴我选择了Scheme式的一流函数和Self-ish(尽管很单一)原型作为主要成分。 不幸的是,Java的影响,特别是y2k Date错误,还有原始对象与对象的区别(例如,字符串与字符串)。
无法满足的期望很难应对。 当您期望使用静态的企业级语言(例如Java语言),但最终使用的语言具有类似Java代码的语法,但其行为更像Scheme和Self时,您会感到惊讶。 如果您喜欢动态语言,这将是一个受欢迎的惊喜; 如果您不熟悉它们,或者对它们不熟悉,那么使用JavaScript编程可能会令人不快。
JavaScript还具有一些真正的优点:强制全局变量,作用域问题,分号插入, ==
的不一致行为等等。 针对这些问题,JavaScript程序员开发了一系列模式和最佳实践,以帮助开发可靠的软件。 下一节讨论了一些可以使用的模式,以及应避免使用的一些模式,以充分利用JavaScript的原型对象系统。
JavaScript对象模式
当试图使JavaScript看起来像Java代码时,它的设计者包括构造函数,这在古典语言中是必需的,但在原型语言中通常是不必要的开销。 考虑下面的模式,其中可以使用构造函数声明一个对象,如清单10所示。
清单10.声明一个对象
function Point(x, y) {
this.x = x;
this.y = y;
}
然后,可以使用类似于Java代码的new
关键字创建对象,如清单11所示。
清单11.创建对象
var p = new Point(3, 4);
p.x // => 3
p.y // => 4
在JavaScript中,函数也是对象,因此可以将方法添加到构造函数的原型中,如清单12所示。
清单12.添加一个方法
Point.prototype.r = function() {
return Math.sqrt((this.x * this.x) + (this.y * this.y));
};
连同构造函数一起,您可以使用伪古典继承模式,如清单13所示。
清单13.伪经典继承模式
function Point3D(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
Point3D.prototype = new Point(); // inherits from Point
Point3D.prototype.r = function() {
return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
};
尽管这肯定是在JavaScript中定义对象的有效方法(有时可能是最好的方法),但感觉有些笨拙。 与采用原型模式和纯粹以这种方式定义对象相比,它给代码增加了不必要的干扰。 回顾一下,您可以使用对象文字来定义对象,如清单14所示。
清单14.定义对象
var point = {
x: 1,
y: 2,
r: function () {
return Math.sqrt((this.x * this.x) + (this.y * this.y));
}
};
如清单15所示,然后使用Object.create
进行继承。
清单15.使用Object.create
继承
var point3D = Object.create(point);
point3D.z = 3;
point3D.r = function() {
return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
};
这种对象创建方法在JavaScript中感觉很自然,并突出了其原型对象的优点。 然而,原型和伪古典模式的一个缺点是它们不提供任何成员隐私。 有时,隐私无关紧要,有时却如此。 清单16显示了一种模式,允许您使用私有成员创建对象。 道格拉斯·克罗克福德(Douglas Crockford)在他的《 JavaScript:好的零件》一书中将其称为功能继承模式。
清单16.功能继承模式
var point = function(spec) {
var that = {};
that.getTimesSet = function() {
return timesSet;
};
that.getX = function() {
return spec.x;
};
that.setX = function(val) {
spec.x = val;
};
that.getY = function() {
return spec.y;
};
that.setY = function(val) {
spec.y = val;
};
return that;
};
var point3D = function(spec) {
var that = point(spec);
that.getZ = function() {
return spec.z;
};
that.setZ = function(val) {
spec.z = val;
};
return that;
};
构造函数用于生成对象,在内部定义私有成员,并通过将spec
传递给构造函数来创建实例,如清单17所示。
清单17.创建实例
var p = point({ x: 3, y: 4 });
p.getX(); // => 3
p.setX(5);
var p2 = point3D({ x: 1, y: 4, z: 2 });
p.getZ(); // => 2
p.setZ(3);
结论
本文只是介绍了原型OO编程的内容。 Self,Lua,Io和REBOL等许多其他语言也实现了原型模式。 原型模式可以用任何语言实现,包括静态类型的语言。 在设计需要简单性和灵活性的任何系统时,它也很有用。
原型OO编程提供了强大的功能和简便性,并且以非常清晰和优雅的方式实现了OO编程的目标。 它是JavaScript的资产之一,而不是疣。
翻译自: https://www.ibm.com/developerworks/web/library/wa-protoop/index.html
基于原型的面向对象