.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N160617201
出刊日期:2016/6/1
開發工具:Visual Studio 2015 Update 1
在《JavaScript 設計原則-1》這篇文章之中,我們介紹了一些JavaScript重要的設計原則來開發應用程式,遵循一些設計原則來撰寫程式碼,這樣的好處是後續要維護這個系統的人,會更容易進入狀況,快速上,手,也能更容易的進行除錯的動作,找出應用程式的問題所在。本篇文章將延續前文的概念,繼續來介紹一些常用的設計原則或開發模式。
使用模組(Module)設計模式
我們繼續來探討前文提及的模組(Module)設計模式範例。現在先假設一個情況,若模組(Module)的程式碼是放在一個外部檔案,檔名為Module1.js,檔案中包含以下myModule程式碼,模組中包含一個private 變數x、一個 printValue(private function)函式。myModule 回傳一個物件,定義x屬性開放外部程式存取內部變數,與public函式供外部程式呼叫:
//Module1.js檔案
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
};
}());
再假設我們在一個HTML的網頁引用這個檔案的內容,並且加入以下<script>區塊的程式碼,在網頁之中,便可以叫用到Module中的程式碼,我們利用屬性的語法「.」符號來存取x變數的值,叫用inc()與printValue()函式:
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
</head>
<body>
<script src="Module1.js"></script>
<script>
console.log(myModule.x); //1
myModule.inc();
myModule.printValue(); //2
</script>
</body>
</html>
不過,你可能會遇到一種情況,當一個Module的程式太多時,你可能會希望將Module的程式碼分散到別的JavaScript檔案之中。若要解決這個問題,我們可以這樣做,在其它的JavaScript檔案中,加入原來的myModule的定義,例如下列的範例程式碼,我們將此程式儲存在Module2.js檔案之中,使用Immediate Invoked Functions語法定義myModule,並且將myModule當做是函式的的參數傳遞到函式之中,在函式中我們可以再為myModule新增新的函式,或是屬性,最後再將這個模組回傳:
//Module2.js檔案
var myModule = (function (mod) {
mod.alertValue = function (s) {
alert(s);
}
return mod;
}(myModule));
那麼我們就可以在網頁之中,匯入這兩個同時包含myModule定義的JavaScript檔案,並且呼叫定義在兩個模組檔案之中的方法,參考以下範例程式碼:
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
</head>
<body>
<script src="Module1.js"></script>
<script src="Module2.js"></script>
<script>
console.log(myModule.x); //1
myModule.inc();
myModule.printValue(); //2
myModule.alertValue("hello");
</script>
</body>
</html>
使用參數
上一個例子雖然解決跨檔案存放模組程式的問題,但是它有一個缺點,便是引用JavaScript的順序必需先是Module1.js,然後再引用Module2.js檔案,若檔案的順序相反,則alertValue函式就沒有辦法正確執行,參考以下範例程式碼:
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
</head>
<body>
<script src="Module2.js"></script>
<script src="Module1.js"></script>
<script>
console.log(myModule.x); //1
myModule.inc();
myModule.printValue(); //2
myModule.alertValue("hello");
</script>
</body>
</html>
這個網頁執行時會印出錯誤訊息,參考如下圖所示:

