JavaScript重构攻略

JavaScript重构攻略

[Updated 11/3/2017] 文章写在好多年前,由于时代和自身技术水平的限制,很多文中的观点都已经不准确。现在有好的多的方法和工具来完成JavaScript重构。

一、模块划分和命名空间

通常我们的团队中,开发人员在Java语言层面具备相当的技术素养,经验丰富,而且有许多成熟的、合理的规约,类型繁多的代码隐患检查工具,甚至在团队间还有计划内的评审和飞检。但是前端的代码不似后台,就像一个没人疼的孩子,不仅仅容易被低估、被轻视,导致质量低劣、可维护性差,技能上,更缺少优秀的前端开发人员。

JavaScript是前台代码中重要组成部分,随着版本的延续,产品越做越大,JavaScript层面的重构,需要在整个过程中逐步强化起来。

当代码量达到一定程度,JavaScript最好能够与页面模块组件(例如自定义的FreeMarker标签)一起被模块化。

模块化带来的最大好处就是独立性和可维护性,不用在海量的js中定位问题位置,简单了,也就更容易被理解和接受,更容易被定制。

模块之间的依赖关系最好能够保持简单,例如有一个common.js,成为最通用的函数型代码,不包含或者包含统一管理的全局变量,要求其可以独立发布,其他组件js可以轻松地依赖于它。举个例子,我们经常需要对字符串实现一个trim方法,可是js本身是不具备的,那么就可以在这个common.js中扩展string的prototype来实现,这对外部的使用者是透明的。

使用命名空间是保持js互不干扰的一个好办法,js讲究起面向对象,就必须遵循封装、继承和多态的原则。

参照Java import的用法,我希望命名空间能带来这样的效果,看一个最简单的实例吧:

我有一个模块play,其中包含了一个方法webOnlinePlay,那么在没有import这个模块的时候,我希望是js的执行是错误的:

webOnlinePlay(); //Error! 无法找到方法

但是如果我引入了这个模块:

import("play");  
webOnlinePlay(); //正确,能够找到方法

其实实现这样的效果也很简单,因为默认调用一个方法webOnlinePlay()的实质是:window.webOnlinePlay(),对吗?

所以在import(“play”)的时候,内部实现机制如下:

var module = new playModule();

对于这个模块中的每一个方法,都导入到window对象上面,以直接使用:

window[methodName] = module[methodName];

其实这里并没有什么玄机,但是这种即需即取的思想却给前端重构带来了一个思路,一个封装带来的可维护性增强的思路,不是吗?

聪明的你也许还会提到一个问题:

如果我没有import这个play模块,这个页面都不需要,那我能否连这个play.js都不加载呢?

当然可以,请关注后面的分解——关于js的动态加载的部分。

————————————————————————————————————–

二、JS的动态加载

前一节留下了一个问题,如果JS分门别类也清晰了,那我现在需要在必要的情况下才加载某一模块的JS,这个怎么实现呢?

方法一,最简单也是最容易被接受的方法,通过后台代码来控制,还是少些复杂的JS吧,通过一个标签、一个分支判断,就可以做到,何乐而不为呢?

方法二,如果要使用纯JS来控制,那么看看这样如何:

$.ajax(){   
  url:"xxx/play.js";   
  ……   
  success:function(res){   
    eval(res.responseText);   
  }   
}

原理是很简单,不过有一个藏匿着的魔鬼:eval,js加载的生效就靠它了,那么执行的上下文就在它的里面,这就会带来一些潜在的问题,而且,调试也变得困难。

方法三,通过添加<script>标签的方式来动态引入脚本:

原理相信大家也马上能领悟个大概了,需要的时候动态地往页面的<head>里面写一对<script>标签,让浏览器自己去取需要的js,这样的就解决了方法二里面的魔鬼eval的问题,是一个比较好的方法:

<script src="xxx/play.js" ... />

这里啰嗦一句,<script>标签中的src——本质上不就是对src所表示的地址发送一个get请求吗?这虽然看起来有点歪门邪道,却恰恰是一个跨域问题的解决办法!因为浏览器对<script>引用js页面可没有同域的安全限制(以前转载过过一篇跨域问题的讨论,见此)。

