闭包是JavaScript中的一个重要特性,在之前的博文中,我们说闭包是一个'看似简单,其实很有内涵'的特性。当我们用JavaScript来实现相对复杂的业务建模时,我们可以如何利用'闭包'这个特性呢?JavaScript中的'原型继承',又可以解决业务建模中的哪些问题呢?今天我们就通过一家'榨汁机工厂'生产设计'榨汁机'的故事,来聊一聊'闭包'和'原型继承'在业务建模中的作用。
现在直播开始:

1》 工厂默认选用A型刀头方案制造榨汁机

例子当中我们主要涉及到2个函数:
1.榨汁机的生产工厂(JuiceMachineFactory)。
2.生产榨汁机的方案(createJuiceMachineGuide)。
在JuiceMachineFactory中,通过向createJuiceMachineGuide传入参数,可以获得能生产特定刀头的榨汁机的方法_createJuiceMachine,而通过执行_createJuiceMachine,则可以生产出特定刀头的榨汁机。对外表现出来的逻辑就是:JuiceMachineFactory可以确定生产出来的榨汁机所使用的刀头,并且能够生产榨汁机。
用代码表示如下:

  1. var JuiceMachineFactory = function(){
  2. var newFactory = {};
  3. var _cutterFn = function(){
  4. //榨汁机工厂当前使用的榨汁方法,相当于定义了一个特定的刀头
  5. console.log( '正在使用:A型刀头。' );
  6. };
  7.  
  8. //获得一种制造榨汁机的方法
  9. var _createJuiceMachine = createJuiceMachineGuide( _cutterFn );
  10.  
  11. //制造了一台榨汁机
  12. newFactory.createMachine = function(){
  13. return _createJuiceMachine();
  14. }
  15.  
  16. return newFactory;
  17.  
  18. };
  19.  
  20. var createJuiceMachineGuide = function( cutterFn ){
  21. return function(){
  22. var newMachine = {};
  23. var _cutterInMachine = cutterFn ; //用传入的'刀头'作为制造榨汁机的原件
  24.  
  25. newMachine.makeJuice = function(){
  26. //开始榨汁
  27. console.log( '##开始榨汁:' );
  28. //调用传入的私有变量
  29. _cutterInMachine();
  30. }
  31.  
  32. return newMachine ;
  33. }
  34. }
  35. //先创建唯一的一座工厂:
  36. var factory = JuiceMachineFactory();
  37. //现在,我们生产一台榨汁机:
  38. var machine_A = factory.createMachine();
  39. //使用这台榨汁机
  40. machine_A.makeJuice( );

运行代码之后输出如下结果:

  1. ##开始榨汁:
  2. 正在使用:A型刀头。

【分析】
1. 和我们预期的一样,我们通过传入_cutterFn来获得一个'采用刀头A来进行榨汁'的榨汁机制造方案,并且用这个方案制造了一台榨汁机。
2. 在使用新造的这台榨汁机进行榨汁的时候,确实是使用了刀头A的方法。
>> 通俗地讲,工厂制造这台榨汁机时,选用了哪种'制造方案',决定了生产出来的榨汁机的特性

2》工厂选用了B型刀头方案制造榨汁机
经过一段时间的发展,市场人员反馈说,现在用户都希望使用B型的刀头,我们得修改制造一下制造榨汁机的方案了。但是,从目前来看,我们并没有预留'修改榨汁机刀头'的接口,所以需要调整一下工厂,使它能够方便选择采用哪种刀头方案,调整后的代码如下:

  1. var JuiceMachineFactory = function(){
  2. var newFactory = {};
  3. var _cutterFn = function(){
  4. //榨汁机工厂当前使用的榨汁方法,相当于定义了一个特定的刀头
  5. console.log( '正在使用:A型刀头。' );
  6. };
  7.  
  8. //获得一种制造榨汁机的方法
  9. var _createJuiceMachine = createJuiceMachineGuide( _cutterFn );
  10.  
  11. //制造了一台榨汁机
  12. newFactory.createMachine = function(){
  13. return _createJuiceMachine();
  14. }
  15. //修改新的制造榨汁机的方案
  16. newFactory.setCutterFn = function( new_fn ){
  17. _cutterFn = new_fn ;
  18. return true;
  19. }
  20.  
  21. return newFactory;
  22.  
  23. };
  24.  
  25. var createJuiceMachineGuide = function( cutterFn ){
  26. return function(){
  27. var newMachine = {};
  28. var _cutterInMachine = cutterFn ; //用传入的'刀头'作为制造榨汁机的原件
  29.  
  30. newMachine.makeJuice = function(){
  31. //开始榨汁
  32. console.log( '##开始榨汁:' );
  33. //调用传入的私有变量
  34. _cutterInMachine();
  35. }
  36.  
  37. return newMachine ;
  38. }
  39. }
  40. //先创建唯一的一座工厂:
  41. var factory = JuiceMachineFactory();
  42. //现在,我们生产一台榨汁机:
  43. var machine_A = factory.createMachine();
  44. //使用这台榨汁机
  45. machine_A.makeJuice( );
  46.  
  47. //设置一下工厂的制造方案
  48. factory.setCutterFn( function(){
  49. console.log( '正在使用:B型刀头。' );
  50. });
  51. //用新的方案再制造一台榨汁机
  52. var machine_B = factory.createMachine();
  53. machine_B.makeJuice( );

