在业务建模中,咱们常常遇到这样一种状况:“原型”对象负责实现业务的基本诉求(包括:有哪些属性,有哪些函数以及它们之间的关系),以“原型”对象为基础建立的“子对象”则实现一些个性化的业务特性,从而方便的实现业务扩展。最多见的搞法是:html
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控件自定义开发中,咱们也是这么作的。可是,若是业务模型比较复杂,那么上面的这种方式的弊端也是明显的:this
没有私有环境,全部的属性都是公开的。编码
今天,咱们就业务建模出发,看看若是借助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)擦一下嘴巴和手。由于你们都是讲卫生,懂文明的"四有新人"。今天的分享到此为止,感谢你们捧场,但愿诸位大侠不吝赐教。