如何編寫可維護的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能夠提升咱們的編碼效率,而且最重要的是能夠提供多種參考解決方案。

相關文章
相關標籤/搜索