运行代码之后输出如下结果:

  1. ##开始榨汁:
  2. 正在使用:A型刀头。
  3. ##开始榨汁:
  4. 正在使用:A型刀头。

【分析】
1. 采用默认的工艺,制造出来的榨汁机,采用的是 A型刀头。
2. 后面通过setCutterFn( ) 调整了制造方案,生产出来的榨汁机,按理,应该是采用“B型刀头”了啊!为什么还是A型刀头?!

这个问题引出了JavaScript的一个知识点:JavaScript中的对象的赋值,是一种'引用'传递。
我们分析一下整个过程。

1. 下面的这段代码执行之后,变量_cutterFn引用了一个'函数对象',我们给这个函数对象一个编号:F_007。

  1. var _cutterFn = function(){
  2.   //榨汁机工厂当前使用的榨汁方法,相当于定义了一个特定的刀头
  3.   console.log( '正在使用:A型刀头。' );
  4. };

示意图如下:

2.  执行下面的这段代码之后,相当于变量_cutterFn 和 _cutterInMachine 都指向了 F007 这个函数对象。

  1. var _createJuiceMachine = createJuiceMachineGuide( _cutterFn );

示意图如下:

3. 当我们向用新的方案来制造B型刀头榨汁机,在前面的代码中,我们用了setMakeMachineMethod方法,代码如下:

  1. //设置一下工厂的制造方案
  2. factory.setMakeMachineMethod( function(){
  3. console.log( '正在使用:B型刀头。' );
  4. });

此时各个变量所指的对象如下所示:

因为只是赋值仅仅是变量之间的'引用'传递,所以,执行完毕之后,变量_cutterFn 和 _cutterInMachine 并没有必然的联系!也就是说,_cutterFn 指向了别的地方(代号为F008的新对象),_cutterInMachine 并不会随之改变,除非再来一次赋值。

在我们的例子,我们要改变的其实不是_cutterFn所指的对象,我们应该改变 _cutterInMachine 所指的对象。也就是要重新'赋一次'值,生成一个新的方案_createJuiceMachine。调整后的代码如下:

  1. var JuiceMachineFactory = function(){
  2. var newFactory = {};
  3. var _cutterFn = function(){
  4. console.log( '正在使用:A型刀头。' );
  5. };
  6.  
  7. //获得一种制造榨汁机的方法
  8. var _createJuiceMachine = createJuiceMachineGuide( _cutterFn );
  9.  
  10. //制造了一台榨汁机
  11. newFactory.createMachine = function(){
  12. return _createJuiceMachine();
  13. }
  14. //修改新的制造榨汁机的方案
  15. newFactory.setMakeMachineMethod = function( new_fn ){
  16. _cutterFn = new_fn ;
  17. //关键是修改下面的内容
  18. _createJuiceMachine = createJuiceMachineGuide( _cutterFn );
  19. return true;
  20. }
  21.  
  22. return newFactory;
  23.  
  24. };
  25.  
  26. var createJuiceMachineGuide = function( cutterFn ){
  27. return function(){
  28. var newMachine = {};
  29. var _cutterInMachine = cutterFn ; //用传入的'刀头'作为制造榨汁机的原件
  30.  
  31. newMachine.makeJuice = function(){
  32. //开始榨汁
  33. console.log( '##开始榨汁:' );
  34. //调用传入的私有变量
  35. _cutterInMachine();
  36. }
  37.  
  38. return newMachine ;
  39. }
  40. }
  41. //先创建唯一的一座工厂:
  42. var factory = JuiceMachineFactory();
  43. //现在,我们生产一台榨汁机:
  44. var machine_A = factory.createMachine();
  45. //使用这台榨汁机
  46. machine_A.makeJuice( );
  47.  
  48. //设置一下工厂的制造方案
  49. factory.setMakeMachineMethod( function(){
  50. console.log( '正在使用:B型刀头。' );
  51. });
  52. //用新的方案再制造一台榨汁机
  53. var machine_B = factory.createMachine();
  54. machine_B.makeJuice( );
  55.  
  56. //再看一下原来制造的那台machine_A,是否还是使用了A型刀头?
  57. //答案是还是A型刀头,因为每执行一次createJuiceMachineGuide,其实是新增一个闭包!
  58. //也就是说,
  59. //制造machine_A的那套方案,并没有消失,它的榨汁的方法,还是指向F007那个函数对象。
  60. //而制造machine_B的那套方案,榨汁方法指向了F008这个函数对象。
  61. machine_A.makeJuice( );

