如何寫出高品質的JavaScript代碼

來源:互聯網
上載者:User

優秀的Stoyan Stefanov在他的新書中(《Javascript Patterns》)介紹了很多編寫高品質代碼的技巧,比如避免使用全域變數,使用單一的var關鍵字,迴圈式預存長度等等。

這篇文章不僅僅從代碼本身來考慮如何最佳化編碼,也從代碼的設計階段來考慮,包括書寫API文檔,同事的review,使用JSLint。這些習慣都能協助你編寫更加高品質的、更易於理解的、可維護的代碼(讓你的代碼在多年之後仍使你引以為傲)。

編寫可維護的代碼

軟體的BUG修複需要花費大量的精力。尤其當代碼已經發布之後,隨著時間的增長,維護的成本愈發的高。當你一發現BUG的時候,就立即去修複,這時候你的代碼還是熱乎的,你也不需要回憶,因為就是剛剛寫好的。但是當你做了其他任務,幾乎完全忘記了這份代碼,這時候就需要:

  • 重新學習和理解問題
  • 理解代碼是如何解決問題的

另外一個問題是,在大項目或者大公司裡面,經常是解決BUG的人不是產生BUG的人,而且也不是發現BUG的人。所以減少理解代碼的時間就是最重要的問題,無論這個代碼是你自己以前寫的還是團隊中的其他成員寫的,因為我們都想去搞搞新的有意思的東西,而不是去維護那些個陳舊的代碼。

還有一個開發中的普遍問題就是,往往讀代碼的時間比寫代碼的時間還要多。有時候你鑽研一個問題,可以花整整一個下午的時間來考慮代碼的編寫。這個代碼當時是可以工作的,但是隨著開發的進行,其他東西發生了很大的變化,這時候也就需要你自己來重新審查修改編寫代碼。比如:

  • 還有BUG沒有解決
  • 添加了新的功能
  • 程式需要在新的環境中運行(比如一個新上市的瀏覽器)
  • 代碼有問題
  • 代碼需要重寫因為修改了架構甚至要使用另一個語言

因為這些原因,也許你當時一個下午寫好的代碼,後面需要花費幾周的時間來閱讀。所以編寫可維護的代碼對於軟體的成功至關重要。可維護的程式碼封裝括:

  • 可讀性
  • 連續性
  • 預見性
  • 看起來是一個人寫的
  • 有文檔
最少化全域變數

Javascript使用函數來約定範圍。一個在函數內部聲明的變數在外部是不可見的。所以,全域變數也就是聲明在任何函數之外的或者沒有被聲明的變數。

Javascript中,在任何函數之外有個可訪問的全域對象,每一個你建立的全域變數都是這個對象的一個屬性。在瀏覽器中,為了方便,通常用window來指代這個全域變數。下面的代碼就是說明如何建立一個全域變數:

myglobal = "hello"; // antipatternconsole.log(myglobal); // "hello"console.log(window.myglobal); // "hello"console.log(window["myglobal"]); // "hello"console.log(this.myglobal); // "hello
全域變數的問題

全域變數的問題在於,他在你的所有代碼或者一個頁面中都共用。他們在同一個命名空間下面,這通常會造成變數名衝突–兩個同名的變數,但是確實不同的用處。

通常在一些頁面中需要引入一些其他人的代碼,比如:

  • 第三方的JS庫
  • 廣告夥伴的指令碼
  • 第三方的使用者行為分析或者統計指令碼
  • 不同的組件、按鈕等等

加入其中一個第三方組件定義了一個全域變數:result。然後在你的程式中,也定義了一個全域變數result。最後的這個result會覆蓋點之前的result,這樣第三方的指令碼就會停止工作。

所以,為了對其他的指令碼友好,在一個頁面中使用越少的全域變數越好。在後面會有一些方法來告訴你如何減少全域變數,比如使用命名空間,或者自執行的匿名函數,但是最好的避免全域變數的方法就是使用var關鍵字來聲明變數。

因為javascript的兩個特性,建立一個全域變數非常的簡單。第一,你可以使用一個甚至沒有聲明的變數,第二,在javascript中,所有未聲明的變數都會成為全域對象的一個屬性(就像一個聲明了的全域變數一樣)。看看這個例子:

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

在這個代碼中,result在沒有被聲明的情況下就被使用了,這個代碼也能很好的工作,但是在調用了這個函數之後,就會多一個名為result的全域變數,這是所有問題的根源了。

解決這個問題的辦法就是使用var:

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

兩外一個不好的習慣就是在聲明變數的時候使用鏈式的方法來賦值,這時候,a是局部變數,但是b就成為了全域變數。

function foo(){     var a=b=0;     ....}

這是因為,b = 0這個運算式先執行,執行的時候b並沒有被聲明,所以b就成為了全域變數,然後返回這個運算式的值0,給聲明了的變數a,換句話說,就好像你輸入的是:var a = (b=0);