方法四,很多JS框架都提供了易于使用的JS动态加载的方法,比如JQuery的loadScript方法,这里不讨论了。

另外,如果使用上pushlet的方法,对一个js文件无止境地读取,能否实现这样的效果呢?仅作设想,希望有人与我讨论。

————————————————————————————————————–

三、JavaScript的测试

进行JavaScript重构时,我希望引入易于使用的测试框架来保证重构的顺利进行,未来能持续通过测试代码对JavaScript逻辑的正确性做保障。

JsUnit (http://sourceforge.net/projects/jsunit/http://www.jsunit.net/

JsUnit是一个独立的JavaScript单元测试框架,和JUnit差不多,没有上手难度,包括传统的setUp和tearDown,提供的assert方法也和JUnit类似,多了assertNaN和assertUndefined等等JavaScript特有的方法。测试页面必须在<head>里面引入jsUnitCore.js这个js文件。

测试套件的支持:提供了addTestPage和addTestSuite;

测试日志的支持:包括warn、info和debug三种日志级别,前端编码不似后台代码,正式代码中不宜使用过多log,再说log也只有FF下才支持。

千言万语不及一个例子:

<script language="javascript" src="jsUnitCore.js"></script>   
<script language="javascript" src="play.js"></script> //模块JS 

function testWithMainProcess() {   
  assertEquals("Web play url", "##http://...##", webOnlinePlay());   
}

项目的代码里到处是Ajax调用,要做单元测试,看来打桩是不可避免了。Mock类的工具有许多,比如适合JQuery的QMock:

var mockJquery = new Mock();   
mockJquery   
.expects(1)   
.method('ajax')   
.withArguments({   
  url: 'http://xxx, 
  success: Function,   
  dataType: "jsonp"
})   
.callFunctionWith({ feed : { entry : "data response" }});

这个桩正是mock了一个假的ajax jason返回:[feed:[entry:"data response"]],看看,使用就和以前接触过的EasyMock差不多嘛。

对于JavaScript测试框架感兴趣的同学还可以了解一些其他的测试框架,例如JSpec。

单元测试代码建议就放在模块的包内:test.html,即便理想状况下,模块单独发布时,也是伴随着测试用例的可靠的前端代码。

从哪些JavaScript代码开始做?

1、函数式的代码。这样的代码保证独立性好,也不需要打什么桩,测试成本低。

2、复杂的逻辑。

是否尝试TDD?不建议在我们团队内部使用,前端TDD需要更高的技巧,对人的因素要求更高。如果有一天,后台Java代码的TDD做好了,那么换成JavaScript的代码,没有本质区别。

如果效果得当,为什么不能把JavaScript的UT集成到ICP-CI上作为持续集成的一部分呢?

————————————————————————————————————–

四、JavaScript编码规则

没有规矩,不成方圆,JavaScript带来了灵活性,也带来了不受控的变量和访问,所以要用规则限制它。一支成熟的团队,还是一支新鲜的团队,规则应当是不一样的,我只是列出一些常见的或者有效的办法,来约束跳跃的开发人员,思维可以任意飞跃,代码却要持续受控。当然,任何规则都是建立在一定的认知基础之上的,面向对象JavaScript的基础是必备的,否则一切无从谈起。

变量和方法控制:

模块开发不允许存放独立的全局变量、全局方法,只允许把变量和方法放置到相应模块的“命名空间”中。实在心痒了,那么使用匿名函数如何?

(function() {   
  var value = 'xxx';   
  var func = function() {...};   
})();

模块化需要严格控制住代码的区域性,这不仅仅是代码可维护性、可定制性的一方面,同时也让JavaScript引擎在属性和方法使用完毕后及时地回收掉。

不允许在模块代码中污染原生对象,例如

String.prototype.func = new function(){...};

如此的代码必须集中控制,例如统一放置在common.js中,严格保护起来。

数据存放约束:

普通变量、prototype变量和function变量分而治之,方法名一律大写开头,变量名还是遵从骆驼命名法如何:

function T(name){   
  T.prototype._instance_number++;   
  this.name = name;   
  this.showName=function(){   
    alert(this.name);   
  }   
};   
T.prototype = {   
  _instance_number:0,   
  getInstanceNum: function(){   
    return T.prototype._instance_number;   
  }   
};   

var t = new T("PortalONE");   
t.showName();   
new T("Again");   
alert(t.getInstanceNum()); //打印:2

这里有意做了一件事情,T内部的属性和私有方法使用下划线开头,这样很好地实现了封装(上述代码中如果使用t.instanceNum,是无法访问到这个值的),如果这段代码都看不懂的话,赶紧温习一下JavaScript的面向对象吧 :)。JavaScript中提供了闭包和原型两种办法来实现继承和多态,关于重构中应用这一点,后续的章节我再啰嗦吧。

