如何编写可维护的JavaScript代码?

JavaScript这门编程语言发展至今已经很是流行了,各类名词也层出不穷,咱们随便列举下就有一大堆,好比Node.js、jQuery、JavaScript MVC、Backbone.js、AMD、CommonJS、RequireJS、CoffeScript、Flexigrid、Highchart、Script Loader、Script Minifier、JSLint、JSON、Ajax......这么多的东西席卷咱们的脑海,无疑让人头晕目眩。但本质的东西老是不变的,而所谓本质就是一些核心的基础概念。这里的基础不是指JavaScript的表达式、数据类型、函数API等基础知识,而是指支撑上面这么一大堆JavaScript名词背后东西的基础。我知道这样会让我这篇文章很难写下去,由于那将包含太多主题,因此本文只打算管中窥豹:本文将先讲一些概念,而后讲一些实践指导原则,最后涉及一些工具的讨论。javascript

在正式开始这篇博客以前,咱们须要问本身为何代码可维护性值得咱们关注。相信只要你写过至关量的代码后,都已经发现了这点:Fix Bug比写代码困可贵多。花三个小时写的代码,而以后为了Fix其中的一个Bug花两三天时间,这种状况并很多见。再加上Fix Bug的人极可能不是代码原做者,这无疑更雪上加霜。因此代码可维护性是一个很是值得探讨的话题,提升代码可维护性就必定程度上能节省Fix Bug的时间,节省Fix Bug的时间进而就节省了人力成本。html

No 1. 将代码组织成模块

基本任何一门编程语言都认为模块化能提高代码可维护性。咱们知道软件工程的核心在于控制复杂度,而模块化本质上是分离关注点,从而分解复杂度。java

IIFE模块模式

当咱们最开始学习编写JavaSript代码时,基本都会写下面这样的代码:jquery

var myVar = 10;
var myFunc = function() {
   // ...
};

这样的代码自己没有什么问题,可是当这样的代码愈来愈多时,会给代码维护带来沉重的负担。缘由是这样致使myVar和myFunc暴露给全局命名空间,从而污染了全局命名空间。以我我的经验来看,通常当某个页面中的JavaScript代码达到200行左右时就开始要考虑这个问题了,尤为是在企业项目中。那么咱们该怎么办呢?web

