實作一個 JavaScript 迷你框架 (二)

前言

前一篇筆記,我們完成的需求有:

  • 是一個可重複使用的 library/framework,也就是說,每一個安裝此 framework 的人可以直接使用,不會和它原本程式碼有所衝突

  • 和 jQuery 只需要輸入 $( ) 一樣,我們可以使用 G$() 來建立物件

剩下的需求有:

  • 當我們告訴它我們的姓(lastname)名(firstname)還有選擇的語言(language)時,它可以用正式(formal)和非正式(informal)的方式和我們打招呼。

  • 支援英文(English)和中文(Chinese)兩種語言。

  • 支援 jQuery,可以把 greetr 產生的訊息直接顯示於HTML中。


新增一些變數 & 方法

建立全域環境無法取用的變數

在開始建立方法(method)前,我想要先建立一些變數是我之後可以在方法中使用的,但這些變數又不會和外層的 global environment 有所衝突也不能被外層所取用,要如何達到呢?

參考:
[筆記] JavaScript中Scope Chain和outer environment的概念
[筆記] 談談JavaScript中closure的概念 – Part 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;(function(global, $){

var Greetr = function(firstname, lastname, language){
return new Greetr.init(firstname, lastname, language);
}

var supportedLangs = ['en', 'ch'] /* 看這邊就好 */

Greetr.prototype = {}

Greetr.init = function(firstname, lastname, language){

var self = this;
self.firstname = firstname || '';
self.lastname = lastname || '';
self.language = language || 'ch';

}

})(window, jQuery)

我們不用放在 prototypefunction constructor 裡面,這樣會佔據額外的記憶體位置

直接將變數建立在 IIFE裡 就可以了,通過 scope chain的特性,外部環境 (這邊相對於IIFE的外部環境就是window) 是參照不到這個 IIFE 內的變數的。
透過以上程式碼其實我們已經產生一個閉包 Closure。透過這種方法,我們可以讓使用 framework 的人沒有辦法去改這些變數的值,但是在使用 method 的時候,仍然可以參考到這些變數。


建立更多變數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
;(function(global, $){

var Greetr = function(firstname, lastname, language){
return new Greetr.init(firstname, lastname, language);
}

/* 看這邊就好 */
var supportedLangs = ['en', 'ch'] ;

var greetings = {
en: 'Hello',
ch: '你好'
};

var formalGreetings = {
en: 'Greetings',
ch: '您好'
};

var logMessages = {
en: 'Logged in',
ch: '已登入'
};
/* 看這邊就好 */

// ...略

})(window, jQuery)

開始建立方法(method)

方法該寫在哪 ?

有兩個地方可以放置我們想要的方法,分別是如下圖的(1)和 (2)。但是如果放在這個 function constructor 中(2),變成每一個所建立的物件都會直接帶有這個方法,如此會佔據相當多的記憶體空間;所以比較好的方式是利用原型的概念,把根據這個 function constructor 所建立的物件,都可以使用到的方法,放到(1) 的 prototype 當中。

參考:
JS的原型繼承(方法1) - 函數建構子 與「new」 - 我們不該把方法放在 function constructor 中

開始撰寫程式碼

接著,我們要在 prototype 中開始建立一些 framework 裡面可以使用的方法,就像是jQuery的 animate()attr() 這類的自訂函數。

我們先來看一下建立完方法後完整的程式碼長什麼樣子,這些程式碼都是放在Greetr.prototype 內,接著我們再來分別解釋每個方法的意義

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/* 屬性 */

var supportedLangs = ['en','ch']

var greetings = {
en: 'Hello',
ch: '你好'
};

var formalGreetings = {
en: 'Greetings',
ch: '您好'
};

var logMessages = {
en: 'Logged in',
ch: '已登入'
};

/* 方法 */

Greetr.prototype = {

fullName: function() {
return this.firstname + ' ' + this.lastname;
},

validate: function () {
if (supportedLangs.indexOf(this.language) === -1) {
throw "Invalid language";
};
},

greeting: function() {
return greetings[this.language] + ' ' + this.firstname + '!';
},

formalGreetings: function() {
return formalGreetings[this.language] + ',' + this.fullName();
},

greet: function(formal) {
var msg;

if (formal) {
msg = this.formalGreetings();
}else {
msg = this.greeting();
}

if (console) {
console.log(msg);
}
return this;
},

log: function() {
if (console) {
console.log(logMessages[this.language] + ': ' + this.fullName());
}

return this;
},

setLang: function(lang) {
this.language = lang;
this.validate();
return this;
}

};

方法解析

fullName() - 全名字串組合
1
2
3
fullName: function() {
return this.firstname + ' ' + this.lastname;
}

這個比較單純,就是印出物件實體的名字屬性。


validate() - 驗證語言
1
2
3
4
5
validate: function () {
if (supportedLangs.indexOf(this.language) === -1) {
throw "Invalid language";
};
}

用來檢測使用者所輸入的語言有沒有支援,我們剛剛新增了一個陣列supportedLangs = ['en', 'ch'],我們可以透過 indexOf 來檢測該物件的語言this.language,如果沒有與陣列中任何一個值吻合,indexOf 會回傳 -1,並透過 throw 在 console 裡丟出錯誤訊息。