运行代码之后输出如下结果:

  1. ##开始榨汁:
  2. 正在使用:A型刀头。
  3. ##开始榨汁:
  4. 正在使用:B型刀头。
  5. ##开始榨汁:
  6. 正在使用:A型刀头。

这回就正确了!
在生成对象machine_A和machine_B的时候,实际上我们调用了两次createJuiceMachineGuide,生成了两个独立的'运行时环境',而在这两次调用的过程中,运行时环境中的_cutterInMachine私有变量所指向的对象是不一样的。
整个过程示意如下:

【小节】
1. JavaScript中的赋值、参数传递都是'引用'传递。
    "这个好理解不?",
    "好理解。"
2. 执行一次'闭包'函数,会生成一个独立的'运行时环境'。
   "这个好理解不?"
   "好理解哈,相当于返回一个对象,这个对象中的函数可以访问'闭包'函数中的私有成员。"
把它们放到一起综合起来理解,就可以很好的解释"工厂可以决定生产出来的榨汁机采用哪种'刀头'了。"

2》给榨汁机添加'水果'

截止目前为止,我们在'榨汁机'这个模型中,其实还仅仅定义了'榨汁方法',并且确定,采用哪种榨汁方法(选用哪种刀头),是在工厂设置'制造方案'是决定的。但是,完整的榨汁过程,应该是有'水果'的,并且,'水果'应该是可以替换的。

用代码描述如下:

  1. var JuiceMachineFactory = function(){
  2. var newFactory = {};
  3. var _cutterFn = function(){
  4. console.log( '正在使用:A型刀头。' );
  5. };
  6.  
  7. //获得一种制造榨汁机的方法
  8. var _createJuiceMachine = createJuiceMachineGuide( _cutterFn );
  9.  
  10. //制造了一台榨汁机
  11. newFactory.createMachine = function(){
  12. return _createJuiceMachine();
  13. }
  14. //修改新的制造榨汁机的方案
  15. newFactory.setMakeMachineMethod = function( new_fn ){
  16. _cutterFn = new_fn ;
  17. //关键是修改下面的内容
  18. _createJuiceMachine = createJuiceMachineGuide( _cutterFn );
  19. return true;
  20. }
  21.  
  22. return newFactory;
  23.  
  24. };
  25.  
  26. var createJuiceMachineGuide = function( cutterFn ){
  27. return function(){
  28. var newMachine = {};
  29. var _cutterInMachine = cutterFn ; //用传入的'刀头'作为制造榨汁机的原件
  30. var _fruit = '橙子' ; //默认是橙子汁

  31. newMachine.selectFruit = function( new_fruit ){
  32. _fruit = new_fruit ;
  33. }
  34.  
  35. newMachine.makeJuice = function( ){
  36. //开始榨汁
  37. console.log( '##开始榨汁:' );
  38. console.log( '使用水果<' +_fruit+'>' );
  39. //调用传入的私有变量
  40. _cutterInMachine();
  41. }
  42.  
  43. return newMachine ;
  44. }
  45. }
  46. //先创建唯一的一座工厂:
  47. var factory = JuiceMachineFactory();
  48. //现在,我们生产一台榨汁机:
  49. var machine_A = factory.createMachine();
  50. //使用这台榨汁机
  51. machine_A.makeJuice( );
  52. //选择另外的水果
  53. machine_A.selectFruit( '西瓜' );
  54. //再次榨水果
  55. machine_A.makeJuice( );

