函式 Functions 的基本概念

函式是物件的一種

除了基本型別以外的都是物件 (Object)

當我們透過 typeof 去檢查一個「函式 (function) 」的時候,雖然你會得到 “function” 的結果,讓你以為 function 也是 JavaScript 定義的一種型別,但實際上它仍屬於 Object 的一種。

你可以把它想像成是一種可以被呼叫 (be invoked) 的特殊物件 (值)。

定義函式的方式

  • 函式宣告(Function Declaration)
  • 函式運算式(Function Expressions)
  • 透過 new Function 關鍵字建立函式

函式宣告(Function Declaration)

「函式宣告」應該是屬於最常見的用法:

1
2
3
4
5
6
7
function 名稱([參數]) {
// 做某事
}

function square(number) {
return number * number;
}

函式運算式(Function Expressions)

透過 變數名稱 = function([參數]){ … }; 的方式,將一個函式透過 = 指定給某個變數。

像這樣:

1
2
3
4
var square = function (number) {
console.log( typeof square ); // "function"
return number * number;
};

沒有名字的函式?

在範例裡 = 後面的 function 是「沒有名字」的
像這類沒有名字的函式在 JavaScript 是合法的,通常我們會稱它為「匿名函式」。

在「匿名函式」的函式運算式情況下,你還是可以透過自定義的變數名稱取得 function,沒有一定要替這個函式取名的理由

在函式運算式中,如果想要在 function 後面加上一個名字是可以的嗎?

可以,像這樣:

1
2
3
var square = function func(number) {
return number * number;
};

但是要注意的是,這個名字只在「自己函式的區塊內」有效

也就是說:

1
2
3
4
5
6
var square = function func(number) {
console.log( typeof func ); // "function"
return number * number;
};

console.log( typeof func ); // undefined

像這樣,脫離了函式自身區塊後,變數 func 就不存在了。

透過 new Function 關鍵字建立函式

直接使用 Function (注意 F 大寫) 這個關鍵字來建立函式物件。 使用時將參數與函式的內容依序傳入 Function,就可以建立一個函式物件了。

1
2
// 透過 new 來建立 Function "物件"
var square = new Function('number', 'return number * number');

透過 new Function 所建立的函式物件,每次執行時都會進行解析「字串」(如 ‘return number * number’ ) 的動作,運作效能較差,所以通常實務上也較少會這樣做。

變數的有效範圍 (Scope)

終於要講到全域變數與區域變數的差異了。

在 ES6 之前,JavaScript 變數有效範圍的最小單位是以 function 做分界的。 [註2]
什麼意思呢? 讓我用簡單的範例來說明:

[註2] ES6 之後有 let 與 const 分別定義「變數」與「常數」。 與 var 不同的是,它們的 scope 是透過大括號 { } 來切分的。

1
2
3
4
5
6
7
8
9
var x = 1;

var doSomeThing = function(y) {
var x = 100;
return x + y;
};

console.log( doSomeThing(50) ); // ?
console.log( x ); // ?

猜猜看,這兩組 console.log() 分別會印出什麼?

.
.
.

答案是 1501

由於函式 doSomeThing() 裡面再次定義了變數 x,所以當我們執行 doSomeThing(50) 時,會將 50 作為參數傳入 doSomeThing()y,那麼 return x + y 的結果自然就是 100 + 50150 了。

那麼下一行再印出的 x 呢? 為什麼是 1 而不是 100 ?
因為…

「切分變數有效範圍的最小單位是 “function” 」
「切分變數有效範圍的最小單位是 “function” 」
「切分變數有效範圍的最小單位是 “function” 」

很重要,所以要講三次。

因為切分變數有效範圍的最小單位是 “function”,所以在函式區塊內透過 var 定義的 x 實際上只屬於這個函式。

換句話說,外面的 xfunction 內的 x 其實是兩個不同的變數。

因此在最後印出來的 console.log( x ); 自然就是外面的 x 也就是 1 了。

所以我們說,變數有效範圍的最小單位是 “function”, 這個有效範圍我們通常稱它為「Scope」。


那麼,如果 function 內部沒有 var x 呢?
很簡單,自己的 function 內如果找不到,就會一層層往外找,直到全域變數為止:

1
2
3
4
5
6
7
8
var x = 1;
var doSomeThing = function(y) {
// 內部找不到 x 就會到外面找,直到全域變數為止。
// 都沒有就會報錯:ReferenceError: x is not defined
return x + y;
};

console.log( doSomeThing(50) ); // 51

要注意的是, function 可以讀取外層已經宣告的變數,但外層拿不到裡面宣告的變數。

沒有 var 宣告的變數很危險!
「沒有 var 宣告的變數很危險」什麼意思?

來,稍微修改一下剛剛的範例,把 function 內的 var 拿掉:

1
2
3
4
5
6
7
8
9
var x = 1;

var doSomeThing = function(y) {
x = 100;
return x + y;
};

console.log( doSomeThing(50) ); // ?
console.log( x ); // ?

猜猜看,這兩組 console.log() 分別會印出什麼?

.
.
.

答案是 150100

先別急著崩潰,剛剛說過「切分變數有效範圍的最小單位是 “Function” 」對吧?
但這句話的前提是你得在 function 內部再次用 var 宣告這個變數,否則 JavaScript 會再往外層去找到同名的變數,直到最外層,也就是「全域變數」。

換言之,由於在 function 內沒有重新宣告 x 變數,使得 x = 100 跑去變更了外層的同名變數 x:

1
2
3
4
var doSomeThing = function(y) {
x = 100;
return x + y;
};

導致在呼叫 doSomeThing(50) 之後再印出 x 的值自然就變成 100 囉。

宣告式 v.s. 表達式:命名與 Hoisiting

  • 陳述式:會執行一些程式碼,可能是幾個單詞或是一個片段(但不會是單一個字母),但最大的特徵是不會回傳結果。
  • 表達式:最大的特徵在於會回傳結果

函式的 name 屬性

對函式宣告式和具名函式表達式,name 屬性都有定義。對匿名函式表達式說,name 可能為 undefined 或空字串。
name 屬性也會用來遞迴呼叫自己,或是在除錯工具中顯示函式名稱,如果這兩種狀況都沒有需要,用不具名表達式會比較簡單也不囉唆。

函式的 Hoisting

這兩種定義方式最大的差別在於,透過「函式宣告」方式定義的函式可以在宣告前使用 (函式提升):

1
2
3
4
5
square(2);    // 4

function square(number) {
return number * number;
}

而透過「函式運算式」定義的函式則是會出現錯誤:

1
2
3
4
5
square(2);    // TypeError: square is not a function

var square = function (number) {
return number * number;
};

與變數提升的差別在於變數提升只有宣告式被提升,而函式的提升則是包括內容完全被提升。 除了可呼叫的時機不同外,「函式宣告」與「函式運算式」在執行時期兩者無明顯差異。


參考資料

重新認識 JavaScript: Day 10 函式 Functions 的基本概念

Day03:JavaScript 設計模式 第四章函式 筆記精要(上)