這篇文章主要為大家展示了“怎么編寫可維護的JavaScript代碼”,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領大家一起研究并學習一下“怎么編寫可維護的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 名詞背后東西的基礎。我知道這樣會讓我這篇文章很難寫下去,因為那將包含太多主題,所以本文只打算管中窺豹:本文將先講一些概念,然后講一些實踐指導原則,***涉及一些工具的討論。
在正式開始這篇博客之前,我們需要問自己為什么代碼可維護性值得我們關注。相信只要你寫過相當量的代碼后,都已經發現了這點:Fix Bug比寫代碼困難得多?;ㄈ齻€小時寫的代碼,而之后為了Fix其中的一個Bug花兩三天時間,這種情況并不少見。再加上Fix Bug的人很可能不是代碼原作者,這無疑更雪上加霜。所以代碼可維護性是一個非常值得探討的話題,提高代碼可維護性就一定程度上能節省Fix Bug的時間,節省Fix Bug的時間進而就節省了人力成本。
No 1. 將代碼組織成模塊
基本任何一門編程語言都認為模塊化能提升代碼可維護性。我們知道軟件工程的核心在于控制復雜度,而模塊化本質上是分離關注點,從而分解復雜度。
IIFE模塊模式
當我們最開始學習編寫JavaSript代碼時,基本都會寫下面這樣的代碼:
var myVar = 10; var myFunc = function() { // ... };
這樣的代碼本身沒有什么問題,但是當這樣的代碼越來越多時,會給代碼維護帶來沉重的負擔。原因是這樣導致myVar和myFunc暴露給全局命名空間,從而污染了全局命名空間。以我個人經驗來看,一般當某個頁面中的JavaScript代碼達到200行左右時就開始要考慮這個問題了,尤其是在企業項目中。那么我們該怎么辦呢?
最簡單的解決方法是采用IIFE(Immediate Invoked Function Expression,立即執行函數表達式)來解決(注意這里是函數表達式,而不是函數聲明,函數聲明類似 var myFunc = function() { // ... }),如下:
(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導出要暴露給外部的函數。
比如我們看一個實際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 ) { var jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' return new jQuery.fn.init( selector, context, rootjQuery ); }, // 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);
myPrivateVar1、 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
上面的例子傳入的MODULE可能是undefined,也就是說它之前可以不存在。與之對應Tight Augmentation模式要求傳入的MODULE一定存在并且已經被加載進來。
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函數,并且可以在重寫的函數中調用od_moduleMethod。但這種寫法不夠靈活,因為它假定了一個先決條件:MODULE模塊一定存在并且被加載進來了,且它包含moduleMethod函數。
子模塊模式
這個模式非常簡單,比如我們為現有模塊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"
使用全局變量的缺點是:
全局變量被應用中所有代碼共享,所以很容易導致不同頁面出現命名沖突(尤其是包含第三方代碼時)
全局變量可能與宿主環境的變量沖突
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(man.hasOwnProperty(i)) { 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'> 北京南 <br> 12:40,<img src='/otsweb/images/tips/last.gif'> 上海虹橋 <br> 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#8A72A4AD8B70A5E0FF02AC9290DDB39C6E0B6D***0F8A9A8FB305FB11#P2')>預 訂</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'> 北京南 <br> 12:45,<img src='/otsweb/images/tips/last.gif'> 上海虹橋
為什么不能只返回數據(比如用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結構時幾乎可以不修改返回的數據的結構
只返回純粹的數據,節省了網絡帶寬(網絡帶寬就是錢)
采用一致的命名規范
構造函數首字母大寫。
而非構造函數的首字母小寫,標識它們不應該通過new操作符被調用。
常量名稱應該全大寫。
私有變量或似有函數名稱前帶上下劃線,如下:
var person = { getName: function () { return this._getFirst() + ' ' + this._getLast(); }, _getFirst: function () { // ... }, _getLast: function () { // ... } };
不吝嗇注釋,但也不要胡亂注釋
為一些相對艱澀些的代碼(比如算法實現)添加注釋。
為函數的功能、參數和返回值添加注釋。
不要對一些常識性的代碼進行注釋,也不要像下面這樣多此一舉地注釋:
var myName = "jxq"; // 聲明字符串變量myName,其值為"jxq"
No4. 合理高效地使用工具
這里的工具包括JavaScript框架、JavaScript類庫以及一些平時自己積累的Code Snippet。
使用JavaScript框架的好處是框架為我們提供了一種合理的組織代碼方式,比如Backbone.js、Knockout.js這種框架能讓我們更好地將代碼按MVC或者MVP模式分離。
而使用JavaScript類庫可以避免重復造輪子(而且往往造出一些不那么好的輪子),也可以讓我們更專注于整體業務流程而不是某個函數的具體實現。一些通用的功能如日期處理、金額數值處理***用現有的成熟類庫。
***使用自己平時積累的Code Snippet可以提高我們的編碼效率,并且最重要的是可以提供多種參考解決方案。
下面是一些流行的工具列表。
jQueryCoreUISelect
它提供了完全自定制功能,支持選項組、回調函數等等。另外一個擴展Select控件的插件是jQuery Chosen,可以參考我分享的代碼:美化Select下拉框
Sisyphus.js
它提供了表單離線存儲功能,能夠自動保存用戶未提交的表單數據。而當提交表單后會清除數據。
TextExt
這個類庫允許我們將HTML文本轉換成輸入域。
Validate.js
這是一個輕量級表單驗證類庫,它預定義了一系列驗證規則(通過正則表達式),并且支持定制驗證回調函數和驗證失敗消息,兼容所有主流瀏覽器(包括IE 6),更詳細的信息有興趣的話可以參考我的博客 Validate.js框架源碼完全解讀
jQuery File Upload
jQuery文件上傳插件,支持多文件上傳
Handsontables: Excel-Like Tables For The Web
提供Web Excel功能的jQuery插件
Pivot.js
通過Pivot我們可以很方便地展現大量數據。數據源可以是CSV或者JSON
Date.js
一個很方便的日期處理類庫。
使用很簡單,下面是兩個小實例:
// What date is next thursday? Date.today().next().thursday(); // Add 3 days to Today Date.today().add(3).days(); ...
RequireJS
RequireJS是一個JavaScript文件和模塊加載器。使用RequireJS可以顯著提高代碼的運行效率。據說百度音樂盒利用RequireJS后加載速度提高了好幾秒(按需加載),號稱神器。
Grunt.js
Grunt.js是一個基于任務的命令行工具,可以用于JavaScript項目構建。它預包含幾十個內置的任務:文件合并、項目腳手架(基于一個預定義的模板)、JSLint驗證、UglifyJS代碼壓縮、qUnit單元測試、啟動服務器等。
以上是“怎么編寫可維護的JavaScript代碼”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。