运行代码之后输出如下结果:

  1. ##开始榨汁:
  2. 使用水果<橙子>
  3. 正在使用:A型刀头。
  4. ##开始榨汁:
  5. 使用水果<西瓜>
  6. 正在使用:A型刀头。

【分析】
与我们预期的一样:默认情况下,我们使用<橙子>来榨果汁,因为我们也开放了操纵_fruit的接口,所以,我们也可以使用<西瓜>来榨果汁。

【小节】
到目前为止,我们掌握了以下两个情况的处理:
1. 在工厂中可以根据需要调整制造方案:

我们通过传入参数的方式,结合闭包的特性,更换_createJuiceMachine的值,用另外一个'闭包'的运行结果代替之前的'闭包运行结果'。
由于两个运行结果中的_cutterInMachine的值不同,相当于产生了两套不同的制造榨汁机的方法。
>>
回到举例的场景中:制造榨汁机的工厂,可以决定生产出来的榨汁机采用哪种'刀头'。
对于JavaScript本身,由于'高阶函数'的特点,相当于通过传入参数,可以定制'闭包'函数本身。
举例中:
         createJuiceMachineGuide:制造'闭包'函数的函数。
         _createJuiceMachine:就是执行createJuiceMachineGuide之后返回的'闭包'函数。
         var machine_A = factory.createMachine();:machine_A,执行_createJuiceMachine之后的返回的一个'闭包'。
相关知识点:闭包,高阶函数特性,JavaScript赋值操作的本质。

2. 在榨汁时,可以设定榨汁采用的'水果'。
这相当于给了我们一个从'业务模型'认识'闭包'函数的好例子:
a. 运行'闭包'函数,会产生一个'对象'(或函数定义)。这个'对象'中,原来定义在'闭包'函数中的局部变量和参数,成为这个'对象'的私有成员。而这个'对象'中的方法,则成为'对象'的对外接口(公共函数),这些对外接口可以访问'对象'的私有成员。当然,一般不会在返回的'对象'中直接设置'属性'成员,因为那样是'违背'封装性的原则的。
b. 设计一个'闭包'函数的时候:
1> 如果某个成员是业务对象的属性(例如:_fruit),那么,就把它声明为'闭包'函数的局部变量或通过参数传入。
2> 如果某个函数并不想开放给别人使用(例如:_cutterInMachine),那么也把它声明为'闭包'函数的局部变量或通过参数传入。
3> 业务对象对外呈现出来的功能(例如:makeJuice),则把它声明为该对象的'成员'函数。
4> '闭包'函数中的'私有成员',可以通过对外开放出来的公共函数(makeJuice和selectFruit)更改。
讨论到这里,借助'闭包'和'高阶函数'的特性,我们似乎已经能够实现应用中的'业务'建模。我们定义好一个'闭包函数'之后,就可以'产生满足我们业务要求'的对象,并且,这些对象具有很好的'封装性'。
到现在为止,我们还没有用到this,没有this的日子,似乎也是很美好的。

3》给榨汁机增加云端系统

