Skip to content

四火的唠叨

一个纯正程序员的啰嗦

Menu
  • 所有文章
  • About Me
  • 关于四火
  • 旅行映像
  • 独立游戏
  • 资源链接
Menu

JavaScript 重构攻略

Posted on 05/15/201109/25/2019 by 四火

flash

[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 博客上的连载,你是否从中有所收获呢?:)

 

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

×Scan to share with WeChat

你可能也喜欢看:

  1. Javascript Memoizer
  2. JavaScript 3D 图表
  3. JavaScript 实现继承的几种方式
  4. Function Invocation Patterns
  5. 关于接口设计,还有 Fluent Interface,这种有趣的接口设计风格

4 thoughts on “JavaScript 重构攻略”

  1. ycf says:
    08/04/2015 at 4:37 PM

    拜读了~

    Reply
  2. aflext says:
    07/15/2014 at 2:07 PM

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

    Reply
  3. 陈默 says:
    09/20/2012 at 2:40 PM

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

    Reply
  4. hap says:
    08/10/2012 at 8:44 AM

    好文, 能总结的都总结了

    Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

订阅·联系

四火,啰嗦的程序员一枚,现居西雅图

Amazon Google Groovy Hadoop Haskell Java JavaScript LeetCode Oracle Python Spark 互联网 前端 华为 历史 同步 团队 图解笔记 基础设施 工作 工作流 工具 工程师 应用系统 异步 微博 思考 技术 数据库 曼联 测试 生活 程序员 管理 系统设计 缓存 编码 编程范型 英语 西雅图 设计 评审 问题 面试 项目

分类

  • Algorithm and Data Structure (30)
  • Concurrency and Asynchronization (6)
  • System Architecture and Design (43)
  • Distributed System (18)
  • Tools Frameworks and Libs (13)
  • Storage and Data Access (8)
  • Front-end Development (33)
  • Programming Languages and Paradigms (55)
  • Testing and Quality Assurance (4)
  • Network and Communication (6)
  • Authentication and Authorization (6)
  • Automation and Operation Excellence (13)
  • Big Data and Machine Learning (5)
  • Product Design (7)
  • Hiring and Interviews (14)
  • Project and Team Management (14)
  • Engineering Culture (17)
  • Critical Thinking (25)
  • Career Growth (57)
  • Life Experience and Thoughts (45)

推荐文章

  • 谈谈分布式锁
  • 常见分布式系统设计图解(汇总)
  • 系统设计中的快速估算技巧
  • 从链表存在环的问题说起
  • 技术面试中,什么样的问题才是好问题?
  • 从物理时钟到逻辑时钟
  • 近期面试观摩的一些思考
  • RSA 背后的算法
  • 谈谈 Ops(汇总 + 最终篇):工具和实践
  • 不要让业务牵着鼻子走
  • 倔强的程序员
  • 谈谈微信的信息流
  • 评审的艺术——谈谈现实中的代码评审
  • Blog 安全问题小记
  • 求第 K 个数的问题
  • 一些前端框架的比较(下)——Ember.js 和 React
  • 一些前端框架的比较(上)——GWT、AngularJS 和 Backbone.js
  • 工作流系统的设计
  • Spark 的性能调优
  • “残酷” 的事实
  • 七年工作,几个故事
  • 从 Java 和 JavaScript 来学习 Haskell 和 Groovy(汇总)
  • 一道随机数题目的求解
  • 层次
  • Dynamo 的实现技术和去中心化
  • 也谈谈全栈工程师
  • 多重继承的演变
  • 编程范型:工具的选择
  • GWT 初体验
  • java.util.concurrent 并发包诸类概览
  • 从 DCL 的对象安全发布谈起
  • 不同团队的困惑
  • 不适合 Hadoop 解决的问题
  • 留心那些潜在的系统设计问题
  • 再谈大楼扔鸡蛋的问题
  • 几种华丽无比的开发方式
  • 我眼中的工程师文化
  • 观点的碰撞
  • 谈谈盗版软件问题
  • 对几个软件开发传统观点的质疑和反驳
  • MVC 框架的映射和解耦
  • 编程的未来
  • DAO 的演进
  • 致那些自嘲码农的苦逼程序员
  • Java 多线程发展简史
  • 珍爱生命,远离微博
  • 网站性能优化的三重境界
  • OSCache 框架源码解析
  • “ 你不适合做程序员”
  • 画圆画方的故事

近期评论

  • + 1.943624 BTC.NEXT - https://graph.org/Ticket--58146-05-02?hs=9a9c6f8dfe3cdbe0074006e3e640b19b& on 所有文章
  • Anonymous on 闲聊投资:亲自体验和护城河
  • 四火 on 关于近期求职的近况和思考
  • YC on 关于近期求职的近况和思考
  • mafulong on 常见分布式基础设施系统设计图解(四):分布式工作流系统
  • 四火 on 常见分布式基础设施系统设计图解(八):分布式键值存储系统
  • Anonymous on 我裸辞了
  • https://umlcn.com on 资源链接
  • Anonymous on 我裸辞了
  • Dylan on 我裸辞了
© 2025 四火的唠叨 | Powered by Minimalist Blog WordPress Theme