第6章: 行为委托
先简单回顾一下JavaScript的
[[Prototype]]
机制:JavaScript的
[[Prototype]
机制,本质上就是对象之间的关联关系。[[Prototype]]
机制是指对象内部中包含另一个对象的引用,当第一个对象的属性访问不到时,引擎会通过引用,到[[Prototype]]
关联的对象上继续查找,后者也没有找到,就会查找它的[[Prototype]]
,以此类推,这一系列的对象被称为原型链
。
我们也说过,与其将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委托TransportCar = 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); }//继承ControllerLoginController.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; }//重写failureLoginController.prototype.failure = function(err){ Controller.prototype.failure.call(this,'登陆失败:'+err); }/** * 授权检查Controller子类 * @param {*} login */function AuthController(login){ Controller.call(this); this.login = login; }//继承ControllerAuthController.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 }) }//重写基础的successAuthController.prototype.success = function(){ Controller.prototype.success.call(this,'授权检查成功!'); }//重写基础的failureAuthController.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关联到LoginControllerObject.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();//检测结果均为trueconsole.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);//检测结果均为trueconsole.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]]
机制本质就是行为委托机制,所以当使用面向委托的设计模式来组织代码时,也让代码更简洁和清晰。
作者:梁同学de自言自语
链接:https://www.jianshu.com/p/94affebd9b72