现在的社会,当我们讲到家电设备时,我们往往会说‘智能家电’,取名称时喜欢带一个'云'字,例如:云冰箱,云电视。其实质就是,每个家电背后都有一个'云端'系统,通过'云端'系统,我们可以'远程'与我们的家电通信。比如,还没有下班,我们知道今天下午有一场AC米兰的球赛。这时候,借助'云端'系统,我们可以通过手机遥控家里的'云电视',让它把AC米兰的球赛先录下来,这样,我们下班回到家就可以观看。
现在,我们要给榨汁机也增加这样的'云端'系统,这样,我们就可以在下班回家的路上远程操控榨汁机,一回到家,就能喝到'新鲜'的果汁。比起那些只有'定时'功能的家电,'云家电'还是要方便很多,比如:你如果发现路上堵车了,就可以晚点启动'榨汁作业'。如果老板告诉你晚上要加一下班,你就可以不启动'榨汁作业'。
好了,现在我们就给榨汁机增加一个'云控制系统',然后增加一个'远程启动'的功能。

  1. var JuiceMachineFactory = function(){
  2. var newFactory = {};
  3. var _cutterFn = function(){
  4. console.log( '正在使用:A型刀头。' );
  5. };
  6.  
  7. //获得一种制造榨汁机的方法
  8. var _createJuiceMachine = createJuiceMachineGuide( _cutterFn );
  9.  
  10. //制造了一台榨汁机
  11. newFactory.createMachine = function(){
  12. return _createJuiceMachine();
  13. }
  14. //修改新的制造榨汁机的方案
  15. newFactory.setMakeMachineMethod = function( new_fn ){
  16. _cutterFn = new_fn ;
  17. //关键是修改下面的内容
  18. _createJuiceMachine = createJuiceMachineGuide( _cutterFn );
  19. return true;
  20. }
  21.  
  22. return newFactory;
  23.  
  24. };
  25.  
  26. var createJuiceMachineGuide = function( cutterFn ){
  27. return function(){
  28. var newMachine = {};
  29. var _cutterInMachine = cutterFn ; //用传入的'刀头'作为制造榨汁机的原件
  30. var _fruit = '橙子' ; //默认是橙子汁
  31.  
  32. var _cloudCenter = {
  33. remoteStart:function(){
  34. //这里模拟执行远程启动时,云端控制'榨汁机'开始榨汁
  35. console.log( '^^云中心收到指令,马上控制榨汁机开始榨汁^^' );
  36. newMachine.makeJuice();
  37. return ;
  38. }
  39. }
  40.  
  41. newMachine.selectFruit = function( new_fruit ){
  42. _fruit = new_fruit ;
  43. }
  44.  
  45. newMachine.makeJuice = function( ){
  46. //开始榨汁
  47. console.log( '##开始榨汁:' );
  48. console.log( '使用水果<' +_fruit+'>' );
  49. //调用传入的私有变量
  50. _cutterInMachine();
  51. }
  52.  
  53. //增加远程启动榨汁机的功能
  54. newMachine.remoteStart = function(){
  55. //模拟这个功能要借助云端中心执行
  56. _cloudCenter.remoteStart();
  57. }
  58.  
  59. return newMachine ;
  60. }
  61. }
  62.  
  63. //先创建唯一的一座工厂:
  64. var factory = JuiceMachineFactory();
  65. //现在,我们生产一台榨汁机:
  66. var machine_A = factory.createMachine();
  67. //使用这台榨汁机
  68. machine_A.remoteStart( );

运行代码之后输出如下结果:

  1. ^^云中心收到指令,马上控制榨汁机开始榨汁^^
  2. ##开始榨汁:
  3. 使用水果<橙子>
  4. 正在使用:A型刀头。

【分析】
嘿嘿,看来增加一个云端的系统也是很简单的嘛。没错,这样的设计目前来看是符合我们的预期的。但是,我们是一个'榨汁机'的工厂,这就意味着我们不是'仅仅'生产一台'榨汁机',我们可能会生产上千台,上万台这样的榨汁机。这样,我们再次从'空间'分配的角度来剖析整个榨汁机的过程。
运行下面的代码:

  1. var machine_A = factory.createMachine();

相当于执行了_createJuiceMachine这样的闭包函数,我们知道:"每执行一次'闭包'函数,JavaScript引擎会为闭包函数中的'局部变量'分配一次空间。"
执行1次'闭包'函数factory.createMachine()之后和执行n次'闭包'函数之后的花费空间示意:

假设_cloudCenter这个局部变量占据了很大的空间,我们用深颜色标识出来。回到业务场景,搭建一个'云中心'需要大量的资金投入,但是,我们现在的搞法就相当于:
"为生产出来的每一台榨汁机,都搭建一个专属的云中心。"
显然,这是不合理的。

有同学可能会说,对于_cloudCenter这个局部变量,我们没有必要在'闭包'函数的函数体内专门全新生成一个对象,可以引用'外部'的一个唯一对象,这样就可以节省大量的空间。

这种方式确实也是一种解决方案,在之前的'地址选择控件开发'这篇博文中,针对全国地址信息模型这个比较大的对象,我们就是采用这种方式。

我们再仔细审视一下_createJuiceMachine这个闭包函数,除了_fruit之外,似乎其他的方法也都很_cloudCenter一样,没有必要每生成一个对象,就全新打造一份!
综合上面的分析,我们可以使用JavaScript的'原型继承'特性,来完成我们的业务目标。
我们先来看一下,优化之前,machine_A, machine_B......这些对象的'原型链'关系。

