业务建模 之 闲话'闭包'与'原型继承'

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

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)擦一下嘴巴和手。
因为大家都是讲卫生,懂文明的"四有新人"。
今天的分享到此为止,感谢大家捧场,希望诸位大侠不吝赐教。

原文地址:https://www.cnblogs.com/alai88/p/5491466.html