我們現在要討論重要的觀念讓你能夠將 JavaScript 使用成其他程式語言無法匹敵的強大工具
這概念稱為一級函數(first class functions)
在 JavaScript, 函數就是物件
一級函數聽起來很複雜
其實沒有
一級函數表示在程式語言中 JavaScript 不是唯一有一級函數的程式語言
但它絕對是最受歡迎的
你可以對別的型別,如物件、字串、數值、布林做的事,你都可以對函數做
你可以指派一個變數的值為函數
你可以將函數當做參數傳入另一個函數
你可以用實體語法立刻創造函數
一級函數改變你寫程式的方式
它們可以讓你用一個完全不同的方法 解決你的問題
所以當我們說 JavaScript 的函數就是物件時
函數物件長的什麼樣子?
其實就像 JavaScript 的其他物件一樣 它在記憶體裡
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 34 影片截圖
它是一個特殊型態的物件,它有所有物件的特色 還有一些其他屬性
大家常在看到 JavaScript 的時候 常常會驚訝一件事
函數可以有屬性和方法
為什麼?
因為它就只是個物件
所以我可以連結到純值 到名稱/值配對
我可以連結到物件
我可以連結到其他函數
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 34 影片截圖
這在 JavaScript 是很有用的
等下我們會看到
在 JavaScript, 函數物件有一些隱藏版的特殊屬性
其中有兩個重要的,第一是名稱
JavaScript 的函數不一定要有名稱
一個函數可以是匿名的, 表示它沒有名字
但它可以有名字
它有另一個屬性稱為「程式屬性」(code property)
表示你寫的程式碼的所在
基本上,我們現在談的
你寫的程式會成為函數物件的特殊屬性
你寫的程式並非就是函數本身
這個函數是有其他屬性的物件
你寫的程式只是其中一種你加上去的屬性
這個屬性特別的是,它是可以呼叫的
代表你可以執行這個程式碼
執行這個函數的程式
這是當整個執行環境的創造和執行時會發生的
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 34 影片截圖
在整個 JavaScript 中,你必須把這個函數的模式放在心中
你必須把函數想像成物件
而它的程式碼是那個物件的屬性之一
還有許多其他東西函數能夠包含
還有許多其他事情函數可以做
它可以被移動、複製、傳入另一個東西
就像是其他型別(物件、字串或數字一樣
它可以做出一些很棒的事情
也可以用一些有趣的語法,但如果你不瞭解這個基本概念可能看起來十分怪異,
好的,為了證明,
來看點程式碼
程式碼如下:
function greet() {
console.log(hi);
}
greet.language = 'english';
console.log(greet.language);
我們說過在JavaScript中函數就是物件
所以我可以用點(成員取用)運算子創造屬性
我新增了一個函數的屬性
在其他程式語言中,這是不可能的
但在 JavaScript,函數就是物件,我寫的程式就只是這個物件的屬性
我們到 Console 中看結果:
你會很好奇,為什麼可以這樣?
因為在 JavaScript, 函數是什麼?
物件
所以當我建立這個函數,它看起來像什麼?
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 34 影片截圖
當這個 greet 函數被創造
這個函數物件會被放進記憶體,
這個情況中是全域物件,然後它有名稱
它的名稱是 greet 因為我幫這函數命名的
然後有程式屬性,這個函數的內容包含了我寫的程式碼
所以如果我用括號呼叫 greet
這會呼叫函數,讓它執行
讓執行環境被創造,以此類推
知道它怎樣運作了嗎?
你必須想像函數只是程式碼的容器
它是物件,所以你可到處使用
它在記憶體中的一個特定位置
它有屬性、它有方法
為什麼?
因為在 JavaScript 中函數就是物件
好的,現在我們要來看看這能讓我們的程式碼變的多有趣
現在你知道 JavaScript 裡面函數就是物件
現在我們要來看 JavaScript 如何用這個觀念 讓我們做一些很有趣又強大的事情
這個一級函數(first class function)的觀念
我們要先瞭解函數陳述句和函數表達式的用法差異
表達式是程式碼的單位,會回傳一個值
所以當我們說函數陳述句 陳述句會做某件事
但函數表達式
或任何表達式,最終會回傳(創造)一個值
而這個值不一定要儲存在某個變數
所以表達式是什麼?
表達式會回傳一個值
我有個程式碼的單位
而這個單位
基本上是一行程式 但它也可以是很多行程式作為一個單位
我有一個程式碼單位 設值為3
如下圖:
記得這個等號是運算子
一個接受兩個值的函數 然後做一些事之後回傳一個值
所以這會回傳 3,因為我把右邊的 3 傳入左邊的 a
然後它會回傳右邊的第二個參數
執行它,然後得到一個值為 3
因為等號運算子回傳一個值
這會設定這個值到記憶體中
但我可以執行一個不同的表達式 像是 1+2
如下圖:
這是有效的表達式
加號是運算子 會接受兩個值,相加回傳結果
我可以執行這個,然後它會回傳 3
但請注意, 我沒有用等號運算子設定這個值放進記憶體中
這就是表達式
也就是說,它執行完後回傳這個值、這個結果
但我沒有對這個值做任何事
但這兩個都是表達式
因為它回傳一個值
這行程式碼會形成一個值
這個值可以是數值或字串或物件 無論是什麼東西都可以
但它仍然是個值
所以我可以用將物件實體指派給變數 a,
如下圖:
它把這個放進記憶體中 然後等號運算子回傳一個值
反之,當我們說到陳述句
例如 if 陳述句 if ( a===3 ) 就做某件事
程式碼如下:
if(a === 3) {
// do something
}
在 if 陳述句的括號裡面你會放進表達式,因為這會形成一個值
但 if 陳述句本身就只是陳述句
因為它不會回傳任何值,所以稱為陳述句
我不行將變數b設定為if 因為這不會起作用
沒有值會被回傳
程式碼如下:
var b = if(a === 3) {
// do something
}
if 是一個陳述句 在括號裡面你可以有表達式
因為表達式,這個三等號 最終會回傳 true 或 false
所以陳述句會做其他事 表達式則會回傳一個值
在 JavaScript,因為函數是物件 我有函數陳述句和函數表達式,這兩個都很強大
我們來看看兩者的差異
先來看函數陳述句
程式碼如下:
function greet() {
console.log(hi)
}
當它被執行,它不會回傳值
這個函數被放進記憶體中,但它只是陳述句
它不會回傳值,直到函數被執行
所以這個函數陳述句會把函數放到記憶體中
這個程式碼單位不會回傳任何值
但它會做一些特別的事
我們已經看過了,它會被 Hoisting
在執行環境的創造階段
這個函數陳述句是在全域執行環境 它會放被進記憶體中
所以它可以被取用
我可以在宣告、建立函數陳述句之前在函數陳述句之前呼叫函數陳述句
程式碼如下:
greet();
function greet() {
console.log(hi)
}
這在記憶體中是可以取用的
它仍然是個物件
它的名稱是 greet,程式屬性是寫在裡面的程式碼,
現在我來看看函數表達式,
我寫一個變數名稱 命名它為 anonymousGreet
用等號(指派)運算子來將變數設定成新的值
等號運算子後面可以是一個數字或字串或物件
或使用物件實體語法
但我要用函數
程式碼如下:
var anonymousGreet = function() {
console.log('hi');
}
記得函數就是物件
所以我要建立一個物件 設定它等於這個變數
也就是這個變數,它在記憶體中指向的位址
它在記憶體中指向的位址,是這個函數物件
我們回到函數陳述句來看看這個函數的宣告 這個函數陳述句
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 35 影片截圖
這是被放進記憶體中的東西
當執行環境被創造
它有名稱屬性還有程式屬性 這些被設定好了
所以我在記憶體有個物件 函數(物件)的名稱是 greet,
這有一行程式碼
當我呼叫它
我們看一下 ConSole 的結果:
這會被連結到記憶體中的那個點 那個函數物件所在的地方
然後,呼叫函數的程式部分來執行程式屬性中的程式碼,
那函數表達式又如何呢?
這個例子中,我有等號運算子 它會把這個物件放到記憶體中
並且指向 anonymousGreet 變數的位址
圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 35 影片截圖
記得,這不是真正去看函數的名稱而是去看它連結到的記憶體位址
它知道記憶體中的 anonymousGreet 變數的位址裡面的值現在是這個匿名函數,
這個例子中,函數物件沒有名稱
因為我沒有在括號前面放任何東西 我也不需要
因為我有一個已經知道物件位址的變數
所以我不需要一個名稱去參照它
所以這是一個匿名函數
你可能聽過這個名稱
它就只有這樣而已
匿名函數就是沒有名稱屬性的函數
但這並沒有關係,因為你可以參照到它
利用指向物件位址的變數名稱
然後我們有相同的程式屬性 在這裡面的同一行程式碼
所以要如何觸發這個函數
我們需要指向那個物件
告訴它執行程式
變數已經指向了它的位置
當我放上括號,可以觸發程式
所以當我在這邊這樣做, anonymousGreet 加括號 執行它
這執行一樣的程式
表達式會回傳一個值
現在我們程式碼中來呼叫這個變數,
程式碼如下:
var anonymousGreet = function() {
console.log('hi');
}
anonymousGreet();
Console 的結果:
這與剛才的函數陳述句執行一樣的程式
會回傳一個值
所以這是表達式
我們在函數的括號前面不需要名稱因為很多此一舉
好的程式碼要很清楚易懂
盡量減少我們需要寫的程式數量
所以匿名函數可以達到這個目標
這很清楚表示我在創造一個函數物件
要瞭解如何觸發、呼叫、執行它並不難
如果我們現在修改一下呼叫函數的位置,
把呼叫函數調整到函數之前,
程式碼如下:
anonymousGreet();
var anonymousGreet = function() {
console.log('hi');
}
你猜這樣我們在 Console 中的結果為何?
我們看一下 Console 中的結果:
為什麼?
表示當 JavaScript 看到這邊有一個變數
將變數放進記憶體
然後在執行程式之前,它會把所有的變數都設值成什麼?
一種特殊的純值,叫作 undefined
因為我們還沒設定任何東西給它
我們只是試著呼叫它,就好像它是個函數一樣
而 JavaScript不同意
undefined 是純值不是函數
直到重新設定為函數物件才被創造
所以函數表達式在創造階段時只會將變數在記憶體空間中設值為 undefined
等到執行到等號運算子將函數指派給 anonymousGreet 變數時,
變數在記憶體空間的值才會變成我們寫的匿名函數,
好的,再做一件更酷的事
我要建立一個新的函數 用函數表達式log
只需要一個變數
變數 a
然後 console.log 傳入函數的東西
再呼叫這個函數
如果我傳入數字,會發生什麼事
我們在來看個例子
程式碼如下:
function log(a) {
console.log(a);
}
log(3);
我們到 Console 中看結果如下:
這個結果我們都知道,呼叫函數時我們可以把數字當作參數傳入,
也可以把字串等純值或物件傳入,
我們將參數改成物件,程式碼如下:
function log(a) {
console.log(a);
}
log({ greeting: "hi" });
Console 中結果如下:
也可將它們先存在一個變數中,改成只傳入變數當作參數
記得我們所學的
函數是物件
函數表達式可以馬上創造物件,一個函數物件
所以我可以做一些看起來怪異的事
我可以宣告函數,然後立刻創造一個函數
程式碼如下:
function log(a) {
console.log(a);
}
log(function() {
console.log('hi');
});
這看起來非常像物件實體
我立刻創造一個函數 寫一些程式碼,創造了物件
把這段程式放到那個函數的程式屬性
這就像字串、數字、物件
我只是建立函數,因為函數就是物件
所以這會被傳進函數
Console 中結果如下:
看到的函數是匿名的,但這可以用參數 a 參照到
這樣我們對函數的運作就比較清楚了
一級函數,可以被傳入別的地方
可以很快創造、使用, 變數也可以被設值成它們
所以如果我想要呼叫這個函數
a 參數現在指向它被創造時在記憶體的位置,
當我呼叫log函數
它把一個函數傳入另一個函數 這是你可以在 JavaScript 中做到的
現在 a 參數指向函數物件
我該如何讓它執行?
我要怎樣執行這個我創造的函數然後 log 出來?
只要用括號呼叫 a 參數就可以了
這告訴 JavaScript 引擎去執行這個函數
程式碼如下:
function log(a) {
a();
}
log(function() {
console.log('hi');
});
Console 中結果如下:
這個一級函數的觀念,你可以把函數傳入別處
把函數給另一個函數 就像使用變數一樣
因為 JavaScript 的函數就是物件
這個概念在程式語言中稱為函數程式設計 (functional programming)
之後會再談到