如果基于'原型继承'来实现我们的业务目标,那么,我们希望我们的'原型链'关系会变成如下所示:

有一个BaseMachine,具有我们'期望'的功能。新创建的machine_A, machine_B都'原型继承'自BaseMachine。

下面,我们用代码来实现这样的业务:

  1. var JuiceMachineFactory = function(){
  2. var newFactory = {};
  3. var _cutterFn = function(){
  4. console.log( '正在使用:A型刀头。' );
  5. };
  6.  
  7. //获得一种制造榨汁机的方法
  8. var _createJuiceMachine = createJuiceMachineGuide( _cutterFn );
  9.  
  10. //先制造一个'原型对象'
  11. var _base_machine = _createJuiceMachine();
  12.  
  13. //制造了一台榨汁机
  14. newFactory.createMachine = function(){
  15. //基于'原型'对象来创建新的榨汁机
  16. var new_machine = Object.create( _base_machine );
  17. new_machine.setDefaultFruit();
  18. return new_machine;
  19. }
  20. return newFactory;
  21.  
  22. };
  23.  
  24. var createJuiceMachineGuide = function( cutterFn ){
  25. return function(){
  26. var newMachine = {};
  27. var _cutterInMachine = cutterFn ; //用传入的'刀头'作为制造榨汁机的原件
  28.  
  29. var _fruit = '橙子' ; //默认是橙子汁
  30.  
  31. var _cloudCenter = {
  32. remoteStart:function(){
  33. //这里模拟执行远程启动时,云端控制'榨汁机'开始榨汁
  34. console.log( '^^云中心收到指令,马上控制榨汁机开始榨汁^^' );
  35. //注意:因为在makeJuice中使用了this.fruit,
  36. //所以,这里也要用this来调用,否则,makeJuice中的this所指对象就不正确!
  37. this.makeJuice();
  38. return ;
  39. }
  40. }
  41.  
  42. newMachine.selectFruit = function( new_fruit ){
  43. this.fruit = new_fruit ;
  44. }
  45.  
  46. //看一下当前使用的苹果
  47. newMachine.showFruit = function( ){
  48. console.log( '>>正在使用的水果是:' + this.fruit );
  49. return ;
  50. }
  51.  
  52. newMachine.setDefaultFruit = function(){
  53. this.fruit = _fruit; //设置新的对象使用默认的水果
  54. }
  55.  
  56. newMachine.makeJuice = function( ){
  57. //开始榨汁
  58. console.log( '##开始榨汁:' );
  59. console.log( '使用水果<' +this.fruit+'>' );
  60. //调用传入的私有变量
  61. _cutterInMachine();
  62. }
  63.  
  64. //增加远程启动榨汁机的功能
  65. newMachine.remoteStart = function(){
  66. //模拟这个功能要'借助'云端中心执行
  67. //相当于要'借用'_cloudCenter.remoteStart这个函数,借用函数怎么搞,用apply哈
  68. _cloudCenter.remoteStart.apply( this , arguments );
  69. }
  70. return newMachine ;
  71. }
  72. }
  73.  
  74. //先创建唯一的一座工厂:
  75. var factory = JuiceMachineFactory();
  76. //现在,我们生产一台榨汁机:
  77. var machine_A = factory.createMachine();
  78. //设置一下水果的值
  79. machine_A.selectFruit( '苹果' );
  80. //查看一下当前所使用的水果
  81. machine_A.showFruit( );
  82. //使用这台榨汁机
  83. machine_A.remoteStart( );
  84.  
  85. //再全新生成一个对象machine_B
  86. //
  87. var machine_B = factory.createMachine();
  88. machine_B.makeJuice( );

运行代码之后输出如下结果:

  1. >>正在使用的水果是:苹果
  2. ^^云中心收到指令,马上控制榨汁机开始榨汁^^
  3. ##开始榨汁:
  4. 使用水果<苹果>
  5. 正在使用:A型刀头。
  6. ##开始榨汁:
  7. 使用水果<橙子>
  8. 正在使用:A型刀头。