indexOf: 如果有吻合會回傳對應的陣列位置,例如輸入supportedLangs.indexOf('en'),因為吻合supportedLangs = ['en', 'ch'] 該陣列的第0個物件,所以會回傳 0


greeting() - 非正式招呼
1
2
3
greeting: function() {
return greetings[this.language] + ' ' + this.firstname + '!';
}

這樣寫的目的是要取得 該物件的語言 ,並對應到剛剛宣告的物件 greetings = {en: 'Hello',ch: '你好'} ,把給抓出來,如果 該物件的語言的語言設定成 en 就抓出Helloch就抓出你好


formalGreetings() - 正式招呼
1
2
3
formalGreetings: function() {
return formalGreetings[this.language] + ',' + this.fullName();
}

基本上和上一個非正式招呼一樣,值得注意的是名字的部分它呼叫了this.fullName(),這邊記得要加上this,因為這個方法存在於這個物件的原型裡,而不是環境中。


greet() - 合併正式與非正式招呼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
greet: function(formal) {
var msg;

if (formal) {
msg = this.formalGreetings();
}else {
msg = this.greeting();
}

if (console) {
console.log(msg);
}

return this;
}

透過 greet 這個方法,我可以直接控制我要使用的是 greeting()formalGreeting() ,而不用打出這兩個方法。

  • 如果我們給予的參數 formal 存在的話( if會型轉為true,然後執行程式碼 ),那麼執行 formalGreeting(),否則使用 greeting()
  • 為了避免有些IE版本不支援 console 這個物件,我們用 if(console),意思是如果有 console 這個物件的話,再幫我輸出。
  • 最重要的是 return this ,這是在模仿jQuery鏈式 methods chainning,而這裡的 this 指的也就是我們的物件,因此透過這個 return this,它可以先針對物件進行欲要進行的方法後,最後再次將它回傳成一個物件,於是,它就可以繼續在接著下一個方法,形成一個方法鍊的作法。

(方法鏈)鏈式簡易範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
var person = {
name: 'Dylan',
method1: function() {
this.name = 'Eva';
return this;
},
method2: function() {
this.name = 'John';
return this;
}
}

person.method1().method2();

person.method() 執行後 person.name 會被改為 Eva,然後透過return this 回傳一個改名後的新物件,再接著執行新物件.method2()


log() - 針對不同語言,在console印出登入訊息+使用者名字
1
2
3
4
5
6
log: function() {
if (console) {
console.log(logMessages[this.language] + ': ' + this.fullName());
}
return this;
}

使用的技巧和上面重複。


setLans() - 更改所設定的語言
1
2
3
4
5
setLang: function(lang) {
this.language = lang;
this.validate();
return this;
}

設定新語言,呼叫上面寫過的 validate() 驗證語言是否支援,且一樣使用鏈式return this


測試功能

接著在 app.js 中,我們可以試著來測試一下所建立的 framework 。

1
2
var person = G$('Dylan', 'Liu');   //  language 預設是 ch
person.greet().setLang('en').greet(true);

我就可以得到以下的結果。因為我們有使用了鏈式的技巧,所以我可以一個接著一個方法的使用,當中我又用了 setLang() 這個方法,把預設的語言改成英文:

結果


1
2
var person = G$('Eva', 'Lin');   //  language的預設是ch
person.log().setLang('jp').greet(true);

首先會回傳登入的訊息,接著因為我把語言設成 “jp” ,但框架並不支援日語,所以回拋出錯誤的訊息。


完整程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
;(function (global, $) {


var Greetr = function (firstname, lastname, language) {
return new Greetr.init(firstname, lastname, language);
}

var supportedLangs = ['en','ch']

var greetings = {
en: 'Hello',
ch: '你好'
};

var formalGreetings = {
en: 'Greetings',
ch: '您好'
};

var logMessages = {
en: 'Logged in',
ch: '已登入'
};

Greetr.prototype = {

fullName: function() {
return this.firstname + ' ' + this.lastname;
},

validate: function () {
if (supportedLangs.indexOf(this.language) === -1) {
throw "Invalid language";
};
},

greeting: function() {
return greetings[this.language] + ' ' + this.firstname + '!';
},

formalGreetings: function() {
return formalGreetings[this.language] + ',' + this.fullName();
},

greet: function(formal) {
var msg;

if (formal) {
msg = this.formalGreetings();
}else {
msg = this.greeting();
}

if (console) {
console.log(msg);
}

return this;
},

log: function() {
if (console) {
console.log(logMessages[this.language] + ': ' + this.fullName());
}

return this;
},

setLang: function(lang) {
this.language = lang;

this.validate();

return this;
}

};

Greetr.init = function (firstname, lastname, language) {

var self = this;
self.firstname = firstname || '';
self.lastname = lastname || '';
self.language = language || 'ch';

}

Greetr.init.prototype = Greetr.prototype;

global.Greetr = global.G$ = Greetr;

})(window, jQuery);

資料來源