JavaScript Patterns 閱讀筆記 (7) Design Patterns

Singleton

  • 讓特定的 instance 只有唯一 instance,即第二次建立的新 object 仍與第一次建立的是同一個
  • Object Literal:每次使用 literal 即已建立 singleton,因 JavaScript 中除非是同一 object,否則 object 不可能相等
  • Constructor Function 方法有二:
    1. 可將 instance 的 cache 放在 static property 中,缺點是其為 public 可能被更動
    2. 將 instance 包進 closure,保持其為 private

Factory

  • 便於建立相似的 object,提供一個建立方式(如以不同字串辨別),且無須先知道明確型別
  • Object() constructor 的行為也是 factory,會根據輸入內容產生不同 object

Iterator

  • 提供資料的集合,以藉由簡單的操作來存取個別元素(如 next()hasNext()

Decorator

  • 在執行時期可動態加入新功能至 object

Strategy

  • 在執行時期選擇適合處理的 algorithm
  • 實例:validator 自動選擇不同檢查類型

Facade

  • 提供替代介面,以保持 method 簡短
  • 例如有多個 methods 常會同時被呼叫,則可另外建立一個新 method 包裝這些會一起呼叫的 methods

Proxy

  • 控制存取權,將多個操作組合,以避免昂貴運算(例如多個 request 組合成一個)

Mediator

  • 讓各個獨立 object 不直接溝通,透過 mediator 進行溝通以降低 coupling,提升可維護性
  • 白話一點就是不要讓 object 認識太多其他 object,維護時會有困難;讓大家只專注在自己的工作即可,並由 mediator 負責彼此的溝通

Observer (PubSub)

  • 又稱 custom events,同樣也是為了降低 coupling 以提升可維護性
  • 使 object 不再直接呼叫其他 object 的 method,而是 subscribe 其他 object 的特定活動,並在 event 發生時由 publisher 通知 subscribers

JavaScript Patterns 閱讀筆記 (6) Code Reuse Patterns

Classical Inheritance vs. Modern Inheritance

JavaScript 沒有 class 的概念,使用 new 雖然很像 class 的語法,但 constructor function 仍舊只是個 function。

  • Classical pattern:以 class 的想法模擬出繼承(inheritance)的方式
  • Modern pattern:不思考 class 的方式(應盡量選擇此 pattern)

Classical Inheritance

目的:使 Child() 所建立的 instance 可以取得 Parent() 的 property

1
2
3
4
5
6
7
8
9
10
11
12
function Parent(name) {
this.name = name || "Father";
}
Parent.prototype.getName = function () {
return this.name;
};
function Child(name) {}
// Child 繼承 Parent
inherit(Child, Parent);

#1 The Default Pattern

1
2
3
4
5
6
function inherit(C, P) {
C.prototype = new P(); // 應指向 instance 而非 function(務必加 new)
}
var kid = new Child();
kid.getName(); // "Father"
  • 缺點:同時繼承了 this 的 property 與 prototype 的 property,通常 this 的 property 是屬於 instance 專屬的,所以並不希望這些被繼承,而我們會把可 reuse 的 member 放在 prototype 中。
  • 缺點:無法透過 child 的 constructor 傳遞參數給 parent 的 method

#2 Rent a Constructor

1
2
3
4
5
6
7
function Child(name) {
Parent.apply(this, arguments); // 若有多行則可實現多重繼承
}
var kid = new Child("Son");
kid.name; // "Son"
typeof kid.getName; // "undefined"
  • 優點:解決 #1 的第二個缺點,且將 parent 中 this 的 property 複製到 child,不會有覆寫的風險
  • 缺點:prototype 的 property 都沒被繼承

#3 Rent & Set Prototype

1
2
3
4
5
6
7
8
9
10
function Child(name) {
Parent.apply(this, arguments);
}
Child.prototype = new Parent();
var kid = new Child("Son");
kid.name; // "Son"
kid.getName(); // "Son"
delete kid.name;
kid.getName(); // "Father"
  • 優點:child 繼承了 parent 自身 member 的複製,也繼承了 parent 的 prototype(即 #1 與 #2 的組合)
  • 缺點:Parent() 被呼叫了兩次,效率較差

#4 Share the Prototype

1
2
3
function inherit(C, P) {
C.prototype = P.prototype;
}
  • 優點:不會呼叫 Parent() 兩次
  • 缺點:修改 prototype 後會影響繼承的所有 object

#5 Temporary Constructor

1
2
3
4
5
function inherit(C, P) {
var F = function () {};
F.prototype = P.prototype;
C.prototype = new F();
}
  • 優點:解決了 #4 的缺點

#最終版

1
2
3
4
5
6
7
8
9
var inherit = (function () { // 以 immediate function 避免每次重複建立 temporary constructor
var F = function () {};
return function (C, P) {
F.prototype = P.prototype;
C.prototype = new F();
C.uber = P.prototype; // superclass
C.prototype.constructor = C; // 重設 constructor pointer
}
}());

Modern Inheritance

概念:object 繼承自其他 object,即從 parent object 取得功能以建立 child object。

Prototypal Inheritance

1
2
3
4
5
6
7
8
9
10
11
12
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var parent = {
name: "Father"
};
var child = object(parent);
console.log(child.name); // "Father"

不一定要用 object literal,也可用 constructor function:

1
2
3
4
5
6
7
8
9
10
function Parent() {
this.name = "Father";
}
Parent.prototype.getName = function () {
return this.name;
};
var kid = object(new Parent()); // 只繼承 prototype 時:object(Parent.prototype)
kid.getName(); // "Father"

在 ECMAScript 5 中,不需自己實作 object(),可直接用 var child = Object.create(parent);

Inheritance by Copying Properties

此 pattern 沒有複製到 prototype,只有自身 property。

淺層複製(不檢查是否為 object 或 array,只複製到 reference)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function extend(parent, child) {
var i;
child = child || {};
for (i in parent) {
if (parent.hasOwnProperty(i)) {
child[i] = parent[i];
}
}
return child;
}
var parent = {name: "Father"};
var child = extend(parent);
child.name; // "Father"

深層複製(將 object 或 array 也複製)

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
function extendDeep(parent, child) {
var i,
toStr = Object.prototype.toString,
astr = "[object Array]";
child = child || {};
for (i in parent) {
if (parent.hasOwnProperty(i)) {
if (typeof parent[i] === "object") {
child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
extendDeep(parent[i], child[i]);
}
else {
child[i] = parent[i];
}
}
}
return child;
}
var parent = {list: [1, 2, 3, 4]};
var child = extendDeep(parent);
child.list.push(5);
child.list.toString(); // "1,2,3,4,5"
parent.list.toString(); // "1,2,3,4"
var child2 = extend(parent);
child2.list.push(5);
child2.list.toString(); // "1,2,3,4,5"
parent.list.toString(); // "1,2,3,4,5" (parent.list被覆寫)

Borrowing Method

只使用想用的 method,不真的繼承所有 property。使用 call()apply() 實作:

1
2
3
obj1.someFn.call(obj2, p1, p2, p3); // 即 obj1.someFn(p1, p2, p3);,且其中的 this 指向 obj2
// or
obj1.someFn.apply(obj2, [p1, p2, p3]);

JavaScript Patterns 閱讀筆記 (5) Object Creation Patterns

Namespace

使用 namespace 可避免發生命名衝突,並降低 global variable 的需求量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 只用一個 global object
var MYAPP = {};
// constructors
MYAPP.Parent = function () {};
MYAPP.Child = function () {};
// a variable
MYAPP.some_var = 1;
// an object container
MYAPP.modules = {};
// nested objects
MYAPP.modules.module1 = {};
MYAPP.modules.module1.data = {a: 1, b: 2};
MYAPP.modules.module2 = {};

為避免覆蓋掉已存在的 namespace 或 property,我們可以設計一個檢查用的 function,用來處理建立 namespace 與新增其 property 的細節。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var MYAPP = MYAPP || {};
MYAPP.namespace = function (ns_string) {
var parts = ns_string.split('.'),
parent = MYAPP,
i;
// strip redundant leading global
if (parts[0] === "MYAPP") {
parts = parts.slice(1);
}
for (i = 0; i < parts.length; i += 1) {
// create a property if it doesn't exist
if (typeof parent[parts[i]] === "undefined") {
parent[parts[i]] = {};
}
parent = parent[parts[i]];
}
return parent;
};
// 使用
var mod = MYAPP.namespace('MYAPP.modules.module2');
mod === MYAPP.modules.module2; // true

這類使用 namespace 且模組化的程式(尤其在完善的 library 中常見),使用時可在 function 頂端建立 local variable 並指向所需模組,如:var event = MYAPP.util.Event。此 pattern 稱為 declaring dependency,這麼做有許多優點:

  • 明確提醒其他會用到這份 code 的人有哪些特殊的 script 檔案必須 include
  • 使用 local variable(如 event)永遠比使用 global variable(如 MYAPP)來得快,尤其比巢狀 property(如 MYAPP.util.Event)效能更好
  • minify 工具會 rename local variable,可得到較少的 code

Private Properties & Methods

JavaScript 所有 object member 皆是 public,雖然語言並沒有為 private member 提供特殊語法,但我們仍可用 closure 來實作出 private member。

Constructor Function

主要觀念即 function 的 scope 特性,只要在 function 中宣告即可成為 local variable,雖然並非真正的 property,但已可達成 private 的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Rectangle() {
// private member
var height = 10,
width = 5,
area = height * width;
// public function
this.getArea = function () {
return area;
};
}
var shape1 = new Rectangle();
console.log(shape1.area); // undefined (private)
console.log(shape1.getArea()); // 50 (public)

要特別注意的一點是,當我們從 privileged method(如上例的 getArea())回傳 private 變數時,該變數千萬不可以是 object 或 array,因為此時回傳的是其 reference,如此將會使得外部的 code 仍然可修改 private member。

如果要解決這個問題,可以讓 privileged method 直接回傳一個新的 object,或直接使用 extend()extendDeep() 做 object 的複製。

Object Literal

使用 object literal 建立 object 一樣也能實作出 private member,可以用 immediate function 來建立 closure。

1
2
3
4
5
6
7
8
9
10
11
12
var obj = (function () {
// private member
var name = "test";
// public function
return {
getName: function () {
return name;
}
};
}());
obj.getName(); // "test"

Prototype

如果每次使用 constructor function 建立的 object 都會重新建立相同的 private member,則可以將共同的 property 或 method 加到 prototype 中以節省 memory,這麼做能使共有的 member 在所有 instance 間共享。

1
2
3
4
5
6
7
8
9
10
11
Rectangle.prototype = (function () {
// private member
var type = "Rectangle";
// public prototype member
return {
getType: function () {
return type;
}
};
}());

Revelation

此 pattern 可將 private method 揭露為 public,但原始的實作內容並不會如同 public 的狀態能夠真的被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var test;
(function () {
function foo() {
return "still alive";
}
test = {
fn1: foo,
fn2: foo
};
}());
test.fn1 = null;
test.fn2(); // "still alive"

Module

雖然 JavaScript 沒有 package 的語法,我們可以結合前面介紹過的 namespace、immediate function、private member、declaring dependency 等 patterns 來實現 module pattern。

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
MYAPP.namespace('MYAPP.utilities.array');
MYAPP.utilities.array = (function () {
// dependencies
var uobj = MYAPP.utilities.object,
ulang = MYAPP.utilities.lang,
// private properties
array_string = "[object Array]",
ops = Object.prototype.toString;
// private methods
// ...
// end var
// optionally one-time init procedures
// ...
// public API
return {
inArray: function (needle, haystack) {
for (var i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle) {
return true;
}
}
},
isArray: function (a) {
return ops.call(a) === array_string;
}
// ... more methods and properties
};
}());

Sandbox

此 pattern 解決 namespace 的缺點:隨時可被變更的 global variable、很長的巢狀 property 導致打字不便及解析效能下降。 (詳細使用方式筆記略)

Static Members

static member 特性:instance 間可共享、不需建立 instance 即可直接使用

Public Static Members

即直接新增 property 給 constructor function。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// constructor
var Circle = function () {};
// a static method
Circle.isShape = function () {
return true;
};
// a normal method added to the prototype
Shape.prototype.setColor = function (color) {
this.color = color;
};
// calling a static method
Circle.isShape(); // true
// creating an instance and calling a method
var moon = new Circle();
moon.setColor("yellow");
typeof Circle.setColor; // "undefined"
typeof moon.isShape; // "undefined"

比較兩種 method,static 可直接透過 constructor 呼叫,一般的則需先有 instance 才可呼叫。當然也可以讓 static method 透過 instance 也能呼叫:Circle.prototype.isShape = Circle.isShape;,但若其實作中有使用到 this,須特別注意兩者 reference 有所不同。

Private Static Members

如同上方 private properties & methods 的做法,只需使用一 immediate function 來建立 closure 以容納 private member,並在 prototype 實作一 privileged method。(closure 內的 private static member 為所有 instance 間共享)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// constructor
var Circle = (function () {
// static variable/property
var counter = 0,
NewCircle;
// new constructor implementation
NewCircle = function () {
counter += 1;
};
// a privileged method
NewCircle.prototype.getLastId = function () {
return counter;
};
// overwrite the constructor
return NewCircle;
}()); // execute immediately
var shape1 = new Circle();
shape1.getLastId(); // 1
var shape2 = new Circle();
shape2.getLastId(); // 2

Object Constants

一般會使用 naming convention 將所有字母改為全大寫來建立 constant,如 Math.PI

附註:如果真的想實作一個不可變動的值,可參考書上提供的 general-purpose constant object。

Chaining

jQuery 的一大特色即使用此 pattern,允許讓 object 不用 assign 給變數或拆成多行,可以直接連續地呼叫多個 method。實作時讓 method 回傳 this(即 instance 本身),即可達成此效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var calculate = {
value: 0,
plus: function (v) {
this.value += v;
return this;
},
minus: function (v) {
this.value -= v;
return this;
},
output: function () {
console.log(this.value);
}
};
calculate.plus(7).minus(2).plus(3).output(); // 8
  • 優點:code 較精簡、各 method 有各別專屬功能,提升可維護性
  • 缺點:debug 難度增加

JavaScript Patterns 閱讀筆記 (4) Functions

簡介

JavaScript 的 function 主要特色有二:

  1. function 是 first-class object,可以像 value 一樣當作參數傳遞,也可以擴充其 property 及 method
  2. function 提供了 scope 的概念,在 function 中用 var 宣告變數皆會是 local variable(不同於某些語言,JavaScript 並不擁有 curly brace local scope)

Function Expression

又稱為 anonymous function,如下例:

1
2
3
var add = function (a, b) {
return a + b;
};

function expression 的一個特例為 named function expression,如下:

1
2
3
var add = function add(a, b) {
return a + b;
};

兩者的差異是,前者 function object 的 name property 會是空字串(Firefox & WebKit)或 undefined(IE),後者 add.name 的值會是 “add”。
註:name property 並非 ECMAScript 標準,但大多數環境皆可使用,特別在使用 debug 工具時可能常會用到。

另外,named function expression 不建議 assign 給另一個名稱的變數,如:var foo = function bar() {};,此用法在部分 browser(如 IE)並未被正確實作。

我們應避免使用如同 var add = new Function('a, b', 'return a + b;'); 的 constructor function,因為這種做法常會需要使用 escape character 且縮排不便,對可讀性影響很大。

Function Declaration

1
2
3
function add(a, b) {
return a + b;
}

上述兩種定義 function 的方式在結尾部分有所不同,expression 需加上分號, declaration 則不用。除此之外,最大的差異在於 hoisting 行為…

Function Hoisting

function declaration 與 named function expression 乍看之下兩者的外觀與行為都十分接近,其實不然。如同第2章筆記提到的 hoisting 行為,不論在程式何處宣告變數,都會在幕後被 hoisted 到最頂端(僅宣告該變數,不會定義其內容,即 var foo = undefined;)。

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
// global functions
function foo() {
alert('global foo');
}
function bar() {
alert('global bar');
}
function hoistMe() {
console.log(typeof foo); // "function"
console.log(typeof bar); // "undefined"
foo(); // "local foo"
bar(); // TypeError: bar is not a function
// function declaration: 宣告及實作都會被 hoisted
function foo() {
alert('local foo');
}
// function expression: 只有變數宣告被 hoisted
var bar = function () {
alert('local bar');
};
}
hoistMe();

從上例可以很明顯看出:由於 hoisting 行為,named function expression 只有變數宣告被 hoisted,其 function 實作的部分並沒有;而 function declaration 則會將宣告及實作皆 hoisted。

Callback Function

設計 function 時應盡可能使其保持 generic,亦即不要將許多功能的實作寫死在同一個 function 中。這種需 coupling 多個動作於同一個 function 的動機,便是非常適合使用 callback 的時機。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var findNodes = function (callback) {
var i = 1000,
nodes = [],
found;
while (i--) {
// 做完複雜操作後,決定 found,並執行 callback
if (typeof callback !== "function") {
callback(found);
}
nodes.push(found);
}
return nodes;
};
var hide = function (node) {
node.style.display = "none";
};
findNodes(hide);

注意 callback 的 scope

當 callback 並非 anonymous function 或 global function,而是某個 object 的 method 時,如果其中有使用 this 去 refer 其所屬的 object,會導致非預期的行為。

解法:除了傳遞 callback 之外,額外傳遞其所屬 object,並將呼叫方式改為 callback.call(callback_obj, found);

其他使用 callback 的例子

  • Asynchronous Event Listener: document.addEventListener("click", callback, false);
  • Timeout: setTimeout(callback, 1000);

Returning Function

1
2
3
4
5
6
7
8
9
10
11
var init = function () {
var count = 0;
return function () {
return (count += 1);
};
};
var count = init();
count(); // 1
count(); // 2
count(); // 3

init() 中包裝 returned function 會產生一個 clusure,可用於儲存 private 資料,因為這些資料只能被 returned function 存取。

Self-Defining Function

1
2
3
4
5
6
7
8
9
var test = function () {
console.log("test");
test = function () {
console.log("new test");
};
};
test(); // "test"
test(); // "new test"

又稱 lazy function definition,用這種方式可回收舊的 function pointer 並指向新 function,即用新的實作內容覆蓋掉自己。對於某些只需執行第一次便不再用到的工作,此法非常適合使用。

Immediate Function

1
2
3
(function () {
console.log("Go!");
}());

又稱 self-invoking function 或 self-executing function,此語法可讓 function 在定義時立刻執行,適用於只需執行一次且不再重複使用時(如初始化動作),也可以避免暫時使用的變數汙染了 global scope。

immediate function 也可以有參數及回傳值:可將 global object 作為參數傳進 function 中以便存取 global variable;也可以在完成某個複雜的運算後回傳其值並將整個 function 直接 assign 給變數。

另外一個與此 pattern 類似的是 Immediate Object Initialization,適用在初始化較繁複的一次性工作,在 init() 完成後不再存取該 object。

1
2
3
4
5
6
7
({
num: 100,
init: function () {
// 初始化工作
// 如果執行一次後未來仍可能會存取此 object,可加上 return this;
}
}).init();

Function Property

由於 function 是 object,因此當然可以擁有 property。我們可以透過自訂 property 來作為運算結果的 cache,下次再被呼叫時便不必重複可能非常繁重的計算。

1
2
3
4
5
6
7
8
9
var myFunc = function (param) {
if (!myFunc.cache[param]) {
var result = {};
// 繁重的計算
myFunc.cache[param] = result;
}
return myFunc.cache[param];
};
// 可透過 myFunc.cache 直接存取計算過的結果

Configuration Object

此 pattern 在提供乾淨的 APIs 時相當有用。程式開發時,需求發生變動是很平常的事,因此可能會出現一些 function 逐漸擁有許多參數,且有些參數是 required 有些則是 optional。

1
2
function addShape(shape, owner, color, size, maker) {...}
addShape("circle", "Dylan", "blue", null, "Tom");

我們可以將所有參數替換成一個 object 並作為唯一的參數,這麼做相較於上例擁有下列優點:

  • 不需記住參數的順序
  • 可以安全的略過 optional 參數
  • 容易閱讀及維護
  • 容易新增及移除參數
1
2
3
4
5
6
var conf = {
shape: "circle",
owner: "Dylan",
color: "blue"
};
addShape(conf);

Curring

即 partial function application,當經常呼叫某個 function 且其傳入的參數大多都相同時,便適合使用 curring 技巧。使用時會先傳入部分參數給 function,並動態產生另一個新 function,如此該新 function 便能保留重複的那些參數,於是就不必每次都傳遞。

術語說明

  • curring:其命名是來自數學家 Haskell Curry,指 function transformations 的過程
  • unction application:實際上 function 並不是被 called 或 invoked,而是 applied。JavaScript 的 function object 有提供 Function.prototype.apply()Function.prototype.call() 兩個 method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var test = function (str) {
return "str: " + str;
};
test("LOL"); // invoke, "str: LOL"
test.apply(null, ["XD"]); // apply, "str: XD"
test.call(null, "XD"); // call, "str: XD"
var obj = {
test: function (str) {
return "str: " + str;
}
};
obj.test("LOL"); // "str: LOL"
test.apply(obj, ["XD"]); // "str: XD"
test.call(obj, "XD"); // "str: XD"

實際範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function add(x, y) {
if (typeof y === "undefined") { // partial
return function (y) {
return x + y;
};
}
// full application
return x + y;
}
typeof add(8); // "function"
add(4)(8); // 12
var ROC2AD = add(1911);
ROC2AD(102); // 2013

附註:書上另有 general-purpose function 可用,實作方式較為複雜,需要時可再查閱。

JavaScript Patterns 閱讀筆記 (3) Literals and Constructors

Object Literal

使用 literal notaion 優於使用內建 constructor function(Object())。

1
2
3
4
5
6
// 1. object literal
var car = {goes: "far"};
// 2. built-in constructor function (antipattern)
var car = new Object();
car.goes = "far";

因為 Object() 接受一個參數,並會根據該參數的值將建立 object 的動作委派給另一個內建的 constructor function 執行,如此再回傳的便是我們預料之外的 object,違反了「可預期的」這條規則。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// empty object
var obj = new Object();
console.log(obj.constructor === Object); // true
// number object
var obj = new Object(1);
console.log(obj.constructor === Number); // true
console.log(obj.toFixed(3)); // "1.000"
// string object
var obj = new Object("string");
console.log(obj.constructor === String); // true
console.log(obj.substring(1,3)); // "tr"
// 一般物件沒有 substring() 這個 method,但 string object 有
console.log(typeof obj.substring); // "function"
// boolean object
var o = new Object(true);
console.log(o.constructor === Boolean); // true

自訂 Constructor Function

1
2
3
4
5
6
7
8
9
var Dog = function (name) {
this.name = name;
this.bark = function () {
return "Woof!";
}
this.getName = function () {
return this.name;
}
}

使用 new 呼叫 constructor function 時,幕後發生的事類似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var Dog = function (name) {
// 使用 object literal 建立一個 empty object
// var this = {};
// 正確來說應該是:var this = Object.create(Dog.prototype);
// 加入 property 及 method
this.name = name;
this.bark = function () {
return "Woof!";
}
this.getName = function () {
return this.name;
}
// 最後將此 object 回傳,成為一個 instance
// return this;
}

上例中,每次呼叫 new Dog() 時,新 function 就會在 memery 中佔用空間,但因在每個 instance 中的 bark()getName() 皆相同,因此我們可以把這類會重複使用的相同 method 放入 prototype 中,以避免浪費效能。

1
2
3
4
5
6
Dog.prototype.bark = function () {
return "Woof!";
}
Dog.prototype.getName = function () {
return this.name;
}

強制造成 new 效果

本質上仍是 function 的 constructor function,如果忘了 new,其中的 this 將會指向 global object(即 browser 中的 window),造成邏輯錯誤。
註:ECMAScript 5 的 strict mode 中 this 不再指向 global object。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// constructor function
function Pen() {
this.color = "red";
}
// 有加 new
var stationery = new Pen();
console.log(typeof stationery); // "object"
console.log(stationery.color); // "red"
// 沒加 new (antipattern)
var stationery = Pen();
console.log(typeof stationery); // "undefined"
console.log(stationery.color); // TypeError: Cannot read property 'color' of undefined
console.log(window.color); // "red" (成為 global object 的一個 property)

解決方法:self-invoking constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Pen() {
// 兩種用法皆可
/*
// 第一種在不想寫死 function 名稱時可用(但 ECMAScript 5 的 strict mode 不可用 arguments.callee)
if (!(this instanceof arguments.callee)) {
return new arguments.callee();
}
*/
if (!(this instanceof Pen)) {
return new Pen();
}
this.color = "red";
}

Array Literal

使用 literal notaion 優於使用內建 constructor function(Array())。

1
2
3
4
5
// 1. array literal
var animals = ["bird", "cat", "dog"];
// 2. built-in constructor function (antipattern)
var animals = new Array("bird", "cat", "dog");

使用 new Array() 須注意此 constructor function 可能的陷阱,當傳遞單一參數給 Array() 且其為數值時,該數值表示此 array 的長度而非第一個元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 一個元素的 array
var a = [3];
console.log(a.length); // 1
console.log(a[0]); // 3
// 三個元素的 array
var a = new Array(3); // 3 是指長度而非元素值
console.log(a.length); // 3
console.log(typeof a[0]); // "undefined"
// 當參數非整數時便會發生錯誤
var a = new Array(3.14); // RangeError: Invalid array length
console.log(typeof a); // "undefined"

若要檢查某 object 是否為 array,ECMAScript 5 提供了 Array.isArray()可用來檢查。如果環境不支援此 method,可用以下方式處理:

1
2
3
4
5
if (typeof Array.isArray === "undefined") {
Array.isArray = function (arg) {
return Object.prototype.toString.call(arg) === "[object Array]";
};
}

JSON (JavaScript Object Notation)

JSON 其實就是 object literal 與 array literal 組合成的一種資料格式。

1
2
3
4
{
"name": "value",
"list": [1, 2, 3]
}

JSON 與 object literal notaion 在語法上的差別:前者必須以引號包住其 property 才算合法;後者只在不合法的名稱(如中間有穿插空格)時才需要引號。除此之外,JSON 中不可使用 function 及 regex literal。

基於安全性考量,不可使用 eval() 執行 JSON 字串,應用 ECMAScript 5 提供的 JSON.parse(),若不支援也可用其他 library 的 parse 工具。

Regular Expression Literal

使用 literal notaion 優於使用內建 constructor function(RegExp())。

1
2
3
4
5
// 1. regex literal
var re = /\\/gm;
// 2. built-in constructor function (antipattern)
var re = new RegExp("\\\\", "gm");

因為使用 constructor function 需使用 escape character(如上例的四個反斜線),如此會使 regex 變長且難以閱讀。除非無法事先得知,在執行時才能決定 pattern,此時才適合使用 new RegExp()

regex literal 用 / 包住 regex,最後可加入修飾詞:

  • g: global matching
  • m: multiline
  • i: case-insensitive matching

Primitive Wrapper

原始型別有對應的 wrapper object,可用內建的 constructor function 來建立,number, string, boolean 分別對應 Number(), String(), Boolean()

1
2
3
4
5
6
7
// primitive number
var n = 100;
console.log(typeof n); // "number"
// number object
var n = new Number(100);
console.log(typeof n); // "object"

wrapper object 內建許多有用的 property 或 method,如 string object 可用 substring()toLowerCase() 等 method,以及 length 此 property。而原始型別也可以直接使用這些 property 及 method,當我們使用時會在背景暫時轉型為一 object。

1
2
3
// privitive string 也可以當成 string object 來使用
var str = "test";
console.log(str.toUpperCase()); // "TEST"

使用 wrapper object 可以擴充功能,原始型別因並非 object 則無法擴充。除非必要,否則應使用較簡潔的原始型別。

1
2
3
4
5
6
7
8
9
// 盡量少用
var s = new String("my string");
var n = new Number(101);
var b = new Boolean(true);
// 這些比較簡潔
var s = "my string";
var n = 101;
var b = true;

總結

除了 Data() 之外,極少有需求會使用內建的 constructor function。下表整理此篇建議用來取代內建 constructor 的 literal 與原始型別:

內建 constructors (少用) literals & primitives (推薦)
var o = new Object(); var o = {};
var a = new Array(); var a = [];
var re = new RegExp("[a-z]", "g"); var re = /[a-z]/g;
var s = new String(); var s = "";
var n = new Number(); var n = 0;
var b = new Boolean(); var b = false;

附註:書中此章節還有提到 error object,但因我個人比較少用到,暫時先略過,之後有疑惑再回去翻書。

JavaScript Patterns 閱讀筆記 (2) Essentials

閱讀 code 的時間會比 撰寫 的時間來得多,因此寫出容易維護的 code 便顯得相當重要。
容易維護的 code 具備以下條件:

  • 可讀性
  • 一致性(像同一個人寫的)
  • 可預期的
  • 文件化

少用全域變數(global variable)

  • 變數的 scope 在 function 內,即 local variable
  • 定義在 function 外或未經宣告直接使用(不論 function 內外)則為 global
  • JavaScript 執行環境皆有 global object,在 function 外可用 this 進行存取;在 browser 中,window 為 global object 的一個 property,指向 global object 本身,因此也可用 window 存取 global variable
1
2
3
4
5
gvar = "test"; // antipattern
console.log(gvar); // "test"
console.log(window.gvar); // "test"
console.log(window["gvar"]); // "test"
console.log(this.gvar); // "test"
1
2
3
4
// 如果在不同執行環境中,`window` 未必可用,若不寫死可用以下方法
var global = (function () {
return this;
}());
  • 使用 global variable 可能發生命名衝突(例如有多個 libraries 或多段不同人寫的 code 時),應盡可能少用
  • chain assignment 也會建立 global variable
1
2
3
4
5
function foo() {
var a = b = 0; // a: local; b: global
var c, d;
c = d = 0; // c: local; d: local
}
  • 隱含的 global variable(即未經 var 宣告就直接使用的)可以用 delete 刪除;因為該變數嚴格來說其實是 global object 的 property 而非變數,delete 可刪除 object 的 property 而不能刪除變數

使用單一 var

  • hoisting 行為:在任何位置以 var 宣告變數,其行為與「在頂端宣告」相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bar = "global"; // global variable
function foo() {
console.log(bar); // "undefined"
var bar = "local";
console.log(bar); // "local"
}
foo();
// 上例相當於以下行為
bar = "global"; // global variable
function foo() {
var bar; // same as: var bar = undefined;
console.log(bar); // "undefined"
bar = "local";
console.log(bar); // "local"
}
foo();
  • 在 function 開頭使用單一 var 敘述,優點:
    • 查閱使用變數時只需找一個地方
    • 避免 hoisting 行為造成的邏輯錯誤
    • 減少「造成隱含的全域變數」發生
    • 使 code 字數更精簡
1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 0,
b = 1,
sum = a + b,
myobj = {},
i,
j,
result = document.getElementById("result"),
resultStyle = result.style;
}