【分析】
就是为了使用'原型继承'这个特性,我们引入了this,为了达到各种预期的效果,我们对各个函数都进行了调整。
1. 工厂中,生成machine对象的方式,调整成:先生成一个原型对象(_base_machine),然后基于这个原型对象创建全新的对象
2. 基于原型对象创建的全新对象,自己是没有任何'成员属性'的。
所以,我们通过调用new_machine.setDefaultFruit();来为新创建的对象添加自己的成员属性fruit,而它的值就是原型对象(_base_machine)中闭包空间中的值_fruit,这样符合我们的业务预期。
3. 将原型对象中,对外公开方法中_fruit的地方,都换成了this.fruit,因为我们希望,当我们使用原型对象_base_machine中的对外公开方法时,其中的fruit应该指代全新创建的对象的fruit成员,而不是生成_base_machine时,闭包中存在的_fruit。
4. 原型对象的对外公开方法(remoteStart),其实调用的不是原型对象本身拥有的方法,而是'借用'了'第三方对象'_cloudCenter中的方法。如果不做调整,直接调用_cloudCenter.remoteStart();那么,函数体的所指代的this就会指向了_cloudCenter。如何向别的对象'借用'方法,我们在前面的博客'闲聊JS中的apply和call'中有比较详细的介绍,所以,我们采用如下的方式调用:

  1. _cloudCenter.remoteStart.apply( this , arguments );

这样,就确保了当你执行new_machine.remoteStart()时,_cloudCenter.remoteStart函数体中的this也是指代new_machine,与我们的业务预期相符。

经过一番周折,我们终于利用JavaScript的'原型继承'特性,实现了我们的'业务目标'。JavaScript的函数调用特性,使得JavaScript更具有'弹性',当然,也使实现逻辑变得更加复杂。当你'迫不得已'要在函数体中使用this的时候,你一定要想清楚,到时候真正调用这个函数的对象会是谁,它具有什么样的成员属性。
回顾整个过程,下面这个语句,其实是用到了一定的随意性,或者说是JavaScript语言的'弹性'。

  1. newMachine.setDefaultFruit = function(){
  2. this.fruit = _fruit; //设置新的对象使用默认的水果
  3. }

如果不存在,那么就给对象新增一个fruit成员属性。不过问题不大,我们把这个随意性控制在selectFruit这个方法当中了。现在再回到第2个场景"工厂选用了B型刀头方案制造榨汁机",因为我们使用了'原型继承',要换一种榨汁方式,就需要修改原型对象中'运行时环境'中的_cutterInMachine变量。到底要如何修改呢?就留作练习吧。

【总结】

今天我们以'榨汁机工厂'生产特定功能的'榨汁机'为场景,讲解了如何用JavaScript的'闭包'特性进行'业务建模'。首先我们采用了更改'_createJuiceMachine'的方式达到更换榨汁方式的目的。后来,我们给生产的'榨汁机'增加了'云'功能。当增加了'云'功能之后,发现从'空间利用'的角度看,原来的生产'榨汁机'的方式存在一些问题。最后,我们利用了JavaScript的'原型继承'特性,解决了'空间利用'问题。'榨汁机'只是我们讲故事的道具,真正想表达的是JavaScript的各种特性。
下次如果有时间,我们一起聊聊JavaScript社区中的设计模式。这里先举一个小故事。
有一天,老板对小明说,“小明啊,周末我们超市苹果打特价,6.98一斤,你画一张海报宣传一下。”于是,小明设计了下面的一张海报:

老板说,“设计得不错,果然身手不凡哈!这样,你再画1000份,明天周五给你放一天假,你去东门路口把画好的1000份宣传单发了。”
"再画1000份?!"
现在已经是周四下午5点,你觉得小明是会拿起画笔赶紧开始画呢?还是拿起画笔赶紧开始画呢?当然不会啦,小明肯定会选择拿着之前画好的宣传海报,去扫描复印1000份。

生活中会遇到许多的'迫不得已',所以,我们要学会聪明做事,善待自己。只有聪明地做事,才能使用户满意,老板开心,自己也不闹心。而对于我们写代码的人来说,了解一些'设计模式',也许能使自己做到:聪明地做事情,优雅地写代码,从容面对各种'迫不得已'。
感谢诸位捧场,谢谢。

