JavaScript 設計原則-1

by vivid 22. 十月 2014 02:39

.NET Magazine國際中文電子雜誌
者:許薰尹
稿:張智凱
文章編號:N141015302
出刊日期:2014/10/22

近來news.dice.com網站的《5 Programming Languages You’ll Need Next Year (and Beyond)》文章提出,未來流行的五大語言是JavaScript, HTML5, and CSS3、C#、Java、PHP、Swift。顯然網站程式開發永遠不會退流行。由於JavaScript是一個不嚴格的程式語言,若撰寫程式時,沒有遵循一些設計原則來開發,後續將不易維護與除錯。本篇文章將簡介一些撰寫JavaScript重要的設計原則。

 

變數宣告

JavaScript區域變數的可視範圍是在變數定義所在的函式(function)之中,因此若你在函式之外使用var宣告變數,或是直接使用變數,這些變數都會是全域變數(global variable)。在瀏覽器使用JavaScript時,當你宣告全域變數,此變數就會成為全域的window物件之屬性,例如以下程式碼未定義便使用到aVar變數,它就會成為全域變數,因此在程式中可以使用多種語法來存取它:

<script>
  aVar = 100;
  console.log( aVar ); //100
  console.log( window.aVar ); //100
  console.log( this.aVar ); //100
  console.log( window["aVar"] ); //100
</script>

 

因在程式中未經宣告就可以使用變數,因此我們可能會不小心使用到全域變數,例如以下程式碼範例,在sayHello函式之中,使用到msg,如此它將變成是全域變數,最後一行程式碼還是可以存取到此變數值:

<script>
  function sayHello( name ) {
    msg = 'Hello, ' + name;
    console.log( msg ); //Hello, mary
  }

  sayHello( 'mary' );
  console.log( msg ); //Hello, mary
</script>

 

在網頁寫JavaScript程式碼,難免會引用到一些第三協力廠商的JavaScript開發套件,若這些開發套件恰巧使用到一個全域變數名稱和你的全域變數相同,那麼你的變數值可能會被這些套件的程式碼影響,這就是所謂的命名衝突的問題。因此較好的做法是儘量少用全域變數,且使用變數之前一定要使用var做宣告,例如上例程式碼應該修改如下,這樣最後一行程式碼就不能夠存取到msg變數值:

 

<script>
  function sayHello( name ) {
    var msg = 'Hello, ' + name;
    console.log( msg ); //Hello, mary
  }

  sayHello( 'mary' );
  console.log( msg ); //msg is not defined
</script>


宣告多個變數

當你在函式之中宣告變數時,若變數有多個,建議以一行var來做宣告,並將這行宣告放在函式中的第一行,參考以下範例程式碼,為較差的寫法:

function myfunction() {
   var firstName = 'Mary';
   var lastName = 'Wang';
   var age = 30;
   var married = false;
}

 

較佳的寫法,一次宣告四個區域變數並賦予值:

function myfunction() {
  var firstName = 'Mary',
    lastName = 'Wang',
    age = 30,
    married = false;
}

 

使用var宣告變數,建議每一個變數宣告在新的一行;而未初始化的變數最後再宣告,這樣在指派變數值時將會非常有用,例如變數值相依於定義在之前的變數值。以下範例程式中fullName的變數值是由firstName與lastName串接而來的。

 

function myfunction() {
  var firstName = 'Mary',
    lastName = 'Wang',
    fullName = firstName + ' ' + lastName,
    age,
    married;
}

 

程式換行

JavaScript引擎有一個特色,當你的程式碼未以分號做結尾時,會自動地在程式最後附加「;」號,這樣的自動化服務有時會造成一些程式的問題,例如若有程式碼如下,test()函式回傳一個字串,return和字串寫在同一行:

function test() {
  return "hello";
}

console.log(test()); //hello


 

這個程式碼沒有什麼大問題,可以正確執行,並印出「hello」字串到控制台。但若我們在return與"hello"之間換了一行,例如修改程式如下:

function test() {
  return
  "hello";
}

console.log(test()); //undefined


則test()函式印出的結果是undefined,這是因為Java Script會在return之後加上「;」號,類似以下程式碼,因此執行的結果會是undefined:

function test() {
  return;
  "hello";
}

console.log(test()); //undefined

 

Hoisting

在JavaScript函式(function)中,可以在任意位置使用var關鍵字宣告區域變數,JavaScript會將這些變數,提升到function中的第一行程式,就好像你是在函式中的第一行宣告變數一樣,這個行為稱做Hoisting。例如以下範例程式碼第三行,在Immediately Invoked Functions之中直接印出x變數值,在第四行程式碼才使用var做宣告:

var x = 100; //global 變數
( function () {
  console.log( x ); //undefined
  var x = 200; //區域變數
  console.log( x ); //200
}() );

console.log( x ); //100 global 變數


 

經過Hoisting的過程,JavaScript會將其解釋成類似以下的程式碼:

 

var x = 100; //global 變數
( function () {
  var x; //區域變數
  console.log( x ); //undefined
  x = 200;
  console.log( x ); //200
}() );

console.log( x ); //100 ,global 變數


 

因此在函式中的x變數將被視為區域變數。而Immediately Invoked Functions之外的變數x則是全域(global)變數。

 

儘量使用===(!==)運算子取代==(!=)

JavaScript在比較變數的值時,會自動轉換型別後再進行比較,如此會導致一些不可預測的行為。例如以下程式碼:

var x = 10,
  y = "10";

console.log( x == y ); //true
console.log( x === y ); //false

console.log( x != y ); //false
console.log( x !== y ); //true


 

使用==與!=做比較時,會做自動轉型之後再比較,因此x與y會視為相等;若使用===(!==)進行比較則不相等,這兩個運算子也會檢查兩邊的型別是否相同。

儘亮不要使用eval()函式

JavaScript中的eval()函式之主要用途是將傳入參數的字串,解析成JavaScript程式碼,並執行之。例如以下程式碼:

eval( "function test() { alert('test'); } test();" );

若在網頁主控台直接執行這段程式碼,網頁會馬上會跳出一個訊息:

clip_image002

圖 1

因此使用eval()方法可能會有安全性上的問題,讓惡意使用者更容易任意插入程式碼到你的程式之中執行。有時有些程式設計師會使用AJAX技術下載伺服端的JSON資料,再用eval()函式解析,例如Web伺服端若有一個data.json檔案資料如下:

 

{"d":[{"name":"mary","age":25}]}

Web伺服端提供一個GetJSON.aspx程式讀取JSON資料,並將之送到瀏覽器:

 

<%@ Page Language="C#" %>

<script runat="server">
    protected void Page_Load( object sender , EventArgs e ) {
        Response.ContentType = @"application/json";
        Response.WriteFile( Server.MapPath( "data.json" ) );
    }
</script>


 

用戶端使用以下程式碼,建立XMLHttpRequest物件取回資料,再利用eval()函式解析JSON格式資料:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title></title>
</head>
<body>
  <input id="btnGet" type="button" value="GetData" />
  <div id="div1">
  </div>
  <script>
    function contentLoaded() {
      var request = new XMLHttpRequest();
      var btnGet = document.getElementById("btnGet");
      btnGet.addEventListener("click", function () {
        request.addEventListener("readystatechange", function () {
          if (request.readyState === 4) {
            var doc = eval("(" + request.responseText + ")");
            var d = doc.d;
            for (var i = 0; i < d.length; i++) {
              document.getElementById("div1").innerHTML += d[i].name + "," + d[i].age + "<br/>";
            }
          }
        }, false);
        request.open("GET", "GetJSON.aspx", true);
        request.send();
      }, false);
    }
    window.addEventListener("DOMContentLoaded", contentLoaded, false);
  </script>