預先計算 iteration 次數

  • 使用 for loop 時預先計算次數,以避免每次皆計算而影響效能(尤其針對 HTMLCollection object 時影響更大)
1
2
3
for (var i = 0, max = myarray.length; i < max; i++) {
// ...
}
  • 效能更佳的 loop(只使用一個變數且只判斷零與非零)
1
2
3
4
5
6
7
8
9
10
11
12
// for-loop
var i, myarray = [];
for (i = myarray.length; i--;) {
// ...
}
// while-loop
var myarray = [],
i = myarray.length;
while (i--) {
// ...
}
// 註:JSLint 看到 ++ 或 -- 會抱怨
  • for-in loop 不保證列出的順序,建議將 for 用於重複 array,將 for-in 用於重複 object
  • 使用 for-in 時應加上 hasOwnProperty() 檢查該 property 是否來自 prototype,因為擴充 prototype 的行為是 live,所有已存在的 object 將自動能存取新加入的 property
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
var dog = {
eye: 2,
head: 1,
leg: 4
}
// 在某處將一個新 method 加入到所有 object 中
if (typeof Object.prototype.clone === "undefined") {
Object.prototype.clone = function () {};
}
// 1. for-in loop
for (var i in dog) {
// 兩種用法皆可
// if (Object.prototype.hasOwnProperty.call(dog, i)) {
if (dog.hasOwnProperty(i)) {
console.log(i, ":", dog[i]);
}
}
/*
console 中的結果:
eye : 2
head : 1
leg : 4
\*/
// 2. antipattern (no hasOwnProperty())
for (var i in dog) {
console.log(i, ":", dog[i]);
}
/*
console 中的結果:
eye : 2
head : 1
leg : 4
clone : function () {}
*/

