函式的定義
JavaScript 函式定義有 3 種方式,分別在效率、解析順序、作用域有不同的特性。
// 1. 語句函式
function fun1(){ };
// 2. 函式賦值變數
var test = function(){ };
// 3. 建構式函數
var test2 = new Function();
編譯順序
JavaScript 在執行程式之前,會先對 語句函式
進行解析,再去一行一行執行程式,所以第一種 語句函式
,在被呼叫之前,就會先解析好並儲存在內存中。
- 以第一種宣告方式,由於函式會先行解析好,所以若是把呼叫函式的指令,寫在函式的程式碼上面,是可以成功呼叫到函式的。
- 以第二種宣告方式
函式賦值變數
,系統會先判讀有宣告變數 test 但還不會賦值。只有執行到該行程式碼時,才會把函式賦值給變數。所以若是把呼叫函式的指令,寫在函式的程式碼上面,系統會判斷為 undefined
fun1()
// 第一種宣告方式,可以先呼叫函式,再宣告。
function fun1() { };
test()
// 第二種宣告方式,會判斷 test is not a function
var test = function() { };
解析範例
看看以下經典範例來說明解析步驟:
- JavaScript解析器,會首先找到第一個test1( ) → 1
- 接下來找到第四個test1( ),並蓋掉第一個test1( ) → 4
- 再後來沒有function開頭語句了,所以才從頭一行一行執行並回傳
- 最後依序回傳 4, 2, 3, 3, 5, 6
function test1() { return 1; }
alert(test1()); // 4 → 被第四個 test1() 蓋掉
var test1 = function() { return 2; };
alert(test1()); // 2
var test1 = new Function("return 3");
alert(test1()); // 3
function test1() { return 4; }
alert(test1()); // 3 → 第四個已經被解析過了所以跳過
var test1 = function() { return 5; };
alert(test1()); // 5
var test1 = new Function("return 6");
alert(test1()); // 6
執行效率
以效率方面來比較,相同的執行內容,所需的執行時間,大約是 1 = 2 < 3
- 第一種及第二種方式,由於編譯完成後就會放在內存,使用時再呼叫,所以很適合多次呼叫的情形。
- 第三種方式
建構式函式
,使用後會被釋放掉,不會佔用內存,所以再一次呼叫時,要重新 new 一次,僅適合一次性呼叫。
作用域
- 第一、二種方法具有函式作用域,return 時會得到宣告在函式內部的區域變數
- 第三種
建構函式
方法具有頂級作用域,在函式中宣告的變數,會屬於全域變數。所以 return 時也會去抓全域變數的值。(這部分不是很理解,先暫時記住此特性)
var k = 1; //全域變數
function fun(){
var k = 2; //區域變數
// 函式作用域底下 (return 時會回傳 2)-----
function test(){ return k; }
var test = function() { return k; };
// 頂級作用域底下 (return 時會回傳 1)-----
var test = new Function("return k;");
alert(test());
}
fun();
三種宣告方式比較
類型 | 語句函式 | 函式賦值變數 | 建構式函式 |
---|---|---|---|
名稱 | 具名 | 匿名 | 匿名 | |
性質 | 靜態 | 靜態 | 動態 |
解析時機 | 優先解析 | 順序解析 | 順序解析 |
多次呼叫效率 | 佳 | 佳 | 差 |
作用域 | 函式作用域 | 函式作用域 | 頂級作用域 |
調用函式
- 這部分現在看不懂,先記下來,以後可能會用到
執行函式,是在函式被調用時才開始,非函式定義時,Javascript有四種調用方式
- 作為函式(function)
- 作為方法(methods)
- 作為建構式(constructors)
- 透過 cell() 與 apply() 方法間接調用
方法調用與函式調用有一個重要的差異:調用情境,即函式主體的this
會視呼叫它的物件而定:
- 當方法:其調用物件
- 當函式:
- strict 模式:全域物件
- 非 strict 模式:undifined
return
函式中的 return 指令會使函式停止執行,並回傳運算結果(如果有的話),如果 return 的回傳值沒有東西,就會回傳 undifined。
- 並非所有函式都有 return
選擇性參數
函式調用的 引數比參數少 時,額外參數會被設為 undifined,我們可以為 可能不存在 的參數設定 預設值。
非必須的引數,需要寫在引數列的後面。
var obj1 = {a:"1", b:"2", c:"3"};
var obj2 = {d:"1", e:"2", f:"3"};
function getPropertyNames(o,a) { // o → 必須 ; a → 可以忽略
a = a || []; // 如果a不存在就建一個新陣列
for(var property in o) a.push(property); // 把o的特性值丟進a陣列
return a;
}
var newObj = getPropertyNames(obj1); // 把obj1特性丟進新建的newObj
getPropertyNames(obj2, newObj); // 把obj2特性也丟進newObj
console.log(newObj); // ["a", "b", "c", "d", "e", "f"]
引數 Argument
JavaScript 提供了一個很方便的指令,可以讓我們查看傳入函式的 引數
是什麼。
- Argument 是一種類陣列物件,可以用 length 來看長度,實際上 log 出來看,它其實是一個物件形式。
// 抓出最大值
Max(1,5,100,20); // 100
function Max() {
var max = 0;
var num = arguments; // 把引數陣列丟進num
// num.length → 4
// num[0] → 1 ; num[1] → 5; num[2] → 100...
for (var i=0; i<num.length; i++) {
if (max < num[i]) max = num[i]; // 當前元素大於max時,max更新
}
return max;
}
使用物件作為引數
當函式的參數比較多,可以利用物件 key: value 的特性,讓函式直接接收一個物件。
var a = [1,2,3,4], b = [];
copy({from:a, to:b, length:4});
function copy(obj) {
// obj.from → [1, 2, 3, 4]
// obj.form_start || 30 → 30
// obj.to → []
// obj.to_start: obj.to_start || 20 → 20
// obj.length → 4
}
函式作為值
- 函式也可以當做「值」,這代表他們可以被指定給變數、物件特性或陣列元素中,或是傳給函式當作引數用。
// 計算式
function add(x,y) { return x+y; }
function subtract(x,y) {return x-y; }
// 調用上面運算式做為第一個引數
function operate(method, num1, num2) {
return method(num1, num2);
}
// a = 5+10
var a = operate(add, 5, 10);
// c = (10-5) + (2-1)
var c = operate(add,
operate(subtract, 10, 5),
operate(subtract, 2, 1));
函式的物件特性
函式是一種物件,代表也有特性可以存取,假設你要寫一個每次調用都回傳不同值的函式,且都要記錄上次存取的值,我們可以把資訊直接存在 函式特性 上
addCount.count = 0;
function addCount() {
return addCount.count++;
}
addCount();
addCount();
addCount();
console.log( addCount.count ); // 3
call & apply方法
.call()
與 .apply()
可以用來傳遞參數用,但更重要的功能是用來間接調用某函式,當作是某物件的方法。
- 第一個引數是指出要 在哪個物件上調用,在函式內容中成為this
- call:任何多個引數 fun.call(object, num1, num2)
- apply:一組陣列當引數 fun.apply(object, [num1, num2])
//傳遞參數用--------------
function sum(x,y) {
return x+y;
}
// call
function callTest(num1, num2) {
return sum.call(this, num1, num2);
}
// apply
function applyTest(num1, num2) {
return sum.apply(this, [num1, num2]);
}
/* apply 也可以改寫用 arguments
function applyTest() {
return sum.apply(this, arguments);
}
*/
callTest(10,20); //30
applyTest(10,50); //60
間接調用函式
//間接調用函式,當做是物件的方法--------------
function sum(x,y) {
return x+y;
}
function Obj(x,y) {
this.x = x;
this.y = y;
}
var o = new Obj();
//方法.apply(作用域, [參數陣列])
sum.apply(o, [10,100]); // 110
Closures 閉包
函式執行完畢後,變數會被釋放掉,但是我們可以利用在函式中宣告另一個函式,並把它 return 出去,來 keep 住我們要保留的變數資料
EC
: Execution Context,每個進入一個 工作 BLOCK,會產生一個新的 EC,每次進入一個新的 EC,Scope chain 就會被建立。
VO
: Variable Object,在建立 Global EC 的時候,用來紀錄初始化變數的地方。
Global EC: {
VO: {
str: undefined
fun1: function
}
}
AO
: Activation Object,用來在新增 function EC 的時候,紀錄初始化變數的地方。跟 Variable Object 很像,差別只在 AO 是在新稱 function EC 的時候產生。
function EC: {
AO: {
arr: undefined
fun2: function
}
scopeChain: [function EC.AO, [Scope]]
}
Block 運作方式:
// 請先記住這段程式碼!! 下面會針對這段程式碼來做說明
var a = 1
function test() {
var b = 2
function inner() {
var c = 3
console.log(b)
console.log(a)
}
return inner()
}
test()
模擬 JS 引擎:
- 程式開始執行,產生
Global EC
,並初始化VO
,建立自己的ScopeChain
Global EC: { VO: { a: undefined test: function } ScopeChain: [global EC.VO] } // test.[[scope]] 初始化 test.[[Scope]] = global EC.ScopeChain // global EC.VO
- 開始跑程式,跑到
var a=1
的時候,將 global EC.VO.a 改成 1 - 進入 test function 之後,產生一個
test EC
並初始化AO
test EC: { AO: { b: undefined inner: function } ScopeChain: [test EC.AO, test.[[scope]]] // test EC.AO, global EC.ScopeChain } // inner.[[scope]] 初始化 inner.[[scope]] = test EC.ScopeChain // test EC.AO, global EC.ScopeChain
- 執行到
var b=2
的時候,將 test EC.AO.b 改成 2 - 進入 inner 之後,產生一個
inner EC
,初始化AO
inner EC: { AO: { c: undefined } ScopeChain: [inner EC.AO, inner.[[scope]]] // inner EC.AO, test EC.ScopeChain // inner EC.AO, test EC.AO, global EC.VO }
- 執行到
var c=3
的時候,將 inner EC.AO.c 改成 3 - 執行
console.log(b)
會先在inner EC.AO
找,找不到再去test EC.AO
找,找到了 b,就 log 出 b = 2 - 執行
console.log(a)
會先在inner EC.AO
找,找不到再去test EC.AO
找,找不到再去global EC.VO
找,找到了,就 log 出 a = 1
ScopeChain
說穿了就是從自己所在的 Executin Context,開始一路把到 global EC 過程中的 AO 及 VO 串通通起來。
因為內部的函式,它的 ScopeChain 含有外層函式的 AO,所以內部函式被 return 出去,系統不能把外層函式的 AO 釋放掉,因而保留了變數的資料。
Closure
就是函式結束後要將其釋放,卻因為有保留 ScopeChain,所以即便已經結束函式了,還是可以存取到函式裡的值。
參考資料:JavaScript大全(第六版)