首先简单回顾一下上一章的结论:[[Prototype]] 机制就是指对象中的一个内部链接引用另一个对象。
如果在第一个对象上没有找到需要的属性或者方法引用,引擎就会继续在 [[Prototype]] 关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的 [[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。
换句话说,JavaScript 中这个机制的本质就是对象之间的关联关系。
面向委托的设计
为了更好地学习如何更直观地使用 [[Prototype]],我们必须认识到它代表的是一种不同于类的设计模式。
我们需要试着把思路从类和继承的设计模式转换到委托行为的设计模式。如果你在学习或者工作的过程中几乎一直在使用类,那转换思路可能不太自然并且不太舒服。你可能需要 多重复几次才能熟悉这种思维模式。
类理论
定义一个通用父(基)类,可以将其命名为 Task,在 Task 类中定义所有任务都有的行为。接着定义子类 XYZ 和 ABC,它们都继承自 Task 并且会添加一些特殊的行为来处理对应的任务。
类设计模式鼓励你在继承时使用方法重写(和多态),比如说在 XYZ 任务中重写 Task 中定义的一些通用方法,甚至在添加新行为时通过 super 调用这个方法的原始版本。你会发现许多行为可以先“抽象”到父类然后再用子类进行特殊化(重写)。
委托理论
但是现在我们试着来使用委托行为而不是类来思考同样的问题。
首先你会定义一个名为 Task 的对象(和许多 JavaScript 开发者告诉你的不同,它既不是类也不是函数),它会包含所有任务都可以使用(写作使用,读作委托)的具体行为。接着, 对于每个任务(“XYZ”、“ABC”)你都会定义一个对象来存储对应的数据和行为。你会把特定的任务对象都关联到 Task 功能对象上,让它们在需要的时候可以进行委托。
基本上你可以想象成,执行任务“XYZ”需要两个兄弟对象(XYZ 和 Task)协作完成。但是我们并不需要把这些行为放在一起,通过类的复制,我们可以把它们分别放在各自独立的对象中,需要时可以允许 XYZ 对象委托给 Task。
下面是推荐的代码形式:
Task = {
setID: function(ID) { this.id = ID; },
outputID: function() { console.log( this.id ); }
};
// 让XYZ委托Task
XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
this.setID( ID );
this.label = Label;
};
XYZ.outputTaskDetails = function() {
this.outputID();
console.log( this.label );
};
// ABC = Object.create( Task );
// ABC ... = ...
在这段代码中,Task 和 XYZ 并不是类(或者函数),它们是对象。XYZ 通过 Object. create(..) 创建,它的 [[Prototype]] 委托了 Task 对象。
相比于面向类(或者说面向对象),我会把这种编码风格称为“对象关联”(OLOO,objects linked to other objects)。我们真正关心的只是 XYZ 对象(和 ABC 对象)委托了 Task 对象。
对象关联风格的代码还有一些不同之处。
1、在类设计模式中,我们故意让父类(Task)和子类(XYZ)中都有outputTask方法,这样就可以利用重写(多态)的优势。在委托行为中则恰好相反:我们会尽量避免在 [[Prototype]] 链的不同级别中使用相同的命名,否则就需要使用笨拙并且脆弱的语法来消除引用歧义。
这个设计模式要求尽量少使用容易被重写的通用方法名,提倡使用更有描述性的方法名,尤其是要写清相应对象行为的类型。这样做实际上可以创建出更容易理解和维护的代码,因为方法名(不仅在定义的位置,而是贯穿整个代码)更加清晰(自文档)。
2、this.setID(ID);XYZ中的方法首先会寻找XYZ自身是否有setID(..),但是XYZ中并没有这个方法名,因此会通过 [[Prototype]] 委托关联到 Task 继续寻找,这时就可以找到 setID(..) 方法。此外,由于调用位置触发了 this 的隐式绑定规则,因此虽然 setID(..) 方法在 Task 中,运行时 this 仍然会绑定到 XYZ,这正是我们想要的。 在之后的代码中我们还会看到 this.outputID(),原理相同。
换句话说,我们和 XYZ 进行交互时可以使用 Task 中的通用方法,因为 XYZ 委托了 Task。
委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一个对象(Task)。
这是一种极其强大的设计模式,和父类、子类、继承、多态等概念完全不同。在你的脑海中对象并不是按照父类到子类的关系垂直组织的,而是通过任意方向的委托关联并排组织的。
互相委托(禁止):
你无法在两个或两个以上互相(双向)委托的对象之间创建循环委托。如果你把 B 关联到 A 然后试着把 A 关联到 B,就会出错。
很遗憾(并不是非常出乎意料,但是有点烦人)这种方法是被禁止的。如果你引用了一个两边都不存在的属性或者方法,那就会在 [[Prototype]] 链上产生一个无限递归的循环。
比较思维模型
现在你已经明白了“类”和“委托”这两种设计模式的理论区别,接下来我们看看它们在思维模型方面的区别。
下面是典型的(“原型”)面向对象风格:
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "I am " + this.me;
};
function Bar(who) {
Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();
子类 Bar 继承了父类 Foo,然后生成了 b1 和 b2 两个实例。b1 委托了 Bar.prototype,后者
委托了 Foo.prototype。
下面我们看看如何使用对象关联风格来编写功能完全相同的代码:
Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
Bar = Object.create( Foo );
Bar.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();
这段代码中我们同样利用 [[Prototype]] 把 b1 委托给 Bar 并把 Bar 委托给 Foo,和上一段代码一模一样。我们仍然实现了三个对象之间的关联。
但是非常重要的一点是,这段代码简洁了许多,我们只是把对象关联起来,并不需要那些既复杂又令人困惑的模仿类的行为(构造函数、原型以及 new)。
类与对象
我们已经看到了“类”和“行为委托”在理论和思维模型方面的区别,现在看看在真实场景中如何应用这些方法。
首先看看 Web 开发中非常典型的一种前端场景:创建 UI 控件(按钮、下拉列表,等等)。
控件“类”
下面这段代码展示的是如何在不使用任何“类”辅助库或者语法的情况下,使用纯 JavaScript 实现类风格的代码:
// 父类
function Widget(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
Widget.prototype.render = function($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
};
// 子类
function Button(width,height,label) {
// 调用“super”构造函数
Widget.call( this, width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
}
// 让 Button“继承”Widget
Button.prototype = Object.create( Widget.prototype );
// 重写 render(..)
Button.prototype.render = function($where) {
//“super”调用
Widget.prototype.render.call( this, $where );
this.$elem.click( this.onClick.bind( this ) );
};
Button.prototype.onClick = function(evt) {
console.log( "Button '" + this.label + "' clicked!" );
};
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = new Button( 125, 30, "Hello" );
var btn2 = new Button( 150, 40, "World" );
btn1.render( $body );
btn2.render( $body );
} );
在面向对象设计模式中我们需要先在父类中定义基础的 render(..),然后在子类中重写它。子类并不会替换基础的 render(..),只是添加一些按钮特有的行为。
可以看到代码中出现了丑陋的显式伪多态,即通过 Widget.call 和 Widget. prototype.render.call 从“子类”方法中引用“父类”中的基础方法。
委托控件对象
下面的例子使用对象关联风格委托来更简单地实现 Widget/Button:
var Widget = {
init: function(width,height){
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
},
insert: function($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
}
};
var Button = Object.create( Widget );
Button.setup = function(width,height,label){
// 委托调用
this.init( width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
};
Button.build = function($where) {
// 委托调用
this.insert( $where );
this.$elem.click( this.onClick.bind( this ) );
};
Button.onClick = function(evt) {
console.log( "Button '" + this.label + "' clicked!" );
};
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = Object.create( Button );
btn1.setup( 125, 30, "Hello" );
var btn2 = Object.create( Button );
btn2.setup( 150, 40, "World" );
btn1.build( $body );
btn2.build( $body );
} );
在委托设计模式中,除了建议使用不相同并且更具描述性的方法名之外,还要通过对象关联避免丑陋的显式伪多态调用(Widget.call 和 Widget.prototype.render.call),代之以简单的相对委托调用 this.init(..) 和 this.insert(..)。
从语法角度来说,我们同样没有使用任何构造函数、.prototype 或 new,实际上也没必要使用它们。
对象关联可以更好地支持关注分离(separation of concerns)原则。
更简洁的设计
我们不需要基类来“共享”两个实体之间的行为,因为委托足以满足我们需要的功能。
同样,前面提到过,我们也不需要实例化类,因为它们根本就不是类,它们只是对象。此外,我们也不需要合成,因为两个对象可以通过委托进行合作。
最后,我们避免了面向类设计模式中的多态。我们在不同的对象中没有使用相同的函数名,这样就不需要使用丑陋的显示伪多态。相反,不同的函数名可以更好地描述它们的行为。
总结:我们用一种(极其)简单的设计实现了同样的功能,这就是对象关联风格代码和行为委托设计模式的力量。