Coding Conventions

  • 縮排很重要
  • 永遠都應加上大括號,即使只是單行敘述
  • 左大括號置於同一行
  • 適當地加入空格
  • 以空白行分隔不同段落

Naming Conventions

  • 將 constructor function 首字使用大寫,一般 function 則用小寫
  • 駝峰式命名法:僅每個字的第一個字母使用大寫,其餘皆小寫
  • 常數採用全大寫命名;全域變數也可考慮使用全大寫字母
  • 在變數前或後加底線來表示 private member,如 getElements_()(JSLint 會對底線前綴警告)

其他

  • 最好不要擴充內建型別(Object(), Array(), Function(), etc.)的 prototype
  • switch 中,case 不應縮排,最後務必以 break; 結束每個 case,務必以 default: 結束 switch
  • 避免造成隱含的型別轉換,應使用 ===!== 同時比較型別與值 (reference)
  • 避免使用 eval(),若非用不可時可改用 new Function() 或 immediate function
  • 為避免回傳 NaN,建議使用 parseInt() 將 string 轉成 number,且第二個參數(進位制的基數)不應省略
1
2
var month = "03";
month = parseInt(month, 10); // 10進位

JavaScript Patterns 閱讀筆記 (1) Introduction

最近終於有時間可以好好把這本書讀一讀,書中部分內容透過經驗的累積或許已經知道了,但再次根據系統性的整理與複習對觀念的鞏固仍然是相當有幫助的,讀完後可說是獲益良多。

