zoukankan      html  css  js  c++  java
  • 业务建模 之 闲话'闭包'与'原型继承'

    在业务建模中,我们经常遇到这样一种情况:“原型”对象负责实现业务的基本诉求(包括:有哪些属性,有哪些函数以及它们之间的关系),以“原型”对象为基础创建的“子对象”则实现一些个性化的业务特性,从而方便的实现业务扩展。最常见的搞法是:

    1. 定义一个‘构造函数’,在其中实现属性的初始化,例如:var Person = function( ){};    //函数体中可以进行一些变量的初始化。

    2. 再设置该函数的prototype成员,例如:Person.prototype = { gotoSchool:function(){ console.log( 'on foot' );} };                           //该对象字面量中定义一些方法

    3. 用new来创建一个新对象,例如:var student = new Person();

    4. 个性化新对象的部分行为:student.gotoSchool = function(){ console.log( 'by bus' ); } ;

        >>根据new 和 原型链的特性,调用 student.gotoSchool();  将会输出 by bus,而不是 on foot。

    5. 同理,用new来创建一个teacher的对象,然后再设置它的gotoSchool的成员。

        var teacher = new Person();
    
        teacher.gotoSchool =  function(){ console.log( 'by car' ); } ;
        teacher.gotoSchool() ;        //将会输出 by car 

    说明:本文中的代码可以在Chrome浏览器的控制台中执行验证。方法如下:按F12后单击Console页签,打开Chrome的控制台,可以看到console.log输出的结果。

    上面的方式能够满足我们的基本诉求,并且在之前的Web控件自定义开发中,我们也是这么做的。但是,如果业务模型比较复杂,那么上面的这种方式的弊端也是明显的:

    没有私有环境,所有的属性都是公开的。

    今天,我们就业务建模出发,看看如果借助JavaScript的闭包特性,是否有更好的方式来优雅实现业务建模。

    先看一个原型继承的例子:

     1 var BaseObject = (function(){
     2     var that = {};
     3     
     4     that.name = 'Lily' ;
     5     that.sayHello = function(){
     6         console.log( 'Hello ' + this.getName() );
     7     };
     8     that.getName = function(){
     9         return this.name ;
    10     };
    11     
    12     return that ;
    13 })();
    14 
    15 //创建一个继承的对象
    16 var tomObject = Object.create( BaseObject );
    17 tomObject.name = 'Tom' ;
    18 
    19 //调用公开的方法
    20 tomObject.sayHello( ) ;   //输出:Hello Tom

    【分析】
    当前的这种方式,在编码规范的情况下,是能够正常工作的,但是,从程序的封装的角度来看,却存在明显的不足。
    因为,tomObject也可以设置它的getName函数,
    例如:在tomObject.sayHello();之前添加如下代码:
    //....
    tomObject.getName = function(){ return 'Jack' };
    //调用公开的方法
    tomObject.sayHello( ) ; //输出:Hello Jack

    而实际上,作为一个约定,我们希望getName就是调用当前对象的name的属性值,不允许继承它的子对象任意覆盖它!也就是说,getName应该是一个私有函数!
    现在,我们看如何用【闭包】来解决这个问题:

     1 var createPersonObjFn = function(){
     2     var that = {};
     3     
     4     var name = 'Lily' ;
     5     
     6     var getName = function(){
     7         return name ;
     8     };
     9     
    10     that.setName = function( new_name ){
    11         name = new_name ;
    12     };
    13     that.sayHello = function(){
    14         console.log( 'Hello ' + getName() );
    15     };
    16     
    17     return that ;
    18 };
    19 
    20 //创建一个对象
    21 var tomObject = createPersonObjFn();
    22 tomObject.setName( 'Tom' );
    23 
    24 //调用公开的方法
    25 tomObject.sayHello( ) ;   //输出:Hello Tom

    【分析】
    现在好了,尽管你还是可以给tomObject增加新的getName()函数,但并不会影响sayHello的业务逻辑。同理,
    //...
    tomObject.setName( 'Tom' );
    tomObject.getName = function(){return 'Jack'; }; //设置对象的getName的函数

    //调用公开的方法
    tomObject.sayHello( ) ;                                      //依然输出:Hello Tom

    闭包的特点就是:
    1. 将要'业务对象'的属性保存在'运行时环境'中。
    2. 天然的'工厂模式',要新生成一个对象,就执行一下函数。
    从这也可以看出,采用'闭包'这种模式构建业务时,对于'原型链'的理解要求并不高,这也许是为什么老道在它的书中对于'原型链'着墨甚少的原因吧。

    【优化】
    但是,我们知道,在业务模型中,我们还是希望能够实现'继承'的效果,也就是说,"主体对象"实现基本的框架和逻辑,"子对象"根据自身的特点来自定义一些特定的行为。通过Object.create() 创建对象时,基于"原型链"的特征,我们很好理解,只要在新创建的对象中重新定义一下自定义函数就可以了。但是,同样的业务诉求,在'闭包'这种方式下如何实现呢?

    [方法]
    在闭包对外公开的函数中,调用通过this调用的函数,那么这个函数的行为就可以在闭包之外被自定义。
    试验代码如下:

     1 that.sayHello = function(){
     2     //这里的sayHello调用了当前对象的getNewName()
     3     console.log( 'Hello ' + this.getNewName() );   
     4 };
     5 
     6 //...前面其他的代码不变
     7 var tomObject = createPersonObjFn();
     8 tomObject.getNewName = function(){   //定义当前对象的getNewName, 
     9     return 'Jack' ;
    10 }
    11 
    12 //调用公开的方法
    13 tomObject.sayHello( ) ;              //输出:Hello Jack

    【分析】
    虽然通过修改sayHello中的定义(通过调用方法函数),我们似乎能够自定义对象的一些行为,但是,新定义的行为并不能访问到tomObject的私有属性name!这和对象原来想表达的内容完全没有关系。而我们真实的业务诉求或许是这样,自定义行为之后,sayHello 能够打印"Hello dear Tom!" 或者"Hello my Tom!" 的内容。
    [回顾]我们知道,在闭包中,如果要想访问私有属性,必须要定义相关的公开的方法。所以,我们优化如下:

     1 //...在闭包中,将getName这样的函数由私有函数转换为公开函数
     2 that.getName = function( ){
     3     return name ;
     4 }
     5 
     6 //...定义tomObject的自定义函数getNewName,在函数中调用getName的方法。
     7 tomObject.getNewName = function(){
     8     return 'dear ' + tomObject.getName() + '!' ;
     9 }
    10 tomObject.setName( 'Tom' );
    11 
    12 //调用公开的方法
    13 tomObject.sayHello( ) ;   //输出:Hello dear Tom!
    14 
    15 
    16 //为了体现自定义行为的特点,我们再创建另外一个Jack的对象
    17 var jackObject = createPersonObjFn();
    18 jackObject.getNewName = function(){   //定义当前对象的getNewName, 
    19     return 'my ' + jackObject.getName() + '!' ;
    20 }
    21 jackObject.setName( 'Jack' );
    22 
    23 //调用公开的方法
    24 jackObject.sayHello( ) ;   //输出:Hello my Jack!

    【分析】
    看起来似乎没有什么问题了,但是,还有一个小细节需要优化。我们在sayHello中调用了this.getNewName();但是,如果新创建的对象没有重新定义getNewName函数,
    那样岂不报异常了?所以,严谨的做法应该是,在闭包中也设置一个that.getNewName的函数,默认的行为就是返回当前的name值,
    如果要进行自定义行为,则对象会体现出自定义的行为,覆盖(重载)默认的行为。

    【完整的例子】
    1. 在闭包中,可以定义私有属性(指:对象、字符串、数字、布尔类型等),这些属性只能通过闭包开放的函数访问、修改。
    2. 有些函数,你并不希望外部对象对它进行调用,仅仅供闭包内的函数(包括:公开函数和私有函数)调用,则可以将它定义为私有函数。
    3. 如果要想闭包对象的某一部分行为可以自定义(达到继承的效果),则需要进行如下几步。
      a. 新增能访问私有属性的公开函数,例如:例子中的getName函数。
             因为根据作用域的特点,闭包外部是无法访问到私有属性的,而自定义的函数是在闭包外部的。
         b. 在闭包内部,以公开函数的方式,设置需要自定义函数的默认行为,例如:闭包中getNewName函数的定义。
         c. 在允许自定义行为的公开函数(例如:例子中的sayHello函数)中,通过this调用可以自定义行为的函数。
             例如例子中的this.getNewName()。

    完整的代码如下:

     1 var createPersonObjFn = function(){
     2     var that = {};
     3     
     4     var name = 'Lily' ;
     5     
     6     that.getName = function(){
     7         return name ;
     8     };
     9     that.setName = function( new_name ){
    10         name = new_name ;
    11     };
    12     that.getNewName = function( ){   //默认的行为
    13         return name ;
    14     };
    15     that.sayHello = function(){
    16         console.log( 'Hello ' + this.getNewName() );
    17     };
    18     
    19     return that ;
    20 };
    21 
    22 //1. 创建一个对象
    23 var tomObject = createPersonObjFn();
    24 tomObject.getNewName = function(){
    25     return 'dear ' + tomObject.getName() + '!' ;
    26 }
    27 tomObject.setName( 'Tom' );
    28 
    29 //调用公开的方法
    30 tomObject.sayHello( ) ;   //输出:Hello dear Tom!
    31 
    32 //2. 创建另外一个Jack的对象
    33 var jackObject = createPersonObjFn();
    34 jackObject.getNewName = function(){   //定义当前对象的getNewName, 
    35     return 'my ' + jackObject.getName() + '!' ;
    36 }
    37 jackObject.setName( 'Jack' );
    38 
    39 //调用公开的方法
    40 jackObject.sayHello( ) ;   //输出:Hello my Jack!
    41 
    42 
    43 //3 创建另外一个Bill的对象,不重新定义getNewName函数,采用默认的行为
    44 var billObject = createPersonObjFn();
    45 billObject.setName( 'Bill' );
    46 
    47 //调用公开的方法
    48 billObject.sayHello( ) ;   //输出:Hello Bill

    【总结】

    JavaScript是一个表现力很强的语言,非常的灵活,自然也比较容易出错。上面举的例子中,我们仅仅突出展现了闭包的特性,其实,利用“原型链”的特性,我们完全可以基于tomObject,jackObject这些对象再来创建另外的对象,或者tomObject这些对象的创建过程,放到另外一个闭包中,这样或许可以组合出更加丰富的模型。闭包的特性就在这里,原型链的特性也在这里......到底什么时候用?怎么组合起来用?关键还是看我们的业务诉求,看真实的使用场景,看我们对性能,扩展性,安全等等多个方面的期望。

    另外,本文涉及到一些背景知识,例如:原型链是怎样的一个图谱关系?new这个运算符在创建对象时都做了啥?Object.create又可以如何理解? 由于篇幅有限,就没有展开来讲,如有疑问或建议,欢迎指出讨论,谢谢。

    【再思考】
    细心的同学或许发现了,既然闭包中that.getNewName和that.getName的实现都完全一样,为什么要重复定义这两个函数呢?是不是可以把闭包中that.getName给删除掉呢?
    答案当然是否定的。如果删除了闭包中的that.getName,而你又重新定义了that.getNewName的方法,这时候,闭包中的私有属性name在闭包外就没法访问到了。
    这就像同一包纸巾中的纸,样子完全一样,但职责不同,有些是事前用的,有些则是事后用的。
    比如,你在公园里吃苹果,没有水果刀,你会先抽出一张纸(A)擦一下苹果的外表,吃完苹果之后,把苹果的核用纸包起来扔到垃圾桶,又抽出一张纸(B)擦一下嘴巴和手。
    因为大家都是讲卫生,懂文明的"四有新人"。
    今天的分享到此为止,感谢大家捧场,希望诸位大侠不吝赐教。

  • 相关阅读:
    从程序员到项目经理(5):程序员加油站 不是人人都懂的学习要点
    从程序员到项目经理(14):项目经理必须懂一点“章法”
    从程序员到项目经理(17):你不是一个人在战斗思维一换天地宽
    从程序员到项目经理(19):想改变任何人都是徒劳的
    tp5时间戳转换日期格式
    win10让人愤怒的磁盘占用100%问题
    uniapp将时间日期格式化的组件unidateformat的用法
    Asp.Net大型项目实践系列第二季(一)哥欲善其事,必先利其器...
    Asp.Net大型项目实践(9)ExtJs实现系统框架页(非iframe,附源码,在线demo)
    Asp.Net大型项目实践(10)基于MVC Action粒度的权限管理(在线demo,全部源码)
  • 原文地址:https://www.cnblogs.com/alai88/p/5491466.html
Copyright © 2011-2022 走看看