另外,优先使用JavaScript的原生对象和容器,比如Array,Ajax的数据类型统一切到JSON上来,尽量不要使用隐藏域;另外,通常是不允许随意扩展DOM对象的。

利用闭包特性,这一点可以做得更优雅:

var User = function(){  
    var name;  
    this.setName = function(newName){  
        name = newName;  
    };  
    this.getName = function(){  
        return name;  
    };  
};

访问的时候具备如下封装效果:

var user = new User();  
user.setName("abc");  
alert(user.getName()); //abc
alert(user.name);         //undefined

至于模块间的通信:模块间的通信意味着模块间的耦合性,是需要严格避免的;通信的途径通常使用方法级属性或者模块级的prototype变量。

DOM操纵规则:

在模块代码中,通常要求把对DOM的操纵独立到模块js中,应当避免在DOM模型上显示地写时间触发函数,例如:

<div onclick="xxx" />

借助JQuery基于bind的一系列方法,把行为逻辑独立出来以后,完全可以看到清爽的HTML标签。

DOM对象的访问通常使用id来查找,偶有根据name来查找的,过多次数地、不合理地遍历DOM树是前端性能保持的大忌。

CSS的样式控制:

(1)尽量拒绝style=”xxx”的写法,主要目的是将样式统一到主题样式表单中,当然主题样式表单也是按模块存放的,对于不同语种的定制和不同风格的切换带来便利。

(2)规约JavaScript对样式的操纵,理想状况下,封装性好的UI可以自由地替换它的样式集合。

以上只能算冰山一角,抛砖引玉,实际项目中需要在开发过程中逐步细化和完善。

————————————————————————————————————–

五、利用原型和闭包,完成组件方法

var Player = (function(){   
  Player = function(){ //这只是个空壳   
  throw new Error("Can not instantiate a Player object.");   
};   
Player.MIN_EXTENDED_TIME = 1;   
Player.MAX_EXTENDED_TIME = 3;   
Player._player = false;   
Player.getInstance = function(){   
  if(!Player._player){   
    alert("Init...");   
    Player._player = {   
      _name : name,   
      setName : function(name){   
        this._name = name;   
      },   
      toString : function(){   
        return "Player: " + this._name;   
      }   
    };   
  }   
  return Player._player;   
};   
return Player; //把修缮完工的Player这个组件方法返回   
})();   

//var player = new Player(); //new Player()会抛出异常   
var player1 = Player.getInstance();   
var player2 = Player.getInstance();   
player2.setName("RealPlayer");   
alert(player2.toString()); //输出RealPlayer

终于要定义一个组件方法了,利用原型来实现。看看这样如何:

function Player(name){   
  Player.MIN_EXTENDED_TIME = 1;   
  Player.MAX_EXTENDED_TIME = 3;   
  this._name = name;   
};   
Player.prototype.setName = function(name){   
  this._name = name;   
};   
Player.prototype.toString = function(){   
  return "Player: " + this._name;   
};   

var player = new Player("WindowsMediaPlayer");   
alert(player.toString()); //输出WindowsMediaPlayer   
player.setName("RealPlayer");   
alert(player.toString()); //输出RealPlayer   
alert(Player.MAX_EXTENDED_TIME);

恩,有封装、有常量、也有复写了Object的toString方法,至于继承之类的事情,咱们后面再说,初看看还不错。可是这样的组件方法定义不够优雅,也不够直观,方法都是放在独立的位置定义的,并没有和最开始的组件方法放置在一起,如果能像Java那样定义岂不更好?

对了,可以用闭包来实现。试试看吧:

function Player(name){   
  Player.MIN_EXTENDED_TIME = 1;   
  Player.MAX_EXTENDED_TIME = 3;   
  this._name = name;   
  this.setName = function(name){   
    this._name = name;   
  };   
  this.toString = function(){   
    return "Player: " + this._name;   
  };   
};   

var player = new Player("WindowsMediaPlayer");   
alert(player.toString()); //输出WindowsMediaPlayer   
player.setName("RealPlayer");   
alert(player.toString()); //输出RealPlayer   
alert(Player.MAX_EXTENDED_TIME);

不像Groovy里面,闭包做了很大程度上的强化,包括新的语法的支持;JavaScript的闭包是很简单的闭包,它没有特殊的需要额外学习的语法,任意一个function,里面只要包含未绑定变量,这些变量是在function所属的上下文环境中定义的,那么,这个function就是闭包。顺便罗嗦一句,和闭包相反的,不正是不包含任何未绑定变量的函数式代码吗?

写是写好了,可是转念一想,Player应当只有一份,它是单例的,最好我也能像Java那样弄一个单例模式出来 :),可是事不遂愿,我没有办法在JavaScript做一个private的构造器,用这种思路去实现单例模式似乎不可行……

怎么办?

然而天无绝人之路,我控制不了你new一个Player的对象,我却可以控制你new出来的这个Player对象的属性和行为!当你需要使用你new出来的Player的对象的时候,你发现根本无法完成,或者它只是一个空壳!真正的东西还是要靠单例中经典的getInstance方法来获得:

function Player(){   
  throw new Error("Can not instantiate a Player object.");   
}; //这只是个空壳   

(function(){ //这才是货真价实的东西   
  Player.MIN_EXTENDED_TIME = 1;   
  Player.MAX_EXTENDED_TIME = 3;   
  Player._player = false;   
  Player.getInstance = function(){   
    if(!Player._player){   
      alert("Init...");   
      Player._player = {   
        _name : name,   
        setName : function(name){   
          this._name = name;   
        },   
        toString : function(name){   
          return "Player: " + this._name;   
        }   
      };   
    }   
    return Player._player;   
  };   
})();   

//var player = new Player(); //new Player()会抛出异常   
var player1 = Player.getInstance();   
var player2 = Player.getInstance();   
player2.setName("RealPlayer");   
alert(player2.toString()); //输出RealPlayer

好,真不错,单例模式在JavaScript下也成功实施了——你要胆敢new Player();就会抛出一个异常,这样什么也得不到,只有用getInstance方法得到的对象才是真真正正的Player对象。上面的代码整个执行的结果,只弹出了一次”Init…”的对话框,说明真正的“构造器逻辑”只调用了一次。

都做到这份上了,依然有小小的遗憾,Player的定义依然被拆分成了两部分,一部分定义空壳,一部分是一个匿名函数来定义Player的常量和getInstance方法。这两部分就不能合二为一么?

能。只需要用到一个小小的匿名函数,如果耐心从头看到这里,也一定能理解:

var Player = (function(){   
    Player = function(){ //这只是个空壳   
        throw new Error("Can not instantiate a Player object.");   
    };   
    Player.MIN_EXTENDED_TIME = 1;   
    Player.MAX_EXTENDED_TIME = 3;   
    Player._player = false;   
    Player.getInstance = function(){   
        if(!Player._player){   
            alert("Init...");   
            Player._player = {   
                _name : name,   
                setName : function(name){   
                    this._name = name;   
                },   
                toString : function(name){   
                    return "Player: " + this._name;   
                }   
            };   
        }   
        return Player._player;   
    };   
    return Player; //把修缮完工的Player这个组件方法返回   
})();  

//var player = new Player(); //new Player()会抛出异常   
var player1 = Player.getInstance();   
var player2 = Player.getInstance();   
player2.setName("RealPlayer");   
alert(player2.toString()); //输出RealPlayer

到此,终于如释重负,深入理解JavaScript面向对象,用好原型和闭包这两把锋利的武器,才能写出优秀的前端代码来。有一些同事私下和我交流,后面我尽量贴简洁的代码,希望有面向对象基础和JavaScript基础的同事都能有所收获。

————————————————————————————————————–

六、利用继承来做事

终于要说到JavaScript的继承了,原型链继承是最常用的一种方式:

function Video(){};   
function Movie(){};   
Movie.prototype = new Video();   
Movie.prototype.constructor = Movie; //不要丢失构造器

啰嗦一句,如果我拿到的是方法的实例,一样可以做继承:

function Video(){};   
function Movie(){};   

var video = new Video();   
video.size = 3;   
video.toString = function(){   
  return "video";   
};   
video.getName = function(){   
  return "VideoXXX";   
};   
var movie = new Movie();   
(function inherit(parent,child){   
  for(var ele in parent){   
    if(!child[ele]) //在child不包含该属性或者方法的时候,才会拷贝parent的一份   
      child[ele] = parent[ele];   
    }   
})(video,movie); //匿名函数调用的方式   

alert(movie.size); //3   
alert(movie.toString()); //[object Object]   
alert(movie.getName()); //VideoXXX

可是这种方法是不纯粹继承的,可见其中的toString方法由于是原生方法,无法用var ele in parent遍历到的。

如果仅仅想覆写父类的某个方法,还可以使用call或者apply尝试一下方法的this大挪移,略。

原型链继承看起来似乎是最自然和最具亲和力的继承方式了,但是还记得上一节中对于单例模式的处理吗?我使用了getInstance方法去取得一个唯一的实例,而不是new,这样原型对其实例化起不到作用了:

var Player = (function(){   
  Player = function(){ //这只是个空壳   
  throw new Error("Can not instantiate a Player object.");   
};   

Player.MIN_EXTENDED_TIME = 1;   
Player.MAX_EXTENDED_TIME = 3;   
Player._player = false;   

Player.getInstance = function(){   
  if(!Player._player){   
    alert("Init...");   
    Player._player = {   
      _name : name,   
      setName : function(name){   
        this._name = name;   
      },   
      toString : function(name){   
        return "Player: " + this._name;   
      }   
    };   
  }   
  return Player._player;   
  };   
  return Player; //把修缮完工的Player这个组件方法返回   
})();

现在,我要创建一个WindowsMediaPlayer,去继承上面的Player,怎么做?

这里提供两条思路:

(1)获取Player的实例,然后遍历实例中的方法和属性,构造一个全新的WindowsMediaPlayer,其它的属性照抄Player,但是唯有getInstance方法需要覆写。这个方式不够优雅,而且getInstance方法可能会很复杂和冗余,也许不是一个很好的思路。

(2)从对象设计的角度来说,一个单例的类,本身就不适合被继承,那么,还不如把Player做成一个纯粹的抽象层,让单例这个工作交给其子类WindowMediaPlayer去完成。这个方式要好得多,至于如何把一个function做成一个抽象层,呵呵,咱们下回再说。

————————————————————————————————————–

七、重用老代码

在Java中,有这样一段老代码:

class Round{   
  public void drawRound(); //画圆   
}

现在新代码希望能和它共存,使用一个Person的对象来控制,只不过,可能drawRound,也可能drawRect啊:

class Rect{   
  public void drawRect(); //画方   
}

好,废话少说,我先想到了Adapter模式:

interface Drawable{   
  public void draw();   
}   

public class RoundAdapter implements Drawable{   
  private Round round;   
  public void draw(){   
    round.drawRound();   
  }   
}   

public class RectAdapter implements Drawable{   
  private Rect rect;   
  public void draw(){   
    rect.drawRect();   
  }   
}

然后,我再引入一个Person对象,就能搞定这一切了:

class Person{   
  private Drawable adapter;   
  public Person(Drawable adapter){   
    this.adapter = adapter;   
  }   
  public void draw(){   
    this.adapter.draw();   
  }   
}   

  Drawable rou = new RoundAdapter();   
  Drawable rec = new RectAdapter();   
  new Person(rou).draw(); //画圆  
  new Person(rec).draw(); //画方

想必到此已经让你烦了,一个Adapter模式的最简单例子。再多看一看,这个模式的核心是什么?接口!对,正是例子中的Drawable接口——正是在接口的规约和领导下,我们才能让画圆和画方都变得那么听话。

现在JavaScript中,也有这样一段老代码:

function Round(){   
this.drawRound = function(){   
    alert("round");   
  }   
}

我也想依葫芦画瓢,但是JavaScript没有接口了,怎么办?