Pattern 優點

  • 使用前人驗證過的優良實作方式
  • 將某些技巧或實作方式命名,增進 developer 間溝通效率

Object 觀念

  • number, string, boolean, null, undefined 非 object,屬原始型別(primitive type)
  • function 可擁有 property 與 method,亦屬於 object
  • object 只是 properties 的集合(當 property 是 function 時稱作 method),或 key-value pairs 的清單(如其他語言中的 associative array)
  • 兩種主要 object:
    1. Native Object:ECMAScript 標準中所描述,如內建 object(Array, Date, etc.)或使用者定義的 object(var obj = {};
      1. Host Object:定義在 host 環境中,如 browser 的 window object 及 DOM object

Prototype 觀念

  • 常用 prototype 實作繼承(inheritance)
  • prototype 是一個 object,且每個 function 都有 prototype 這個 property

其他

  • script 語言通常需要一個執行環境作為 host,如 Ruby 的 host 是 Java,Python 的 host 是 C/C++
  • JavaScript 的核心是基於 ECMAScript 標準,ECMAScript 5 於 2009 年 12 月發佈,新增了 strict mode
Strict Mode 使用範例
1
2
3
4
function foo() {
"use strict";
// ...
}
  • 使用 JSLint 檢測 code,養成正確開發習慣
  • 現今大多數 browser 皆支援 console object,是非常用於學習的工具(常用 methods:log(), dir()

How to lose weight (in the browser) 筆記

Source: http://browserdiet.com/

HTML

  • 避免對 stylesheet 或 javascript 使用 inline 或 embedded code 的方式
  • 除非是一次性的瀏覽頁面(一般 user 只會瀏覽一次,不使用 external 可減少 request 數)
  • external 的順序也有影響(最好把 style 放在 script 前)(reference)
  • css 放愈前面愈好
  • script 放愈後面愈好
  • minify html(移除註解、空白、換行)
  • 適當使用 async 如:<script async src="example.js"></script>

CSS

  • minify CSS
  • 合併所有 external stylesheet 以減少 request 數
  • <link> 優於 @import

JavaScript

  • 使用第三方的 script 最好用 async 或 friendly iframes,避免因為連線問題影響讀取速度
  • 避免在 loop 中多次重複計算 array length
  • 避免使用 document.write
  • 在 loop 中設定 layout 時避免多次重複計算 layout
  • 如果要重複在 DOM 取得同一個 element,最好取一次並存在 variable 就好
  • minify JavaScript
  • 合併所有 external JavaScript 以減少 request 數

jQuery

  • 使用愈新的版本愈好
  • JavaScript 原生的語法總是比較快,因此盡量不要用 jQuery.each 改用 for-loop
    Note: 但是 for-in-loop 的效能通常還是比 jQuery.each
  • 除非必要否則盡量少用 jQuery,如 $(this).attr('id') vs. this.id

images

  • (CSS Sprite) 將多個圖檔合併,並以 CSS 控制背景顯示位置,如此可減少 request 數並減低顯示延遲
    Note: 但圖與圖之間的空白不宜太多,空白雖不致影響檔案大小,卻與記憶體消耗有關
  • 使用 Data-URI 可減少 request 數
  • 圖檔指定 width 及 height 屬性可避免瀏覽器做不必要的 repaint 與 reflow
  • 圖檔最佳化(減少不必要的資訊如 Exif metadata 等)
  • 網頁呈現檔案較大的 jpeg 時,使用 progressive jpeg 優於 baseline jpeg

server

  • 自訂 cache 時效,網頁中要更新時可改檔名或用 GET parameter 改版號(但此作法會造成在 proxy 中無效)
  • 使用 GZIP 壓縮資料

tools