</body>
</html>

 

當這個網頁執行時,可以順利取得JSON格式資料,並顯示在畫面上。

clip_image004

圖 2

若有惡意使用者,篡改data.json檔案內容如下:

 

alert('hi')

則網頁執行時,會馬上跳出「hi」訊息,這樣會造成安全性上的問題:

clip_image006

圖 3

建議改用JOSN.parse方法來解析從伺服端取回的JSON格式資料。例如將上述程式碼修改為:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title></title>
</head>
<body>
  <input id="btnGet" type="button" value="GetData" />
  <div id="div1">
  </div>
  <script>
    function contentLoaded() {
      var request = new XMLHttpRequest();
      var btnGet = document.getElementById("btnGet");
      btnGet.addEventListener("click", function () {
        request.addEventListener("readystatechange", function () {
          if (request.readyState === 4) {
            var doc = request.responseText,
              o = JSON.parse(doc),
              d = o.d;         
            for (var i = 0; i < d.length; i++) {
              document.getElementById("div1").innerHTML += d[i].name + "," + d[i].age + "<br/>";
            }
          }

        }, false);
        request.open("GET", "GetJSON.aspx", true);
        request.send();
      }, false);
    }
    window.addEventListener("DOMContentLoaded", contentLoaded, false);
  </script>
</body>
</html>

 

 

for迴圈

在撰寫網頁時,我們經常會存取到DOM物件,而且經常會搭配迴圈的方式來操作,例如以下程式片段,使用document.getElementsByTagName取得網頁中所有的INPUT項目,並將它的背景顏色設定為橘色:

var inputs = document.getElementsByTagName("input");
for (var i = 0; i < inputs.length; i++) {
  inputs[i].style.backgroundColor = "orange";
}

 

使用document.getElementsByTagName取回的項目之型別為HTMLCollection,在for迴圈之中每次存取到inputs.length時,便會從DOM查詢一次,來取得目前inputs.length的值,這樣執行的效能會較差。較好的做法是將程式碼修改如下,先取得inputs.length並放到變數之中:

var inputs = document.getElementsByTagName("input");
for (var i = 0, len = inputs.length; i < len; i++) {
  inputs[i].style.backgroundColor = "orange";
}

這樣只會存取到inputs.length一次。

 

for..in迴圈

for..in迴圈支援列舉(enumeration)的功能,可以把物件的屬性一個一個取出來。建議不要拿for..in來存取陣列的項目,可能會有一些衍生的邏輯問題,最好只拿來存取物件的屬性。例如以下程式碼宣告一個employee物件,包含eID與empName屬性描述員工資訊,其型別是Object:

var employee = {};
employee.eId = 1;
employee.empName = "Mary";
//console.log( typeof employee ); //Object

if ( typeof Object.prototype.toArray === "undefined" ) {
  Object.prototype.toArray = function () { return [this] };
}

for ( var i in employee ) {
  console.log( employee[i] );

}


若定義之後,我們擴充了Object,為其新增一個toArray屬性,則使用for..in印出的結果為:

1
Mary
function () { return [this] }

這樣會將後續擴充的屬性都會列印出來,這個結果可能不是我們要的,因為一開始的employee只包含eId 與empName 兩個屬性。較好的做法是,使用hasOwnProperty來判斷,修改程式碼如下:

var employee = {};
employee.eId = 1;
employee.empName = "Mary";
console.log( typeof employee ); //Object

if ( typeof Object.prototype.toArray === "undefined" ) {
  Object.prototype.toArray = function () { return [this] };
}

for ( var i in employee ) {
  if ( employee.hasOwnProperty( i ) ) {
    console.log( employee[i] );
  }

}


 

這個範例的執行的結果如下:

1
Mary

 

宣告自訂物件

在JavaScript自訂物件時,可以使用內建物件的建構函式來建立,再用「.」號為物件擴充屬性或方法。例如以下程式碼,定義一個物件。包含id與name屬性,以及一個getEmployee()函式:

var employee = new Object();
employee.id = 1;
employee.name = 'mary';

employee.getEmployee = function () {

  return employee.id + "," + employee.name;
}

console.log( employee.id ); //1
console.log( employee.name ); //mary
console.log( employee.getEmployee() ); //1,mary
console.log(typeof employee); //object

 

更精簡的作法是利用Object Literal Notation語法,改寫上述程式碼如下:

var employee = {
  id: 1,
  name: 'mary',
  getEmployee: function () {
    return employee.id + "," + employee.name;
  }
};

console.log( employee.id ); //1
console.log( employee.name ); //mary
console.log( employee.getEmployee() ); //1,mary
console.log( typeof employee ); //object

 

Object Literal Notation語法是以{}符號開始,接著以逗號區隔「屬性 : + 值」或「方法+ : + 方法程式碼」。

有時程式設計師會在最後一個屬性(或方法)定義之後,多一個「,」號,在大多數的瀏覽器之中,沒有什麼特別的問題,會自動忽略掉,不會多一個屬性出來:

var employee = {
  id: 1,
  name: 'mary',
  getEmployee: function () {
    return employee.id + "," + employee.name;
  },

};

 

但在比較舊版的Internet Explorer 瀏覽器就會出現問題。因此建議不要加上這個逗號。

另外特別注意的是,在strict mode之中,JavaScript會自動檢查,不允許屬性的名稱重複出現,例如修改上例,讓id名稱重複出現兩次,執行時會得到錯誤訊息:

'use strict';
var employee = {
  id: 1,
  id: 2, //Duplicate data property in object literal not allowed in strict mode
  name: 'mary',
  getEmployee: function () {
    return employee.id + "," + employee.name;
  },

};


使用JSON格式資料

無庸置疑JSON已經成為現今使用AJAX相關技術的最佳資料交換格式,JSON資料格式是從JavaScript的Object Literal Notation語法演進過來的。例如以下JSON格式的資料語法:

 

{
  "id" : 1,
  "name": "Mary",
  "age": 25
}

 

最大的差別是,在JSON語法中,屬性的名稱要以雙引號包起來;而JavaScript的物件可以不使用雙引號、單引號將屬性的名稱包起來,例如以下都是正確的JavaScript物件的表達方式:

{
  id : 1,
  name: "Mary",
  age: 25
}

 

{
  'id' : 1,
  'name': "Mary",
  'age': 25
}

若要將JavaScript物件轉換成JSON字串,可以使用JSON物件的stringify()方法進行序列化,然後使用JSON物件的parse()方法將JSON字串還原序列化成物件,而不要使用eval()方法將JSON字串還原序列化。例如以下範例程式碼:

var employee =
  {
    "id" : 1,
    "name": "Mary",
    "age": 25
  }
var jstring = JSON.stringify( employee );
console.log(jstring); //{"id":1,"name":"Mary","age":25}

//不好的寫法
var employee2 = eval( '(' + jstring + ')' );

console.log(employee2.id); //1
console.log(employee2.name); //Mary
console.log( employee2.age ); //25

var employee3 = JSON.parse(jstring);

//較好的寫法
console.log( employee2.id ); //1
console.log( employee2.name ); //Mary
console.log( employee2.age ); //25


 

使用物件當函式傳入參數

在撰寫JavaScript程式時,經常會使用到函式(function),例如以下程式碼,定義一個add()函式,可以將傳入的兩個參數值相加之後回傳:

function add(x, y) {
  return x + y;
}

console.log(add(1,2));  //3

 

但若有一天,發現兩個參數不敷使用時,可能又需要修改函式定義,為其再新增多個參數,例如以下程式碼:

function add(x, y, z) {
  return x + y + z;
}

console.log(add(1, 2, 3));