直播开始:'云榨汁机'诞生记--聊聊JavaScript中的'业务建模'的更多相关文章

  1. 面试说:聊聊JavaScript中的数据类型

    前言 请讲下 JavaScript 中的数据类型? 前端面试中,估计大家都被这么问过. 答:Javascript 中的数据类型包括原始类型和引用类型.其中原始类型包括 null.undefined.b ...

  2. 聊聊Javascript中的AOP编程

    Duck punch 我们先不谈AOP编程,先从duck punch编程谈起. 如果你去wikipedia中查找duck punch,你查阅到的应该是monkey patch这个词条.根据解释,Mon ...

  3. 来聊聊JavaScript中的防抖和节流

    目录 JavaScript防抖和节流 问题还原 防抖 什么是防抖 使用场景 节流 什么是节流 使用场景 JavaScript防抖和节流 问题还原 我们先来通过代码把常见的问题还原: <html& ...

  4. 聊聊javascript中的面向对象

    面向对象这个东西一直晕晕乎乎的,正好这段时间没有活,可以好好整理整理了! 1.什么是对象? 其实这个说起来一切东西都是对象 2.目前我们使用对象的时候,使用的是两种设计模式杂糅起来的 分别是原型模式和 ...

  5. 【跟着子迟品 underscore】JavaScript 中如何判断两个元素是否 "相同"

    Why underscore 最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中. 阅读一些著名框架类库的源码,就好像和一个个大师对 ...

  6. JavaScript中的正则表达式(终结篇)

    JavaScript中的正则表达式(终结篇) 在之前的几篇文章中,我们了解了正则表达式的基本语法,但那些语法不是针对于某一个特定语言的.这篇博文我们将通过下面几个部分来了解正则表达式在JavaScri ...

  7. javascript中的原型继承

    在Javascript面向对象编程中,原型继承不仅是一个重点也是一个不容易掌握的点.在本文中,我们将对Javascript中的原型继承进行一些探索. 基本形式 我们先来看下面一段代码: <cod ...

  8. 谈谈javascript 中的函数问题

    聊聊javascript中的函数 本文可作为李刚<疯狂htmlcssjavas讲义>的学习笔记 先说一个题外话 前几天在知乎上流传着一个对联  上联是雷锋推到雷峰塔 nnd 这是什么对联? ...

  9. JavaScript中的this指向规则

    首先,JavaScript的this指向问题并非传说中的那么难,不难的是机制并不复杂,而被认为不好理解的是逻辑关系和容易混淆的执行上下文.这篇博客也就会基于这两个不好理解的角度来展开,如要要严格的来对 ...

随机推荐

  1. ChRoomtst

    https://github.com/JawaJedi/ChRoomtst

  2. 使用ajax和window.history.pushState无刷新改变页面内容和地址栏URL

    <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content ...

  3. fir.im Weekly - 2016 移动开发技术大回顾

    2016 年是移动技术发展迅速的一年,认认真真回顾这一年必不可少.@移动开发前线 的 这篇 2016移动开发技术巡礼 ,精心盘点了 2016 年 移动开发技术大事件,分为 iOS/Android平台篇 ...

  4. hdu1050 Moving Tables

    题目:http://acm.hdu.edu.cn/showproblem.php?pid=1050 求区间上点的最大重叠次数. #include <stdio.h> #include &l ...

  5. 看完《Don't make me think》的总结

    寒假在公司实习,然后公司人数比较少,作为一个前端实习生,分工下,就去负责了项目的业务逻辑的梳理以及页面的设计,为了让页面设计的好看,交互性好,便于用户使用,我就快速看了这本薄薄的却很有用的书.书的整体 ...

  6. hadoop环境安装及简单Map-Reduce示例

    说明:这篇博客来自我的csdn博客,http://blog.csdn.net/lxxgreat/article/details/7753511 一.参考书:<hadoop权威指南--第二版(中文 ...

  7. openlayers加载百度地图

    最近在做openlayers添加百度地图的扩展类,经过轮番的尝试,终于将其接入了,但是发现偏差比较大,有根据百度的坐标进行了比对,将切片原点进行了调整,发现OK了.打开百度地图,可以看出切片的路径如: ...

  8. 【开源项目4】Android ExpandableListView

    如果你对Android提供的Android ExpandableListView并不满意,一心想要实现诸如Spotify应用那般的效果,那么SlideExpandableListView绝对是你最好的 ...

  9. epoll实现linux进程通信

    server.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <s ...

  10. Swift 2.0学习

    前几天在苹果官方文档学习iOS9 3D Touch的时候,发现苹果关于3D Touch的smaple code,竟然是用Swift语法写的,并且根本没有OC版本的. 看来学习Swift语法是势在必行了 ...