……

接口的作用是什么?是对类的行为的规约,可是JavaScript的行为是动态的,无法用简单纯粹的接口来实现、来约束,即便模拟出这样一个接口(参见《JavaScript Design Pattern》),在此又有必要使用它么?强行做出一个接口来,这不是和JavaScript的初衷相违背了吗?

再回到这个问题上面,我原本希望Person的对象可以调用一个统一的draw方法,只是在通过构造Person对象的时候,传入一个不同实现的Drawable对象,做出了不同约束下的实现。

那么,JavaScript中,不仅仅方法的调用者可以作为一个参数传入,方法本身也可以作为参数传入(即所谓方法闭包),这样,所有变化点都控制在这个参数之中,不也实现了我想要的接口规约的效果吗:

function Rect(){   
  this.drawRect = function(){   
    alert("rect");   
  }   
}   

function Person(obj){   
//obj参数的格式:{doWhat,who}   
  for(var i in obj){   
    this.doWhat = i;   
    this.who = obj[i];   
    break;   
  }   
  this.draw = function(){   
    this.who[this.doWhat].call(this.who);   
  };   
}   

var rou = { drawRound : new Round() };   
var rec = { drawRect : new Rect() };   
(new Person(rou)).draw();   
(new Person(rec)).draw();

写到这里,我觉得很开心:

在Java中,通过接口的规约和适配器的帮助,我将变化点封装在Person构造器的参数之中;

JavaScript中,没有了接口、脱离了适配器的帮助,我依然能将变化点封装在Person的构造器参数之中。

————————————————————————————————————–

八、JSDoc和JSLint

JSDoc可以生成类似于JavaDoc一样的API文档,这对于前端开发是必不可少的。

