第6章: 行为委托
- 先简单回顾一下JavaScript的
[[Prototype]]
机制:- JavaScript的
[[Prototype]
机制,本质上就是对象之间的关联关系。 -
[[Prototype]]
机制是指对象内部中包含另一个对象的引用,当第一个对象的属性访问不到时,引擎会通过引用,到[[Prototype]]
关联的对象上继续查找,后者也没有找到,就会查找它的[[Prototype]]
,以此类推,这一系列的对象被称为原型链
。
- JavaScript的
- 我们也说过,与其将JavaScript术语
[[Prototype]]
称为 原型继承 ,不如叫 原型委托 更为准确。 - 而本章主要讲解就是“面向类”和“面向委托”设计模式之间的区别。
6.1 面向委托的设计
6.1.1 类理论
- 面向类的设计模式,通常先“抽象”父类的特征,然后用子类继承父类后进行特殊化。
- 举例说,用面向类的设计模式实现“汽车”和“飞机”:
- 定义一个通用的父类
Transport
(运输工具),Transport类定义公共的特性和行为; - 接着定义子类
Car
(汽车)和Aircraft
(飞机),继承自Transport
并且对自身的属性和行为进行特殊化;
- 定义一个通用的父类
class Transport {
//构造函数
Transport(id,passengerNum,name);
id;
passengerNum; //乘客数
name; //品牌名字
//启动
launch(){
console.log('载重人数:'+passengerNum,'品牌名字:'+name);
};
}
class Car inherits Transport{
//构造函数
Car(id,passengerNum,name,wheelNum){
super(id,passengerNum,name);
wheelNum = wheelNum;
}
wheelNum; //轮子数量
launch(){
super();
console.log('轮子数量:' + wheelNum);
};
}
class Aircraft inherits Transport{
//构造函数
Aircraft(id,passengerNum,name,wingNum){
super(id,passengerNum,name);
wingNum = wingNum;
}
wingNum; //机翼数量
launch(){
super();
console.log('机翼数量:' + wingNum);
}
}
6.1.2 委托理论
- 如果用面向委托的设计模式考虑同样的问题呢:
- 同样需要定义
Transport
(运输工具),但它只是包含公共的功能方法; - 接着定义
Car
(汽车)和Aircraft
(飞机),存储具体的数据以及特殊方法;
- 同样需要定义
class Transport {
setId : function(id){
this.id = id;
},
setPassengerNum : function(num){
this.passengerNum = num;
},
setName : function(name){
this.name = name;
},
launch(){
console.log('载重人数:'+this.passengerNum,'品牌名字:'+this.name);
},
}
//Car委托Transport
Car = Object.create(Transport);
Car.prepareTransport = function(id,passengerNum,name,wheelNum){
this.setId(id);
this.setPassengerNum(passengerNum);
this.wheelNum = wheelNum;
}
Car.prepareLaunch = function(){
this.launch();
console.log('轮子数量:' + wheelNum);
}
“面向类”和“面向委托”设计模式的区别
- 数据存储在委托者(
Car
/Aircraft
)上,而不是委托对象(Transport
)上; - 在面向类的设计模式中,父类和子类都拥有同名的方法
launch()
,但在面向委托的设计模式中,我们会尽量避免在[[Prototype]]
链条的不同级别中,使用相同的命名; - 如同
this.setId()
方法,由于调用位置触发了this的隐式绑定规则,虽然setId()
方法在Transport
中,运行时,仍然会绑定到Car
。这说明,委托行为意味着Car
对象在找不到属性或者方法引用时,会把这个请求委托给另一个对象Transport
。
注意:在两个或两个以上互相委托的对象之间,创建循环委托是禁止的。
6.1.3 比较思维模型
- 用伪代码,从理论上了解“面向类”和“面向委托”两种设计模式的区别后,我们从具体的JavaScript代码来比较两种设计模式的异同:
/**
* 交通工具
* @param {*} id
*/
function Transport(id,name,passengerNum){
this.id = id;
this.name = name;
this.passengerNum = passengerNum;
}
Transport.prototype.launch = function(){
console.log('载重人数:'+this.passengerNum,'品牌名字:'+this.name);
}
/**
* 汽车
* @param {*} id
* @param {*} name
* @param {*} passengerNum
*/
function Car(id,name,passengerNum){
Transport.call(this,id,name,passengerNum);
}
Car.prototype = Object.create(Transport.prototype);
Car.prototype.run = function(){
this.launch();
}
//实例化
var lexus = new Car(1,'雷克萨斯',8);
var bmw = new Car(2,'宝马',8);
lexus.run();
bmw.run();
- 再用“面向委托”的设计模式,通过对象关联的代码风格来编写同样的代码:
/**
* 交通工具
*/
Transport = {
init : function(id,name,passengerNum){
this.id = id;
this.name = name;
this.passengerNum = passengerNum;
},
launch : function(){
console.log('载重人数:'+this.passengerNum,'品牌名字:'+this.name);
}
}
/**
* 汽车
*/
Car = Object.create(Transport);
Car.run = function(){
this.launch();
}
//实例化
var mazda = Object(Car);
mazda.init(1,'马自达',4);
mazda.run();
var toyota = Object(Car);
toyota.init(2,'丰田',4);
toyota.run();
- 对比发现,代码简洁很多。很多时候我们要的只是将对象关联起来,并不需要那些既复杂又令人困惑的模仿类的行为,比如构造函数、原型以及
new
。
6.2 类与对象
- 接着我们通过真实的web前端案例来感受两种设计模式的不同,比如说我们要在页面上创建按钮控件。
/**
* Widget父类
* @param {*} width
* @param {*} height
*/
function Widget(width,height){
this.width = width;
this.height = height;
this.$elem = null;
}
Widget.prototype.render = function($where){
if(this.$elem){
this.$elem.css({
width : this.width + 'px',
height: this.height + 'px'
}).appendTo($where);
}
}
/**
* Button子类
* @param {*} width
* @param {*} height
* @param {*} label
*/
function Button(width,height,label){
Widget.call(this,width,height);
this.label = label;
}
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).render(function(){
var $body = $(document.body);
var saveBtn = new Button(125,30,'暂存');
var submitBtn = new Button(125,30,'提交');
saveBtn.render($body);
submitBtn.render($body);
})
- 用面向委托的代码来更简单的实现Widget/Button:
/**
* Widget父类
*/
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);
}
}
}
/**
* Button子类
*/
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).render(function(){
var $body = $(document.body);
var saveBtn = Object.create(Button);
saveBtn.setup(125,30,'暂存');
var submitBtn = Object.create(Button);
submitBtn.setup(125,30,'提交');
saveBtn.build($body);
submitBtn.build($body);
})
注意:
- 在面向委托的代码中,没有像类一样,定义相同的方法名
render()
,而是定义了两个更具描述性的方法名insert()
和build()
,在很多情况下,将构造和初始化步骤分开,更灵活;- 同时在面向委托的代码中,我们避免丑陋的显式伪多态调用
Widget.call
和Widget.prototype.render.call
,取而代之简单的相对委托调用this.init()
和this.insert()
;
6.3 更简洁的设计
- 第三个例子,是分别用“面向类”和“面向委托”的风格代码来实现一个登陆界面。
/**
* 父类Controller
*/
function Controller(){
this.errors = [];
}
Controller.prototype.showDialog = function(title,msg){
//...
}
Controller.prototype.success = function(msg){
this.showDialog('Success',msg);
}
Controller.prototype.failure = function(err){
this.errors.push(err);
this.showDialog('Error',err);
}
/**
* 登陆表单Controller子类
*/
function LoginController(){
Controller.call(this);
}
//继承Controller
LoginController.prototype = Object.create(Controller.prototype);
//获取用户名
LoginController.prototype.getUser = function(){
return docuemnt.getElmementById('username').value;
}
//获取密码
LoginController.prototype.getPwd = function(){
return docuemnt.getElmementById('userPwd').value;
}
//表单校验
LoginController.prototype.validate = function(user,pwd){
user = user || this.getUser();
pwd = pwd || this.getPwd();
if(!(user && pwd)){
return this.failure('请输入用户名/密码!');
}
else if(pwd.length < 5){
return this.failure('密码长度不能少于五位!');
}
return true;
}
//重写failure
LoginController.prototype.failure = function(err){
Controller.prototype.failure.call(this,'登陆失败:'+err);
}
/**
* 授权检查Controller子类
* @param {*} login
*/
function AuthController(login){
Controller.call(this);
this.login = login;
}
//继承Controller
AuthController.prototype = Object.create(Controller.prototype);
//授权检查
AuthController.prototype.checkAuth = function(){
var user = this.login.getUser();
var pwd = this.login.getPwd();
if(this.login.validate(user,pwd)){
this.send('/check-auth',{
user : user,
pwd : pwd
})
.then(this.success.bind(this))
.fail(this.failure.bind(this));
}
}
//发送请求
AuthController.prototype.send = functioin(url,data){
return $.ajax({
url : url,
data : data
})
}
//重写基础的success
AuthController.prototype.success = function(){
Controller.prototype.success.call(this,'授权检查成功!');
}
//重写基础的failure
AuthController.prototype.failure = function(err){
Controller.prototype.failure.call(this,'授权检查失败:'+err);
}
/**
* 调用
*/
//除了继承,还要合成
var auth = new AuthController(new LoginController());
auth.checkAuth();
- 用对象关联风格的行为委托来实现:
/**
* Login
*/
var LoginController = {
errors : [],
getUser : function(){
return docuemnt.getElmementById('username').value;
},
getPwd : function(){
return docuemnt.getElmementById('userPwd').value;
},
validate : function(user,pwd){
user = user || this.getUser();
pwd = pwd || this.getPwd();
if(!(user && pwd)){
return this.failure('请输入用户名/密码!');
}
else if(pwd.length < 5){
return this.failure('密码长度不能少于五位!');
}
return true;
},
showDialog : function(title,msg){
//...
},
failure : function(err){
this.errors.push(err);
this.showDialog('Error','登陆失败:'+err);
}
}
/**
* Auth
*/
var AuthController = Object.create(LoginController);
AuthController.errors = [];
AuthController.checkAuth = function(){
var user = this.login.getUser();
var pwd = this.login.getPwd();
if(this.login.validate(user,pwd)){
this.send('/check-auth',{
user : user,
pwd : pwd
})
.then(this.success.bind(this))
.fail(this.failure.bind(this));
}
}
AuthController.send = functioin(url,data){
return $.ajax({
url : url,
data : data
})
}
AuthController.accepted = function(){
this.showDialog('系统提示','授权检查成功!');
}
AuthController.rejected = function(err){
this.showDialog('系统提示','授权检查失败:'+err)
}
/**
* 调用
*/
var loginCtrl = Object.create(LoginController);
var authCtrl = Object.create(AuthController);
- 在行为委托模式中,
AuthController
和LoginController
只是兄弟关系的对象,不是子类和父类的关系。相比起面向类的设计模式,只需要两个实体就可以了,也不需要实例化类。
6.4 更好的语法
- 另外,ES6的
class
的语法可以简洁的定义类方法,不用function
关键字:
var LoginController = {
errors : [],
getUser() {
//可以不用function来定义
},
getPwd() {
//...
}
}
var AuthController = {
errors : [],
checkAuth(){
//...
},
send(){
//...
}
}
//把AuthController关联到LoginController
Object.setPrototypeOf(AuthController,LoginController);
6.5 内省
- 内省即检查实例的类型,在这个方面上,面向委托的代码也比面向类更方便。
- 前文提到,检测实例是否属于某个类对象,用
instance
:
//类
function Fruit(){}
Fruit.prototype.something = function(){}
//实例
var fruit = new Fruit();
//判断实例的类型,以便调用某个方法
if(fruit instanceof Fruit){
fruit.something();
}
- 但如果是多继承关系的情况下,则必须借助
Object.isPrototypeOf()
和Object.getPrototypeOf()
等方法:
//水果
function Fruit(){}
Fruit.prototype.something = function(){}
//苹果
function Apple(){}
Apple.prototype = Object.create(Fruit.prototype);
//苹果实例
var apple = new Apple();
//检测结果均为true
console.log(Apple.prototype instanceof Fruit);
console.log(Object.getPrototypeOf(Apple.prototype) === Fruit.prototype);
console.log(Fruit.prototype.isPrototypeOf(Apple.prototype));
console.log(apple instanceof Apple);
console.log(apple instanceof Fruit);
console.log(Object.getPrototypeOf(apple) === Apple.prototype);
console.log(Apple.prototype.isPrototypeOf(apple));
console.log(Fruit.prototype.isPrototypeOf(apple));
//或者通过鸭子类型的方式来检查,只要方法存在即调用
if(apple.something){
apple.something();
}
- 而面向委托的代码检测起来则比较简单:
var Fruit = {
something : function(){}
};
var Apple = Object.create(Fruit);
var apple = Object.craate(Apple);
//检测结果均为true
console.log(Fruit.isPrototypeOf(Apple));
console.log(Object.getPrototypeOf(Apple) === Apple);
console.log(Fruit.isPrototypeOf(apple));
console.log(Apple.isPrototypeOf(apple));
console.log(Object.getPrototypeOf(apple) === Apple);
6.6 小结
- JavaScript是一门灵活的语言,既可以采用面向类和继承的设计模式,这也是大多数开发者习惯的代码组织方式,另外也可以采用行为委托的设计模式。
- 相对于“面向类”的设计模式把对象看成父子关系,行为委托的设计模式认为对象之间是相互委托的兄弟关系。
- JavaScript的
[[Prototype]]
机制本质就是行为委托机制,所以当使用面向委托的设计模式来组织代码时,也让代码更简洁和清晰。