新增的參數可能在原有參數清單之後,也可能在所有參數之前,或許也會出現在現有參數之中,因此你也要確保函式的邏輯不能被新增的參數搞亂。較好的做法是,你可以將所需的參數定義在物件,做為物件的屬性,參考以下範例程式碼:

var param = {
  x: 1,
  y: 2,
  z: 3
};

function add(p) {
  var r = 0;
  for (var i in p) {
    r += p[i];
  }
  return r;
}
console.log(add(param)); //6


保護區域變數 - Immediate Invoked Functions

Immediate Invoked Functions是宣告之後就馬上執行的函式,語法是在小括號之中,撰寫一個匿名函式,最後再附加一個()號,呼叫之,參考下例:

(function () {
  alert('Hi');
})();

以下則是另一種替代性的寫法:

(function () {
  alert('Hi');
}());

 

這兩段程式碼只要一執行,就馬上顯示一個「Hi」的訊息。

Immediate Invoked Functions經常被應用在保護區域變數,在函式之中,使用var宣告的變數將會是區域變數,只有在函式之中的程式碼可以存取,這樣可以保護變數的值,不會被外部其它程式碼修改。例如以下程式碼,第一行宣告一個全域變數x,而Immediate Invoked Functions之中也宣告一個區域變數x,兩個變數是獨立不相關的:

var x = 100;
(function () {
  var x = 200;
  console.log(x); //200, local variable
})();

console.log(x); //100, global variable


 

保護區域變數 - Immediate Object Initialization

另一種保護區域變數的常用手段為Immediate Object Initialization。通常使用Object Literal Notation語法宣告一個物件,內含一個init()方法來進行初始化的動作,並在建立物件時,馬上呼叫init()方法。參考以下範例:

var x = 100;

({
  x: 200,
  inc: function () {
    return this.x += 1;
  },
  init: function () {
    console.log(this.x); //200
    console.log(this.inc()); //201
  }
}).init();

console.log(x); //100


 

第一行定義一個全域變數x,接著是()號,在其中使用Object Literal Notation語法宣告一個物件,其中包含一個區域變數x,一個inc()方法,用來累加x變數值。在()號最後,馬上呼叫init()方法初始化。以下則是替代性的寫法,將所有程式放在()號之中在「}」號之後,直接呼叫init()方法,:

var x = 100;
({
  x: 200,
  inc: function () {
    return this.x += 1;
  },
  init: function () {
    console.log(this.x); //200
    console.log(this.inc()); //201
  }
}.init());

console.log(x); //100


使用模組(Module)設計模式

有些程式語言提供類別(Class)的語法,透過成員存取修飾詞(Access Modifier)來保護私有成員,如變數。JavaScript程式語言雖然沒有提供類似的語法,但是可以很容易使用一些設計模式來模擬這個功能。以下是一個模組(Module)設計模式範例:

var myModule = ( function () {
  //private 變數
  var x = 1;

  //private function
  function printValue() {
    console.log( x );
  }

  //回傳一個物件開放外部存取
  return {
    x: x, //public變數
    printValue: printValue, //public function
    inc: function () { x++; } //public function
  };
}() );

console.log( myModule.x ); //1
myModule.inc();
myModule.printValue(); //2

 

myModule使用Immediate Invoked Functions語法定義,其中包含一個private 變數x,變數的有效範圍在myModule函式之中,myModule函式之外的程式碼不能直接存取此變數值。同樣的myModule之中包含一個printValue(private function),巢狀式的子函式只能被父函式呼叫,所以myModule函式之外的程式碼不能直接呼叫它。myModule會回傳一個物件,利用此物件來開放外部程式存取內部變數或方法,在這個物件中,你可以定義屬性開放外部程式存取內部變數,也可以定義public函式供外部程式呼叫。

參考文章

· Learning JavaScript Design Patterns

http://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript

Tags:

.NET Magazine國際中文電子雜誌 | 許薰尹Vivid Hsu | JavaScript

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List