下载jsdoc-tookit(http://code.google.com/p/jsdoc-toolkit/)和jsdoc-tookit-ant-task(http://code.google.com/p/jsdoc-toolkit-ant-task/),DOC生成器对于任何一个成熟的前端开发团队都是必不可少的。

<project default="build-docs">   
    <target name="build-docs">   
        <property name="base" location="." />   
        <taskdef name="jsdoctoolkit" classname="uk.co.darrenhurley.ant.tasks.JsDocToolkit" classpath="jsdoc-toolkit-ant-task-1.1.0.jar;jsdoc-toolkit/java/classes/js.jar"/>   
        <jsdoctoolkit template="jsdoc" jsdochome="${base}/jsdoc-toolkit/" outputdir="${base}/output/">   
            <source file="portalone-common.js" />   
        </jsdoctoolkit>  
    </target>   
</project>

JSLint是用来对JavaScript代码做静态检查的工具(http://jslint.com/),不过这个应该不是开源的;而且需要ruby运行环境和gvim,再配合cscript engine,使用起来有诸多不便。项目中不可能总使用在线版本。

Eclipse上也开发了相应的JSLint plugin,另外,有一个很方便的工具jslint-toolkit(http://code.google.com/p/jslint-toolkit/):

需要配置config.json:

{   
  // JavaScript files to check   
  //"includes": ["scripts//source", "scripts//jquery"],   
  "includes": ["scripts//my"],   
  // Exclude files   
  "excludes": [],   
  // Exclude file names (Regex expression)   
  "excludeNames": ["//.svn", "CVS"],   
  // Output directory   
  "outPath": "out"   
}

输出结果一目了然。

————————————————————————————————————–

九、自定义JavaScript产品框架

产品做到一定程度,JavaScript不仅仅需要几个层面上的重构,而需要将这些合理的、零散的重构集成起来、系统化,最终形成一套适合自己产品的前端框架。

以某套产品的前端框架为例,包含了这么几个组件:

  • 1、通用工具组件,提供了UI组件最基础的通用能力,包括:日志、缓存、数据共享、数据异步加载、原生对象扩展、Ajax产品定制化等等。
  • 2、共享UI组件,包括:通用弹出框、通用按钮等。
  • 3、产品基础模块,在所有页面均加载该JS,包括:评论模块、打分模块、基本资费模块、下载模块、播放模块等等。
  • 4、扩展产品模块,仅在特定页面加载该JS,包括:播放器组件、直播频道组件等等。
  • 5、关联常量预置模块,这部分主要是一些系统常量无法在JavaScript中确定下来,需要外部传值进去。

(依赖关系:5->1->2->3->4)

上述JS在开发过程中需要细化,并且需要严格限定互相之间的依赖关系,但在发布时,使用脚本或者JS聚合压缩工具整合到特定的一个或几个JS文件中。

UI模型和业务模型:

这部分可以说是框架的核心,包括模型的定义和模型数据的存储,所有的接口都是围绕模型制定的。

产品框架实施中遇到的几个典型问题:

1、页面JavaScript脚本对于不同语种下需要保持的差异:

譬如阿拉伯语是从右至左排的,那么对于操纵DOM的脚本来说,很可能和英文下有不同的差异,通常语种引起的差异可以用问题抽象和语种归类的办法化解:

比如语言文字从右向左排列和从左向右排列是造成某些展示不同的根本原因,那么在关联常量预置模块中设置好语种,涉及到的语种和左右排列方向的对应关系应当存放在代码中,最后在JavaScript代码中区分对待就可以了。

2、页面上的一些非通用的DOM操纵密切相关的代码和页面展示耦合紧密,这部分代码是不宜置入框架中的,置入后反而不便于产品定制,需要明确这个框架内外的分界线是什么。

3、结合开发团队技能情况制定详细的产品框架实现方案。

比如开发团队成员普遍缺乏JavaScript面向对象能力,这时候就不应当把框架做得太厚,应该对框架外的JavaScript使用适当放宽限制,同时做好命名规约。

4、API接口把关。

需要由有经验的程序员对于框架发布的接口把关,保证接口设计的合理性。

————————————————————————————————————–

十、强化对象封装和模块封装

1、类本身就是一种封装形式,先来看看最简单的封装,JavaScript中没有private关键字,对于私有成员,不如我们统一一个以下划线开头的命名来标识:

var User = function(name){  
    this._name = name;  
    this.getName = function(){  
        return _name;  
    };  
};

2、不过,上面的办法还不够好,我依然可以用user._name访问到这个变量。现在换个思路,通过使用var来定义User中的name属性,并且通过getName方法来给它暴露访问入口,实现了private一样的效果:

var User = function(name){  
    var name = arguments[0];  
    this.getName = function(){  
        return name;  
    };  
};  
User.SORT = 1;  

var user = new User("Test");  
alert(user.getName());  //正确打印  
alert(user.name);       //封装起来的私有成员,不能随意访问  
alert(User.SORT);       //类变量

3、通过匿名方法,把代码块的影响范围限制在一定区域内:

(function($){  
    $.fn.extend({  
        sayHi : function(){  
            alert("Hi: " + this.get(0).tagName);  
        }  
    });  
})(jQuery);  

jQuery("body").sayHi();

上例中,外部由于命名冲突的关系,无法使用“$”来获取jQuery的引用,但是通过这样匿名函数的调用,在函数实现内部依然可以使用到“$”,并且给JQuery的原型增加了一个sayHi的方法。

4、命名空间带来的封装,文章一开始已经介绍过了。

5、通过合理规约JS文件的依赖关系和加载执行顺序,保证区域代码执行时对外部的访问范围:

//首先加载URLUtil的类定义,再加载User的类定义,保证了依赖关系是User依赖于URLUtil,而不会倒置,避免了在URLUtil的代码区域附近去访问User对象  
var URLUtil = {  
    getURL : function(){  
        return "http://xxx";  
    }  
};  

……  

var User = function(){  
    var url;  
    this.setURL = function(newUrl){  
        url = newUrl;  
    };  
};  

……  

var user = new User();  
user.setURL(URLUtil.getURL());

总算完了。这篇文章最早来自我的CSDN博客上的连载,你是否从中有所收获呢?:)

 

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:

6 comments

  1. aflext 说道:

    lz写的不错,但是不太具体。赞一个

  2. [...] JavaScript重构攻略 [...]

  3. 陈默 说道:

    刚刚还在想你这文章可能是转的,后面居然还标注“文章系本人原创……”,原来那个连载就是你写的,囧~~
    总之,谢谢分享!加油!!!

  4. hap 说道:

    好文, 能总结的都总结了

发表评论

电子邮件地址不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>