如果你已經聲明變數,那麼這種鏈式的賦值沒有問題:

function foo(){     var a,b;     ...}

另外一個避免使用全域變數的原因是考慮到程式的可移植性。如果你想讓你的代碼在不同的環境中都可以工作,那麼使用全域變數就很可能會與新的系統中的全域變數衝突(或許在之前的系統中沒有問題)。

忘記var的影響

使用var聲明的全域變數和沒有使用var產生的全域變數還有一個區別在於刪除:

使用var聲明建立的全域變數不能被刪除。沒有使用var聲明的全域變數可以被刪除。這說明沒有使用var聲明產生的全域變數不是真正的變數,他們只是全域對象的屬性。屬性可以通過delete刪除,但是變數不行:

// define three globalsvar global_var = 1;global_novar = 2; // antipattern(function () {   global_fromfunc = 3; // antipattern}());  // attempt to deletedelete global_var; // falsedelete global_novar; // truedelete global_fromfunc; // true  // test the deletiontypeof global_var; // "number"typeof global_novar; // "undefined"typeof global_fromfunc; // "undefined"

在ES5的strict 模式下,給一個為聲明的變數賦值會報錯。

讀取全域對象

在瀏覽器中,你可以通過window變數來讀取全域對象(除非你在函數內部重新定義了window對象)。但在有的環境中,可能不叫window,那麼你可以使用下面的代碼來擷取全域對象:

var global = (function(){     return this;})();

這樣可以擷取到全域對象的原因是在function的內部,this指向全域對象。但是這在ES5的strict 模式下會不起作用,你需要適配一些其他模式。當你開發自己的庫的時候,你可以把你的代碼封裝在一個立即函數中,然後將this作為一個參數傳進來。

單個var模式

在你的代碼的頂部只是用一個var關鍵字,會有以下的好處:

  • 對於所有需要的變數,在一個地方就可以全部看到
  • 避免使用一個未定義的變數
  • 協助你記憶聲明的變數,減少全域變數
  • 更精簡的代碼

書寫很簡單:

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

通過一個var和逗號來聲明多個變數。在聲明的時候給變數賦預設值也是不錯的做法,可以避免一些邏輯錯誤,提高代碼的可讀性。而後你閱讀的代碼的時候也可以根據變數的預設值來方便的猜測變數的用途。

你也可以在聲明變數的時候做一些實際的工作,比如sum = a + b;另外,在操作DOM元素的時候,你也可以把DOM元素的引用儲存在一個變數中:

function updateElement() {   var el = document.getElementById("result"),       style = el.style;   // do something with el and style...}
濫用了的var

JavaScript允許你在函數內部有多個var語句,但是卻都表現的如同在函數的頂部聲明一樣。這個特性在你使用一個變數然後在後面又聲明了這個變數時會導致一些奇怪的邏輯問題。對於JavaScript來說,只要變數在同一個範圍,那麼就認為是聲明了的,就算是在var語句之前使用也一樣。看看這個例子:

myname = "global"; // global variablefunction func() {    alert(myname); // "undefined"    var myname = "local";    alert(myname); // "local"}func();

在這個例子中,或許你期望第一次會彈出global,第二次彈出local。因為第一次的時候沒有還沒有使用var聲明myname,這是應該是全域變數的myname,第二次聲明了,然後alert之後應該是local的值。而事實上不是這樣的,只要你在函數中出現了var myname,那麼js就認為你在這個函數中聲明了這個變數,但是在讀取這個變數的值的時候,因為var語句還沒有執行,所以是undefined,很奇怪的邏輯吧。上面的代碼相當於:

myname = "global"; // global variablefunction func() {   var myname; // same as -> var myname = undefined;   alert(myname); // "undefined"   myname = "local";   alert(myname); // "local"}func();

我們來解釋一下這個現象,在代碼的解析中,分兩個步驟,第一步先處理變數函數的聲明,這一步處理整個代碼的上下文。第二步就是代碼的運行時,建立函數運算式以及未定義的變數。實際上,我們只是假設了這個概念,這並不在ECMAScript的規範中,但是這個行為常常就是這樣解釋的。

for迴圈

在for迴圈中你會去迭代一些數組元素或者一些HTML元素。for迴圈常常如此:

for (var i = 0; i < myarray.length; i++) {   // do something with myarray[i]}

這樣寫的問題在於,每一次迭代的時候都會計算數組的長度,尤其在這個參數不是一個數組而是一組HTML元素的時候會降低你的程式的效能。HTML元素的集合在頁面上,這樣每次都會去再頁面上尋找相應的元素,這是非常耗時的。所以對於for迴圈,你需要預先儲存數組的長度,這樣寫:

for (var i = 0, max = myarray.length; i < max; i++) {   // do something with myarray[i]}

這樣緩衝了參數的長度,在每次迭代的時候就不用再去尋找計算了。在尋找HTML元素集合的時候,緩衝參數長度可以帶來可觀的效能提升,Safari下面提高兩倍的速度,在IE7下面提高190倍的速度。需要注意的是,當你需要操作修改DOM元素的數量的時候,你肯定希望這個值是隨時更新的而不是一個常量。

使用下面的單一var模式,你也可以把var提到迴圈之外:

function looper() {   var i = 0,        max,        myarray = [];   // ...   for (i = 0, max = myarray.length; i < max; i++) {      // do something with myarray[i]   }}

這個模式可以增強整個代碼的連續性,但是不好的一點是當你重構代碼的時候覆制粘貼就沒那麼容易了。例如:如果你想在其他函數中也使用這個迴圈,那你需要確定在新的函數中處理好了i和max(或許還需要刪掉這個)。

這個函數還有兩個點可以最佳化的:可以少一個變數(不需要max),遞減到0,一個數字與0比較比這個數字與另外一個數字比較更快。

所以就可以寫為:

var i, myarray = [];for (i = myarray.length; i--;) {   // do something with myarray[i]}

針對第二點:

var myarray = [],    i = myarray.length;while (i--) {   // do something with myarray[i]}

這是兩個比較微小的點的最佳化。另外,JSLint可能對於i–會有意見。

for-in迴圈

for-in迴圈用來迭代非數組的對象。使用for-in迴圈通常也成為枚舉。

從技術上來說,你也可以用for-in來迴圈數組,因為數組也是對象,但是不推薦。如果數組有一些自訂的擴充函數,那麼就會出錯。另外,對象屬性的順序在for-in迴圈中也是不確定的。所以最好還是用普通的迴圈來迴圈數組用for-in來迴圈對象。

在迴圈對象的過程中,使用hasOwnProperty()方法來檢驗是對象本身的屬性還是原型鏈上的屬性很重要。

看看下面的這個例子。

// the objectvar man = {   hands: 2,   legs: 2,   heads: 1};  // somewhere else in the code// a method was added to all objectsif (typeof Object.prototype.clone === "undefined") {   Object.prototype.clone = function () {};}

在這個例子中,我們有一個簡單的稱作man的對象字面量。在其他man定義之前或之後的地方,對象原型有一個很有用的clone()方法。因為原型鏈的原因,所有的對象都自動獲得了這個方法。為了在枚舉man對象的時候出現clone方法,你需要使用hasOwnProperty方法來區別。如果沒有區別來自原型鏈的方法,那麼就會有一些意想不到的事情發生:

// 1.// for-in loopfor (var i in man) {   if (man.hasOwnProperty(i)) { // filter      console.log(i, ":", man[i]);   }}/* result in the consolehands : 2legs : 2heads : 1*/// 2.// antipattern:// for-in loop without checking hasOwnProperty()for (var i in man) {   console.log(i, ":", man[i]);}/*result in the consolehands : 2legs : 2heads : 1clone: function()*/

另外一種使用方法如下:

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

這樣寫的好處是可以防止man重新定義了hasOwnProperty方法導致的衝突。如果不想寫這麼長的一串,你也可以這樣:

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

嚴格意義上講,不適用hasOwnProperty也不是什麼錯誤。根據任務的難度和你對代碼的自信程度,你也可以不用這個直接迴圈。但是當你不確定的時候,最好還是使用這個方法檢測一下。

另外一種格式上的改變(不會通過jsLint的檢查),去掉for的大括弧,然後把if放在同一行。這樣做的好處可以讓迴圈體更加突出,縮排也就少一些:

// Warning: doesn't pass JSLintvar i, hasOwn = Object.prototype.hasOwnProperty;for (i in man) if (hasOwn.call(man, i)) { // filter    console.log(i, ":", man[i]);}
不要擴充內建的原型

擴充原型的建構函式,可以提供一些很強大的功能,但是有時候他太強大了。

有時候你會去擴充Object(),Array(),Fucntion()的原型方法,這樣會導致可維護性的問題,因為這會讓你的代碼的移植性變差。其他的開發人員使用你的代碼的時候,可能只需要原生的方法,並不需要額外的功能。

另外,你添加進去的方法,如果在迴圈的時候沒有使用hasOwnProperty方法就會被遍曆出來,這會讓人很迷惑。

所以,最好還是不要擴充基本的對象。除非是下面的情況:

  • 你確定在將來根據ECMAScript規範,瀏覽器會添加相應的原型方法,那麼是可以的,你只不過是提前實現了這個功能。
  • 你確定的你要實現的方法不存在–或許有時候在代碼的其他的地方實現了,或者有的瀏覽器支援,這都是不行的。
  • 有非常清晰的文檔,並且與團隊成員溝通過

如果在這些情況之下,那麼你就可以添加,最好是下面這種形式:

if (typeof Object.prototype.myMethod !== "function") {   Object.prototype.myMethod = function () {      // implementation...   };}

待續。。。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.