委托理论
相比于面向类(或者说面向对象),这种风格可以被称之为“对象关联”(OLOO,object linked to other objects)
对象关联风格的代码不同之处
- 应该把状态保存在委托者,而不是委托目标上。
-
避免
[[prototype]]
链的不同层级上使用相同的命名。 - 触发
this
的隐式绑定规则达到我们想要的调用效果。
委托行为意味着某些对象在找不到属性或者方法引用时会把这个请求委托给另一个对象
并且,委托最好是在内部实现,而不是直接暴露出去。
Task = {
setID : function(ID) { this.id = ID},
outputID : function() { console.log(this.id);}
}
XYZ = Object.create(Task);
XYZ.prepareTask = function(ID, Label) {
// 我们把委托隐藏在 API 的内部!!!
// 也就是说,XYZ.prepareTask(..) 会委托 Task.setID(..)
this.setID(ID);
this.label = Label;
}
XYZ.outputTaskDetails = function() {
this.outputID();
console.log(this.label);
}
ABC = Object.create(Task); // .....
比较“类”和“委托”的思维
类的风格
子类Bar
继承了父类Foo
,然后成成实例。实例委托了Bar.prototype
,后者委托了Foo.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
并把Bar
委托给了Foo
,从而实现了三个对象之间的关联。但是这里更简洁,不需要那些复杂困惑的模仿类行为,比如构造函数、原型以及new
。
Foo = {
init : function(who) {
this.me = who;
},
identity : function() {
return "I am " + this.me;
}
};
Bar = Object.create(Foo); // 开始第一层委托
Bar.speak = function() { // 定义自己的功能
alert("Hello" + this.identity() + "."); // 把委托隐藏在 API 内部
}
var b1 = Object.create(Bar); // 开始第二层委托
b1.init("Saul");
var b2 = Object.create(Bar);
b2.init("Carrie");
b1.speak();
b2.speak();
控件举例
“类”的写法
类的写法要求我们现在父类上定义render()
方法,然后在子类里面重写它。但又不是完全替换,而是添加一些按钮特定行为。如果在子类中引用父类的基础方法,就会出现丑陋的显式伪多态。
// 父类
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 render方法
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, 40, "Hello");
var btn2 = new Button(144, 51, "World");
btn1.render($body);
btn2.render($body);
console.log(btn1);
})
ES6的calss
语法糖写法
使用ES6
之后,代码变得好看了很多。但是深入研究之下,其实并不完美。
class Widget {
// 定义父类
constructor(width, height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
render($where) {
if(this.$elem) {
this.$elem.css({
width : this.width + "px",
height : this.height + "px"
}).appendTo($where);
}
}
}
class Button extends Widget {
// 通过扩展父类,来定义子类
constructor(width, height, label) {
super(width, height); //调用父类
this.label = label || "Default";
this.$elem = $("<button>").text(this.label);
}
render($where) {
super.render($where); // 注意这里是 super.render()而不是 super(),书中有误
this.$elem.click(this.onClick.bind(this));
}
onClick(evt) {
console.log("Button " + this.label + " is Clicked!");
}
}
$(document).ready(function() {
var $body = $(document.body);
var btn1 = new Button(125, 40, "Hello");
var btn2 = new Button(144, 51, "World");
btn1.render($body);
btn2.render($body);
console.log(btn1);
})
关联对象的写法
对象关联可以更好地支持关注分离————separation of concerns
的原则。
// 注意,并不存在父类!!!只有对象,只有对象,只有对象!!!
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); // 关联 Widget
Button.setup = function(width, height, label) { // 包装 API ,在内部委托
// 委托调用
this.init(width, height); // 就在这个委托 Widget 对象的 init
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 + " is Clicked!!! ");
};
$(document).ready(function() {
var $body = $(document.body);
var btn1 = Object.create(Button); // 注意这里新建实例也是通过关联对象
btn1.setup(124, 76, 'Hello');
var btn2 = Object.create(Button); // 注意这里新建实例也是通过关联对象
btn2.setup(124, 66, 'World');
btn1.build($body);
btn2.build($body);
})
更简洁的设计
最后一个案例,展示了如何利用对象关联来简化整体设计。
在这个场景中,我们有两个控制器对象:一个用来操作网页中的登录表单,另一个用来于服务器进行验证通信。
var LoginController = { // 我们并不非得需要定义父类和两个子类来建模
errors : [], // 错误信息
getUsers : function() { // 取回用户名
return document.getElementById('login_username').value;
},
getPassword : function() { // 取回密码
return document.getElementById('login_password').value;
},
validateEntry : function(user,pw) { // 验证用户名和密码
user = user || this.getUsers();
pw = pw || this.getPassword();
if(!(user && pw)) { // 验证失败
return this.failure("请输入用户名和密码");
}
else if(pw.length < 5) {
return this.failure("密码必须5位以上!");
}
return true; // 执行到这里,说明通过了验证
},
showDialog : function(title, msg) { // 显示对话
// 给用户显示标题和消息
},
failure : function(err) { // 显示错误
this.errors.push(err); // 储存错误
this.showDialog("错误", "登陆出现 " + err);
}
};
// 让 AuthController 委托给 LoginController
var AuthController = Object.create(LoginController);
AuthController.errors = [];
AuthController.checkAuth = function() {
var user = this.getUsers();
var pw = this.getPassword();
if(this.validateEntry(user, pw)) { // 验证成功
this.server('/check-auth', {
user : user,
pw : pw
})
.then(this.accepted().bind(this)) // then 监听响应
.fail(this.rejected.bind(this)); // 这里使用了链式反应
}
};
AuthController.server = function(url, data) { // 辅助函数来创建 Ajaxt 通信
return $.ajax({
url : url,
data : data
});
};
AuthController.accepted = function() {
this.showDialog("成功", "通过验证")
};
AuthController.rejected = function(err) {
this.failure("认证失败,错误" + err);
};
var controller1 = Object.create(AuthController);
var controller2 = Object.create(AuthController);
[TOC]