一周一章前端书·第11周:《你不知道的JavaScript(上)》S02E06

第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委托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.callWidget.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);
  • 在行为委托模式中,AuthControllerLoginController只是兄弟关系的对象,不是子类和父类的关系。相比起面向类的设计模式,只需要两个实体就可以了,也不需要实例化类。

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]]机制本质就是行为委托机制,所以当使用面向委托的设计模式来组织代码时,也让代码更简洁和清晰。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容