函式 Functions 的基本概念
函式是物件的一種
除了基本型別以外的都是物件 (Object)
當我們透過 typeof 去檢查一個「函式 (function) 」的時候,雖然你會得到 “function” 的結果,讓你以為 function 也是 JavaScript 定義的一種型別,但實際上它仍屬於 Object 的一種。
你可以把它想像成是一種可以被呼叫 (be invoked) 的特殊物件 (值)。
定義函式的方式
- 函式宣告(Function Declaration)
- 函式運算式(Function Expressions)
- 透過 new Function 關鍵字建立函式
函式宣告(Function Declaration)
「函式宣告」應該是屬於最常見的用法:
1 | function 名稱([參數]) { |
函式運算式(Function Expressions)
透過 變數名稱 = function([參數]){ … }; 的方式,將一個函式透過 =
指定給某個變數。
像這樣:
1 | var square = function (number) { |
沒有名字的函式?
在範例裡 = 後面的 function 是「沒有名字」的
像這類沒有名字的函式在 JavaScript 是合法的,通常我們會稱它為「匿名函式」。
在「匿名函式」的函式運算式情況下,你還是可以透過自定義的變數名稱取得 function,沒有一定要替這個函式取名的理由
在函式運算式中,如果想要在 function 後面加上一個名字是可以的嗎?
可以,像這樣:
1 | var square = function func(number) { |
但是要注意的是,這個名字只在「自己函式的區塊內」有效
也就是說:
1 | var square = function func(number) { |
像這樣,脫離了函式自身區塊後,變數 func 就不存在了。
透過 new Function 關鍵字建立函式
直接使用 Function (注意 F 大寫) 這個關鍵字來建立函式物件。 使用時將參數與函式的內容依序傳入 Function,就可以建立一個函式物件了。
1 | // 透過 new 來建立 Function "物件" |
透過 new Function 所建立的函式物件,每次執行時都會進行解析「字串」(如 ‘return number * number’ ) 的動作,運作效能較差,所以通常實務上也較少會這樣做。
變數的有效範圍 (Scope)
終於要講到全域變數與區域變數的差異了。
在 ES6 之前,JavaScript 變數有效範圍的最小單位是以 function 做分界的。 [註2]
什麼意思呢? 讓我用簡單的範例來說明:
[註2] ES6 之後有 let 與 const 分別定義「變數」與「常數」。 與 var 不同的是,它們的 scope 是透過大括號 { } 來切分的。
1 | var x = 1; |
猜猜看,這兩組 console.log() 分別會印出什麼?
.
.
.
答案是 150 與 1。
由於函式 doSomeThing() 裡面再次定義了變數 x,所以當我們執行 doSomeThing(50) 時,會將 50 作為參數傳入 doSomeThing() 的 y,那麼 return x + y 的結果自然就是 100 + 50 的 150 了。
那麼下一行再印出的 x 呢? 為什麼是 1 而不是 100 ?
因為…
「切分變數有效範圍的最小單位是 “function” 」
「切分變數有效範圍的最小單位是 “function” 」
「切分變數有效範圍的最小單位是 “function” 」
很重要,所以要講三次。
因為切分變數有效範圍的最小單位是 “function”,所以在函式區塊內透過 var 定義的 x 實際上只屬於這個函式。
換句話說,外面的 x 跟 function 內的 x 其實是兩個不同的變數。
因此在最後印出來的 console.log( x ); 自然就是外面的 x 也就是 1 了。
所以我們說,變數有效範圍的最小單位是 “function”, 這個有效範圍我們通常稱它為「Scope」。
那麼,如果 function 內部沒有 var x 呢?
很簡單,自己的 function 內如果找不到,就會一層層往外找,直到全域變數為止:
1 | var x = 1; |
要注意的是, function 可以讀取外層已經宣告的變數,但外層拿不到裡面宣告的變數。
沒有 var 宣告的變數很危險!
「沒有 var 宣告的變數很危險」什麼意思?
來,稍微修改一下剛剛的範例,把 function 內的 var 拿掉:
1 | var x = 1; |
猜猜看,這兩組 console.log() 分別會印出什麼?
.
.
.
答案是 150 與 100。
先別急著崩潰,剛剛說過「切分變數有效範圍的最小單位是 “Function” 」對吧?
但這句話的前提是你得在 function 內部再次用 var 宣告這個變數,否則 JavaScript 會再往外層去找到同名的變數,直到最外層,也就是「全域變數」。
換言之,由於在 function 內沒有重新宣告 x 變數,使得 x = 100 跑去變更了外層的同名變數 x:
1 | var doSomeThing = function(y) { |
導致在呼叫 doSomeThing(50) 之後再印出 x 的值自然就變成 100 囉。
宣告式 v.s. 表達式:命名與 Hoisiting
- 陳述式:會執行一些程式碼,可能是幾個單詞或是一個片段(但不會是單一個字母),但最大的特徵是不會回傳結果。
- 表達式:最大的特徵在於會回傳結果
函式的 name 屬性
對函式宣告式和具名函式表達式,name 屬性都有定義。對匿名函式表達式說,name 可能為 undefined 或空字串。
name 屬性也會用來遞迴呼叫自己,或是在除錯工具中顯示函式名稱,如果這兩種狀況都沒有需要,用不具名表達式會比較簡單也不囉唆。
函式的 Hoisting
這兩種定義方式最大的差別在於,透過「函式宣告」方式定義的函式可以在宣告前使用 (函式提升):
1 | square(2); // 4 |
而透過「函式運算式」定義的函式則是會出現錯誤:
1 | square(2); // TypeError: square is not a function |
與變數提升的差別在於變數提升只有宣告式被提升,而函式的提升則是包括內容完全被提升。 除了可呼叫的時機不同外,「函式宣告」與「函式運算式」在執行時期兩者無明顯差異。