雖然你可以修改程式碼中引用JavaScript檔案的順序來避免掉這個問題,但未來若使用非同步的技巧來下載這兩個檔案,我們並無法預期未來哪個檔案會先下載下來,那麼這個解法並不能真的解決掉這個問題。參考以下解法,我們可以在定義模組時,傳入myModule,或建立一個新的物件來解決這個問題,修訂Module1.js程式如下:
//Module1.js
var myModule = (function ( mod ) {
//private 變數
var x = 1;
//private function
function printValue() {
console.log( x );
}
mod.x = x; //public變數
mod.printValue = printValue; //public function
mod.inc = function () { x++; }; //public function
//回傳一個物件開放外部存取
return mod;
}(myModule || {}));
修訂Module2.js程式如下:
var myModule = (function (mod) {
mod.alertValue = function (s) {
alert(s);
}
return mod;
}(myModule || {}));
如此就可以解決JavaScript下載檔案順序性不一定的這個問題。
使用方法串接(Method Chaining)模式
方法串接(Method Chaining)常見於JavaScript程式碼,目地是用來簡化針對一個物件呼叫多次方法時的程式碼。例如以下JavaScript程式碼,宣告一個s變數,重複叫用多次String物件的concat方法,將a、b、c、d串接在一起,最後再叫用toUpperCase()將字串全部轉大寫:
var s = "a"
.concat("b")
.concat("c")
.concat("d")
.toUpperCase();
console.log( s ); //ABCD
方法串接(Method Chaining)也經常出現於jQuery程式,例如以下網頁範例,利用jQuery的css方法設定div的背景顏色與前景顏色,然後透過animate()執行動畫,在2秒內,將寬度、高度設為300px:
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
<style>
#div1 {
border: 1px solid blue;
width: 10px;
height: 10px;
}
</style>
</head>
<body>
<div id="div1">demo</div>
<script src="Scripts/jquery-2.2.3.js"></script>
<script>
$(function () {
$( "#div1" ).css( 'backgroundColor', 'yellow' )
.css( 'color', 'red' )
.animate( { height: '300px', width: '300px' } , 2000 );
});
</script>
</body>
</html>
我們自訂JavaScript物件時,可以套用這個模式,只要讓物件的函式回傳this,這樣就可以利用方法串接的方式來叫用物件的程式碼。以下StringDemo程式碼範例模擬上文的字串串接程式:
function StringDemo( str ) {
this.concat = function ( s ) {
str = str + s;
return this;
}
this.toUpperCase = function () {
str = str.toUpperCase();
return this;
}
this.getResult = function ( callback ) {
callback( str );
return this;
}
}
這樣我們就可以建立一個StringDemo物件,重複叫用concat方法串字串,接著轉換字串為大寫,最後提供一個getResult方法,利用callback函式,在console印出最後字串的內容:
new StringDemo( "a" )
.concat( "b" )
.concat( "c" )
.concat( "d" )
.toUpperCase()
.getResult(function ( result ) {
console.log( result );
});
使用Closure
由於JavaScript的this是一個非常特殊的物件,它可能在執行階段被修改,為了保險起見,建議透過Closure來保護它,在StringDemo函式中,宣告一個變數,來記住this,通常變數的名稱習慣命為self,修改程式碼如下:
function StringDemo( str ) {
var self = this;
this.concat = function ( s ) {
str = str + s;
return self;
}
this.toUpperCase = function () {
str = str.toUpperCase();
return self;
}
this.getResult = function ( callback ) {
callback(str);
return self;
}
}
new StringDemo( "a" )
.concat( "b" )
.concat( "c" )
.concat( "d" )
.toUpperCase()
.getResult(function ( result ) {
console.log( result ); //ABCD
});
設計屬性異動通知功能
若物件的值變動時,想要得到通知,可以為物件設計Observable屬性,通常都應用在事件的設計動作。由於JavaScript的方法可以當做屬性來使用,因此你可以利用這個特色,改用方法來取代屬性的寫法,並在方法中撰寫程式碼,進行異動通知動作。
參考以下範例程式碼,定義一個Stock建構函式,內部定義了一個notifyList陣列,目地是用來記錄所有要得到通知的回呼程式。Stock建構函式定義兩個方法,用來記錄股票的名稱與價格。若呼叫端叫用onPriceChanged方法時,便將回呼函式記錄到notifyList陣列之中,後續當股票的價格變動時,我們利用一個迴圈,執行notifyList陣列之中的回呼函式,來通知異動:
function Stock( name, price ) {
var notifyList = [];
this.name = function ( val ) {
return name;
};
this.stockPrice = function ( val ) {
if (val !== undefined && val !== price) {
price = val;
for (var i = 0; i < notifyList.length; i++) {
notifyList[i](this);
}
}
return price;
};
this.onPriceChanged = function ( callback ) {
notifyList.push(callback);
};
}
var aStock = new Stock( "StockA", 50.5);
console.log( 'The stock name is ' + aStock.name());
console.log( 'The stock price is NT$' + aStock.stockPrice());
aStock.onPriceChanged( function ( b ) {
console.log( 'The stock price has changed to : NT$ ' + b.stockPrice() );
});
aStock.stockPrice( 52.1 );
aStock.stockPrice( 60 );
這個範例的執行結果如下所示,每回叫用stockPrice方法變更股票價格時,就會印出價格變動的訊息:
The stock name is StockA
The stock price is NT$50.5
The stock price has changed to : NT$ 52.1
The stock price has changed to : NT$ 60