function 是 first-class object,可以像 value 一樣當作參數傳遞,也可以擴充其 property 及 method
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 = functionadd(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
functionadd(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
functionfoo(){
alert('global foo');
}
functionbar(){
alert('global bar');
}
functionhoistMe(){
console.log(typeof foo); // "function"
console.log(typeof bar); // "undefined"
foo(); // "local foo"
bar(); // TypeError: bar is not a function
// function declaration: 宣告及實作都會被 hoisted
functionfoo(){
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,會導致非預期的行為。
即 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
functionadd(x, y){
if (typeof y === "undefined") { // partial
returnfunction(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 可用,實作方式較為複雜,需要時可再查閱。
因為 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 = newObject();
console.log(obj.constructor === Object); // true
// number object
var obj = newObject(1);
console.log(obj.constructor === Number); // true
console.log(obj.toFixed(3)); // "1.000"
// string object
var obj = newObject("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 = newObject(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(){
returnthis.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(){
returnthis.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(){
returnthis.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
functionPen(){
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)
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(){
returnthis;
}());
使用 global variable 可能發生命名衝突(例如有多個 libraries 或多段不同人寫的 code 時),應盡可能少用
chain assignment 也會建立 global variable
1
2
3
4
5
functionfoo(){
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
functionfoo(){
console.log(bar); // "undefined"
var bar = "local";
console.log(bar); // "local"
}
foo();
// 上例相當於以下行為
bar = "global"; // global variable
functionfoo(){
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
functionfoo(){
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