最简单的解决方法是采用IIFE(Immediate Invoked Function Expression,当即执行函数表达式)来解决(注意这里是函数表达式,而不是函数声明,函数声明相似 var myFunc = function() { // ... }),以下:ajax

(function() {
   var myVar = 10;
   var myFunc = function() {
      // ...
   };
}) ();

如今myVar和myFunc的做用域范围就被锁定在这个函数表达式内部,而不会污染全局命名空间了。这有点相似”沙盒机制“(也是提供了一个安全的执行上下文)。咱们知道JavaScript中没有块级做用域,能产生做用域只能借助函数,正如上面这个例子同样。算法

可是如今myVar、myFunc只能在函数表达式内部被使用,若是它须要向外提供一些借口或功能(像大部分JavaScript框架或JavaScript库同样),那么该怎么办呢?咱们会采用下面的作法:编程

(function(window, $, undefined) {
   var myFunc = function() {
      // ...
   }
   window.myFunc = myFuc;
}) (window, jQuery);

咱们来简单分析下,代码很简单:首先将window对象和jQuery对象做为当即执行函数表达式的参数,$只是传入的jQuery对象的别名;其次咱们并未传递第三个参数,可是函数却有一个名为undefined的参数,这是一个小技巧,正由于没有传第三个参数,因此这里第三个参数undefined的值始终是undefined,就保证内部能放心使用undefined,而不用担忧其余地方修改undefined的值;最后经过window.myFunc导出要暴露给外部的函数。api

好比咱们看一个实际JavaScript类库的例子,好比 Validate.js,咱们能够看到它是这样导出函数的:数组

(function(window, document, undefined) {
   var FormValidator = function(formName, fields, callback) {
      // ...
   };
   window.FormValidator = FormValidator;
}) (window, document);

是否是与前面说的基本同样?另外一个例子是jQuery插件的编写范式中的一种,以下:

(function($) {    
   $.fn.pluginName = function() {  
     // plugin implementation code
   };  
})(jQuery);

既然jQuery插件都来了,那再来一个jQuery源码的例子也无妨:

(function( window, undefined ) {
   ...
   // Expose jQuery to the global object
   window.jQuery = window.$ = jQuery;
})( window );

上面这样写使得咱们调用jQuery函数既能够用$("body"),又能够用jQuery("body")

命名空间(Namespace)

虽然使用IIEF模块模式让咱们的代码组织成一个个模块,维护性提高了,但若是代码规模进一步增大,好比达到2000-10000级别,这时前面方法的局限性又体现出来了?

怎么说呢?观察下前面的代码,全部函数都是经过做为window对象属性的方式导出的,这样若是有不少个开发人员同时在开发,那么就显得不太优雅了。尤为是有的模块与模块之间可能存在层级关系,这时候咱们须要借助“命名空间”了,命名空间能够用来对函数进行分组。

咱们能够这样写:

(function(myApp, $, undefined) {
   // ...
}) (window.myApp = window.myApp || {}, jQuery);

或者这样:

var myApp = (function(myApp, $, undefined) {
   ...
   return myApp;
}) (window.myApp || {}, jQuery);

如今咱们再也不往当即执行函数表达式传递window对象,而是传递挂载在window对象上的命名空间对象。第二段代码中的 || 是为了不在多个地方使用myApp变量时重复建立对象。

Revealing Module Pattern

这种模块模式的主要做用是区分出私有变量/函数和公共变量/函数,达到将私有变量/函数隐藏在函数内部,而将公有变量/函数暴露给外部的目的。

代码示例以下:

var myModule = (function(window, $, undefined) {
   var _myPrivateVar1 = "";
   var _myPrivateVar2 = "";
   var _myPrivateFunc = function() {
      return _myPrivateVar1 + _myPrivateVar2;
   };
   return {
      getMyVar1: function() { return _myPrivateVar1; },
      setMyVar1: function(val) { _myPrivateVar1 = val; },
      someFunc: _myPrivateFunc
   };
}) (window, jQuery);

myPrivateVar一、myPrivateVar2是私有变量,myPrivateFunc是私有函数。而getMyVar1(public getter)、getMyVar1(public setter)、someFunc是公共函数。是否是有点相似普通的Java Bean?

或者咱们能够写成这种形式(换汤不换药):

var myModule = (function(window, $, undefined) {
   var my= {};
   var _myPrivateVar1 = "";
   var _myPrivateVar2 = "";
   var _myPrivateFunc = function() {
      return _myPrivateVar1 + _myPrivateVar2;
   };
   my.getMyVar1 = function() {
      return _myPrivateVar1;
   };
   my.setMyVar1 = function(val) {
      _myPrivateVar1 = val;
   };
   my.someFunc = _myPrivateFunc;
   return my;
}) (window, jQuery);

模块扩展(Module Augmentation)

有时候咱们想为某个已有模块添加额外功能,能够像下面这样:

var MODULE = (function (my) {
    my.anotherMethod = function () {
        // added method...
    };
    return my;
}(MODULE  || {}));

Tight Augmentation

上面的例子虽然能够为已有模块添加功能,可是若是要扩展示有模块的功能,即在现有模块的某个函数的基础上扩展功能那么该咋办呢?以下:

var MODULE = (function (my) {

    var old_moduleMethod = my.moduleMethod;
    my.moduleMethod = function () {
        // method override, has access to old through old_moduleMethod...
    };
    return my;
}(MODULE));

代码意图很明显:实现了重写原模块的moduleMethod函数。可是有一个地方必须注意,就是这段代码执行前必须确保MODULE模块已经加载。

子模块模式

这个模式很是简单,好比咱们为现有模块MODULE建立一个子模块以下:

MODULE.sub = (function () {
    var my = {};
    // ...
    return my;
}());

No 2. 利用OO

构造函数模式(Constructor Pattern)

JavaScript没有类的概念,因此咱们不能够经过类来建立对象,可是能够经过函数来建立对象。好比下面这样:

var Person = function(firstName, lastName, age) {
   this.firstName = firstName;
   this.lastName = lastName;
   this.age = age;
};

Person.prototype.country = "China";
Person.prototype.greet = function() {
   alert("Hello, I am " + this.firstName + " " + this.lastName);
};

这里firstName、lastName、age能够类比为Java类中的实例变量,每一个对象有专属于本身的一份。而country能够类比为Java类中的静态变量,greet函数类比为Java类中的静态方法,全部对象共享一份。咱们经过下面的代码验证下(在Chrome的控制台输):

var Person = function(firstName, lastName, age) {
   this.firstName = firstName;
   this.lastName = lastName;
   this.age = age;
};

Person.prototype.country = "China";
Person.prototype.greet = function() {
   alert("Hello, I am " + this.firstName + " " + this.lastName);
};

var p1 = new Person("Hub", "John", 30);
var p2 = new Person("Mock", "William", 23);
console.log(p1.fistName == p2.firstName);   // false
console.log(p1.country == p2.country);   // true
console.log(p1.greet == p2.greet);   // true

可是若是你继续测下面的代码,你得不到你可能预期的p2.country也变为UK:

p1.country = "UK";
console.log(p2.country);   // China

这与做用域链有关,后面我会详细阐述。继续回到这里。既然类得以经过函数模拟,那么咱们如何模拟类的继承呢?

好比咱们如今须要一个司机类,让它继承Person,咱们能够这样:

var Driver = function(firstName, lastName, age) {
   this.firstName = firstName;
   this.lastName = lastName;
   this.age = age;
};

Driver.prototype = new Person();   // 1
Driver.prototype.drive = function() {
   alert("I'm driving. ");
};
var myDriver = new Driver("Butter", "John", 28);
myDriver.greet();
myDriver.drive();

代码行1是实现继承的关键,这以后Driver又定义了它扩展的只属于它本身的函数drive,这样它既能够调用从Person继承的greet函数,又能够调用本身的drive函数了。

No3. 遵循一些实践指导原则

下面是一些指导编写高可维护性JavaScript代码的实践原则的不完整总结。

尽可能避免全局变量

JavaScript使用函数来管理做用域。每一个全局变量都会成为Global对象的属性。你也许不熟悉Global对象,那咱们先来讲说Global对象。ECMAScript中的Global对象在某种意义上是做为一个终极的“兜底儿”对象来定义的:即全部不属于任何其余对象的属性和方法最终都是它的属性和方法。全部在全局做用域中定义的变量和函数都是Global对象的属性。像escape()、encodeURIComponent()、undefined都是Global对象的方法或属性。

事实上有一个咱们更熟悉的对象指向Global对象,那就是window对象。下面的代码演示了定义全局对象和访问全局对象:

myglobal = "hello"; // antipattern
console.log(myglobal); // "hello"
console.log(window.myglobal); // "hello"
console.log(window["myglobal"]); // "hello"
console.log(this.myglobal); // "hello"

使用全局变量的缺点是:

  1. 全局变量被应用中全部代码共享,因此很容易致使不一样页面出现命名冲突(尤为是包含第三方代码时)
  2. 全局变量可能与宿主环境的变量冲突

    function sum(x, y) { // antipattern: implied global result = x + y; return result; }

result如今就是一个全局变量。要改正也很简单,以下:

function sum(x, y) {
   var result = x + y;
   return result;
}

另外经过var声明建立的全局变量与未经过var声明隐式建立的全局变量有下面的不一样之处:

  • 经过var声明建立的全局变量没法被delete
  • 而隐式建立的全局变量能够被delete

delete操做符运算后返回true或false,标识是否删除成功,以下:

// define three globals
var global_var = 1;
global_novar = 2; // antipattern
(function () {
   global_fromfunc = 3; // antipattern
}());

// attempt to delete
delete global_var; // false
delete global_novar; // true
delete global_fromfunc; // true
// test the deletion
typeof global_var; // "number"
typeof global_novar; // "undefined"
typeof global_fromfunc; // "undefined"

推荐使用Single Var Pattern来避免全局变量以下:

function func() {
   var a = 1,
       b = 2,
       sum = a + b,
       myobject = {},
       i,
       j;
   // function body...
}

上面只用了一个var关键词就让a、b、sum等变量所有成为局部变量了。而且为每一个变量都设定了初始值,这能够避免未来可能出现的逻辑错误,并提升可读性(设定初始值意味着能很快看出变量保存的究竟是一个数值仍是字符串或者是一个对象)。

局部变量相对于全局变量的另外一个优点在于性能,在函数内部从函数本地做用域查找一个变量毫无疑问比去查找一个全局变量快。

避免变量声明提高(hoisting)陷阱

你极可能已经看到过不少次下面这段代码,这段代码常常用来考察变量提高的概念:

myName = "global";
function func() {
   console.log(myName);   // undefined
   var myName = "local";
   console.log(myName);   // local
}
func();

这段代码输出什么呢?JavaScript的变量提高会让这段代码的效果等价于下面的代码:

myName = "global";
function func() {
   var myName;
   console.log(myName);   // undefined
   myName = "local";
   console.log(myName);   // local
}
func();

因此输出为undefined、local就不难理解了。变量提高不是ECMAScript标准,可是却被广泛采用。

对非数组对象用for-in,而对数组对象用普通for循环

虽然技术上for-in能够对任何对象的属性进行遍历,可是不推荐对数组对象用for-in,理由以下:

  • 若是数组对象包含扩展函数,可能致使逻辑错误
  • for-in不能保证输出顺序
  • for-in遍历数组对象性能较差,由于会沿着原型链一直向上查找所指向的原型对象上的属性

因此推荐对数组对象用普通的for循环,而对非数组对象用for-in。可是对非数组对象使用for-in时经常须要利用hasOwnProperty()来滤除从原型链继承的属性(而通常不是你想要列出来的),好比下面这个例子:

// the object
var man = {
   hands: 2,
   legs: 2,
   heads: 1
};
// somewhere else in the code
// a method was added to all objects
if (typeof Object.prototype.clone === "undefined") {
   Object.prototype.clone = function () {};
}
for(var i  in man) {
   console.log(i, ": ", man[i]);
}

输出以下:

hands :  2
legs :  2
heads :  1
clone :  function () {}

即多了clone,这个多是另一个开发者在Object的原型对象上定义的函数,却影响到了咱们如今的代码,因此规范的作法有两点。第一坚定不容许在原生对象的原型对象上扩展函数或者属性 。 第二将代码改写为相似下面这种:

for(var i  in man) {
   if(man.hasOwnProperty(i)) {
      console.log(i, ": ", man[i]);
   }
}

进一步咱们能够改写代码以下:

for (var i in man) {
   if (Object.prototype.hasOwnProperty.call(man, i)) { // filter
      console.log(i, ":", man[i]);
   }
}

这样有啥好处呢?第一点防止man对象重写了hasOwnProperty函数的状况;第二点性能上提高了,主要是原型链查找更快了。

进一步缓存Object.prototype.hasOwnProperty函数,代码变成下面这样:

var i, hasOwn = Object.prototype.hasOwnProperty;
for (i in man) {
    if (hasOwn.call(man, i)) { // filter
        console.log(i, ":", man[i]);
    }
}

避免隐式类型转换

隐式类型转换可能致使一些微妙的逻辑错误。咱们知道下面的代码返回的是true:

0 == false 0 == ""

建议作法是始终使用恒等于和恒不等于,即===!==

而对于下面的代码:

null == false
undefined == false

咱们经常指望它返回true的,但却返回的是false。

那么咱们能够用下面的代码来将其强制转换为布尔类型后比较:

!!null === false
!!undefined === false

避免eval()

eval()接受任意字符串并将其做为JavaScript代码进行执行,最初经常使用于执行动态生成的代码,可是eval()是有害的,好比可能致使XSS漏洞,若是根据某个可变属性名访问属性值,能够用[]取代eval(),以下:

// antipattern
var property = "name";
alert(eval("obj." + property));
// preferred
var property = "name";
alert(obj[property]);

注意传递字符串给setTimeout()、setInterval()和Function()也相似eval(),也应该避免。好比下面:

// antipatterns
setTimeout("myFunc()", 1000);
setTimeout("myFunc(1, 2, 3)", 1000);
// preferred
setTimeout(myFunc, 1000);
setTimeout(function () {
   myFunc(1, 2, 3);
}, 1000);

若是你遇到非要使用eval()不可的场景,用new Function()替代,由于eval()的字符串参数中即便经过var声明变量,它也会成为一个全局变量,而new Function()则不会,以下:

eval("var myName='jxq'");

则myName成了全局变量,而用newFunction()以下:

var a = new Function("firstName, lastName", "var myName = firstName+lastName");

实际上a如今是一个匿名函数:

function anonymous(firstName, lastName) {
    var myName = firstName+lastName
}

则myName如今就不是全局变量了。固然若是还坚持用eval(),能够用一个当即执行函数表达式将eval()包起来:

(function() {
   eval("var myName='jxq';");
}) ();   // jxq
console.log(typeof myName);   // undefined

另一个eval()和Function()的区别是前者会影响做用域链,然后者不会,以下:

(function() {
   var local = 1;
   eval("console.log(typeof local);");
})();   // number

(function() {
   var local = 1;
   Function("console.log(typeof local);");
})();   // undefined

使用parseInt()时,指定第二个进制参数

这个不用多提,相信你们也都知道了

使用脚本引擎,让JavaScript解析数据生成HTML

传说中的12306在查询车票时返回的是下面这么一大串(我已无力吐槽,这个是我今天刚截的,实际大概100来行):

<span id='id_240000G13502' class='base_txtdiv' onmouseover=javascript:onStopHover('240000G13502#VNP#AOH') onmouseout='onStopOut()'>G135</span>,<img src='/otsweb/images/tips/first.gif'>&nbsp;&nbsp;&nbsp;&nbsp;北京南&nbsp;&nbsp;&nbsp;&nbsp;
<br>
&nbsp;&nbsp;&nbsp;&nbsp;12:40,<img src='/otsweb/images/tips/last.gif'>&nbsp;&nbsp;上海虹桥&nbsp;&nbsp;
<br>
&nbsp;&nbsp;&nbsp;&nbsp;18:04,05:24,8,--,<font color='#008800'></font>,<font color='#008800'></font>,--,--,--,--,--,--,--,<a name='btn130_2' class='btn130_2' style='text-decoration:none;' onclick=javascript:getSelected('G135#05:24#12:40#240000G13502#VNP#AOH#18:04#北京南#上海虹桥#01#08#O*****0072M*****00629*****0008#8A72A4AD8B70A5E0FF02AC9290DDB39C6E0B6D5A0F8A9A8FB305FB11#P2')>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</a>\n1,<span id='id_240000G13705' class='base_txtdiv' onmouseover=javascript:onStopHover('240000G13705#VNP#AOH') onmouseout='onStopOut()'>G137</span>,<img src='/otsweb/images/tips/first.gif'>&nbsp;&nbsp;&nbsp;&nbsp;北京南&nbsp;&nbsp;&nbsp;&nbsp;
<br>
&nbsp;&nbsp;&nbsp;&nbsp;12:45,<img src='/otsweb/images/tips/last.gif'>&nbsp;&nbsp;上海虹桥&nbsp;&nbsp;

为何不能只返回数据(好比用JSON),而后利用JavaScript模板引擎解析数据呢?好比下面这样(使用了jQuery tmpl模板引擎,详细参考个人代码 JavaScript模板引擎使用):

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>JavaScript tmpl Use Demo</title>
        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
        <script src="./tmpl.js" type="text/javascript"></script>
                <!-- 下面是模板user_tmpl -->
        <script type="text/html" id="user_tmpl">
            <% for ( var i = 0; i < users.length; i++ ) { %>
            <li><a href="<%=users[i].url%>"><%=users[i].name%></a></li>
            <% } %>
        </script>
        <script type="text/javascript">
                        // 用来填充模板的数据
            var users = [
                { url: "http://baidu.com", name: "jxq"},
                { url: "http://google.com", name: "william"},
            ];
            $(function() {
                               // 调用模板引擎函数将数据填充到模板得到最终内容
                $("#myUl").html(tmpl("user_tmpl", users));
            });
        </script>
    </head>
    <body>
        <div>
            <ul id="myUl">
            </ul>
        </div>
    </body>
</html>

使用模板引擎能够将数据和HTML内容彻底分离,这样有几个好处:

  • 修改HTML结构时几乎能够不修改返回的数据的结构
  • 只返回纯粹的数据,节省了网络带宽(网络带宽就是钱)

采用一致的命名规范

  1. 构造函数首字母大写。
  2. 而非构造函数的首字母小写,标识它们不该该经过new操做符被调用。
  3. 常量名称应该全大写。
  4. 私有变量或似有函数名称前带上下划线,以下:

    var person = {
        getName: function () {
            return this._getFirst() + ' ' + this._getLast();
        },
        _getFirst: function () {
            // ...
        },
        _getLast: function () {
            // ...
        }
    };

不吝啬注释,但也不要胡乱注释

  1. 为一些相对艰涩些的代码(好比算法实现)添加注释。
  2. 为函数的功能、参数和返回值添加注释。
  3. 不要对一些常识性的代码进行注释,也不要像下面这样画蛇添足地注释:

    var myName = "jxq";   // 声明字符串变量myName,其值为"jxq"

No4. 合理高效地使用工具

这里的工具包括JavaScript框架、JavaScript类库以及一些平时本身积累的Code Snippet。

使用JavaScript框架的好处是框架为咱们提供了一种合理的组织代码方式,好比Backbone.js、Knockout.js这种框架能让咱们更好地将代码按MVC或者MVP模式分离。

而使用JavaScript类库能够避免重复造轮子(并且每每造出一些不那么好的轮子),也可让咱们更专一于总体业务流程而不是某个函数的具体实现。一些通用的功能如日期处理、金额数值处理最好用现有的成熟类库。

最后使用本身平时积累的Code Snippet能够提升咱们的编码效率,而且最重要的是能够提供多种参考解决方案。

相关文章
相关标签/搜索