【引言】在业务建模中,我们经常遇到这样一种情况:“原型”对象负责实现业务的基本诉求(包括:有哪些属性,有哪些函数以及它们之间的关系),以“原型”对象为基础创建的“子对象”则实现一些个性化的业务特性,从而方便的实现业务扩展。最常见的搞法是:
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)擦一下嘴巴和手。
因为大家都是讲卫生,懂文明的"四有新人"。
今天的分享到此为止,感谢大家捧场,希望诸位大侠不吝赐教。