函數是可以呼叫的值。定義函數的方法之一稱為函數宣告。例如,下列程式碼定義函數 id
,它有一個參數 x
:
function
id
(
x
)
{
return
x
;
}
return
陳述式會傳回 id
的值。您可以透過提及函數名稱,後接括號中的引數來呼叫函數:
> id('hello') 'hello'
如果您沒有從函數傳回任何內容,則會(隱式地)傳回 undefined
> function f() { } > f() undefined
本節只展示定義函數和呼叫函數的一種方法。其他方法會在後續說明。
您可以直接呼叫函數。然後它會作為一般函數運作。以下是呼叫範例:
id
(
'hello'
)
根據慣例,一般函數的名稱以小寫字母開頭。
您可以透過 new
算子呼叫函數。然後它會變成建構函數,一個物件工廠。以下是呼叫範例:
new
Date
()
根據慣例,建構函數的名稱以大寫字母開頭。
您可以將函數儲存在物件的屬性中,這會將它變成方法,您可以透過該物件呼叫它。以下是呼叫範例:
obj
.
method
()
根據慣例,方法的名稱以小寫字母開頭。
非方法函數會在本章說明;建構函數和方法會在第 17 章說明。
術語參數和引數通常可以互換使用,因為從脈絡中通常可以清楚了解預期的意思。以下是可以區分它們的經驗法則。
參數 用於定義函數。它們也稱為形式參數和形式引數。在以下範例中,param1
和 param2
是參數:
function
foo
(
param1
,
param2
)
{
...
}
引數 用於呼叫函數。它們也稱為實際參數和實際引數。在以下範例中,3
和 7
是引數:
foo
(
3
,
7
);
Function()
所有函數都是物件,Function
的執行個體
function
id
(
x
)
{
return
x
;
}
console
.
log
(
id
instanceof
Function
);
// true
因此,函數從 Function.prototype
取得其方法。
var
add
=
function
(
x
,
y
)
{
return
x
+
y
};
console
.
log
(
add
(
2
,
3
));
// 5
前述程式碼將函數表達式的結果指定給變數 add
,並透過該變數呼叫它。函數表達式產生的值可以指定給變數(如最後一個範例所示),傳遞為另一個函數的引數,等等。由於一般函數表達式沒有名稱,因此它們也稱為 匿名函數表達式。
您可以為函數表達式命名。 命名函數表達式 允許函數表達式參照它自己,這對於自我遞迴很有用:
var
fac
=
function
me
(
n
)
{
if
(
n
>
0
)
{
return
n
*
me
(
n
-
1
);
}
else
{
return
1
;
}
};
console
.
log
(
fac
(
3
));
// 6
命名函數表達式的名稱只能在函數表達式內使用
var
repeat
=
function
me
(
n
,
str
)
{
return
n
>
0
?
str
+
me
(
n
-
1
,
str
)
:
''
;
};
console
.
log
(
repeat
(
3
,
'Yeah'
));
// YeahYeahYeah
console
.
log
(
me
);
// ReferenceError: me is not defined
以下是 函數宣告:
function
add
(
x
,
y
)
{
return
x
+
y
;
}
前述內容看起來像函數表達式,但它是一個陳述式(請參閱 表達式與陳述式)。它大致等於下列程式碼
var
add
=
function
(
x
,
y
)
{
return
x
+
y
;
};
換句話說,函數宣告宣告一個新變數,建立一個函數物件,並將其指定給變數。
建構函式 Function()
會評估儲存在字串中的 JavaScript 程式碼。例如,下列程式碼等同於前一個範例:
var
add
=
new
Function
(
'x'
,
'y'
,
'return x + y'
);
不過,這種定義函式的寫法很慢,而且會將程式碼保留在字串中(工具無法存取)。因此,如果可行,最好使用函式運算式或函式宣告。 使用 new Function() 評估程式碼 會更詳細地說明 Function()
;它的運作方式類似於 eval()
。
提升 意指「移到範圍的開頭」。函式宣告會完全提升,變數宣告只會部分提升。
函式宣告會完全提升。這讓您可以在宣告函式之前呼叫它
foo
();
function
foo
()
{
// this function is hoisted
...
}
前述程式碼之所以會運作,是因為 JavaScript 引擎會將 foo
的宣告移到範圍的開頭。它們執行程式碼時,就像程式碼看起來像這樣
function
foo
()
{
...
}
foo
();
var
宣告也會提升,但只有宣告會提升,不會提升使用宣告進行的指定。因此,使用 var
宣告和函式運算式,類似於前一個範例,會導致錯誤:
foo
();
// TypeError: undefined is not a function
var
foo
=
function
()
{
...
};
只有變數宣告會提升。引擎會將前述程式碼執行為
var
foo
;
foo
();
// TypeError: undefined is not a function
foo
=
function
()
{
...
};
大多數 JavaScript 引擎支援函式物件的非標準屬性 name
。函式宣告有這個屬性:
> function f1() {} > f1.name 'f1'
匿名的函式運算式的名稱是空字串:
> var f2 = function () {}; > f2.name ''
> var f3 = function myName() {}; > f3.name 'myName'
函式名稱有助於偵錯。有些人會因此始終為函式運算式命名。
您應該偏好下列哪一種函式宣告?
function
id
(
x
)
{
return
x
;
}
還是 var
宣告加上函式運算式的等效組合?
var
id
=
function
(
x
)
{
return
x
;
};
它們基本上相同,但函式宣告相較於函式表達式有兩個優點:
call()
、apply()
和 bind()
是所有函式都具有的方法(請記住,函式是物件,因此具有方法)。它們可以在呼叫方法時提供 this
的值,因此主要在物件導向的環境中令人感興趣(請參閱設定 this 時呼叫函式:call()、apply() 和 bind())。本節說明非方法的兩個使用案例。
這個方法在呼叫函式 func
時使用 argArray
的元素作為引數;亦即,下列兩個表達式是等效的:
func
(
arg1
,
arg2
,
arg3
)
func
.
apply
(
null
,
[
arg1
,
arg2
,
arg3
])
thisValue
是執行 func
時 this
的值。在非物件導向的設定中不需要它,因此這裡是 null
。
apply()
在函式以陣列方式(但不是陣列)接受多個引數時很有用。
感謝 apply()
,我們可以使用 Math.max()
(請參閱其他函式)來判斷陣列中的最大元素
> Math.max(17, 33, 2) 33 > Math.max.apply(null, [17, 33, 2]) 33
這會執行部分函式應用,會建立一個新的函式,呼叫 func
,其中 this
設定為 thisValue
,後續引數:首先是 arg1
,直到 argN
,然後是新函式的實際引數。在下列非物件導向的設定中不需要 thisValue
,因此它是 null
。
在此,我們使用 bind()
建立一個新的函式 plus1()
,它類似於 add()
,但只需要參數 y
,因為 x
永遠是 1
function
add
(
x
,
y
)
{
return
x
+
y
;
}
var
plus1
=
add
.
bind
(
null
,
1
);
console
.
log
(
plus1
(
5
));
// 6
換句話說,我們建立了一個等同於以下程式碼的新函式
function
plus1
(
y
)
{
return
add
(
1
,
y
);
}
JavaScript 不強制函式的元數:您可以使用任意數量的實際參數呼叫它,而與已定義的形式參數無關。因此,實際參數和形式參數的數量可能以兩種方式不同:
arguments
(稍後討論)擷取。
undefined
。
特殊變數 arguments
僅存在於函式(包括方法)中。它是一個陣列類似物件,包含目前函式呼叫的所有實際參數。以下程式碼使用它:
function
logArgs
()
{
for
(
var
i
=
0
;
i
<
arguments
.
length
;
i
++
)
{
console
.
log
(
i
+
'. '
+
arguments
[
i
]);
}
}
以下是互動
> logArgs('hello', 'world') 0. hello 1. world
arguments
具有以下特徵
它類似陣列,但不是陣列。一方面,它具有屬性 length
,而且 個別參數可以用索引讀取和寫入。
另一方面,arguments
不是陣列,它只類似陣列。它沒有任何陣列方法(slice()
、forEach()
等)。幸運的是,您可以借用陣列方法或將 arguments
轉換為陣列,如 類似陣列物件和一般方法 中所說明。
它是一個物件,因此可以使用所有物件方法和運算子。例如,您可以使用 in
運算子(屬性的反覆運算和偵測)檢查 arguments
是否「具有」給定的索引
> function f() { return 1 in arguments } > f('a') false > f('a', 'b') true
你可以使用 hasOwnProperty()
(屬性的迭代和偵測)以類似的方式:
> function g() { return arguments.hasOwnProperty(1) } > g('a', 'b') true
嚴格模式會捨棄 幾個 arguments
比較不常見的功能:
arguments.callee
參照目前的函式。它主要用於在匿名函式中進行自我遞迴,且在嚴格模式中不被允許。作為解決方法,請使用命名函式表達式(請參閱 命名函式表達式),它可以透過其名稱參照自身。
在非嚴格模式中,如果你變更參數,arguments
會保持最新狀態
function
sloppyFunc
(
param
)
{
param
=
'changed'
;
return
arguments
[
0
];
}
console
.
log
(
sloppyFunc
(
'value'
));
// changed
但在嚴格模式中不會進行此類更新
function
strictFunc
(
param
)
{
'use strict'
;
param
=
'changed'
;
return
arguments
[
0
];
}
console
.
log
(
strictFunc
(
'value'
));
// value
arguments
(例如,透過 arguments++
)。仍然允許指派給元素和屬性。
有三個 方法可以找出參數是否遺失。首先,你可以檢查它是否為 undefined
:
function
foo
(
mandatory
,
optional
)
{
if
(
mandatory
===
undefined
)
{
throw
new
Error
(
'Missing parameter: mandatory'
);
}
}
其次,你可以將參數解釋為布林值。然後 undefined
會被視為 false
。然而,有一個警告:其他幾個值也會被視為 false
(請參閱 真值和假值),因此檢查無法區分,例如,0
和遺失的參數
if
(
!
mandatory
)
{
throw
new
Error
(
'Missing parameter: mandatory'
);
}
第三,你也可以檢查 arguments
的長度,以強制執行最小參數個數:
if
(
arguments
.
length
<
1
)
{
throw
new
Error
(
'You need to provide at least 1 argument'
);
}
最後一種方法與其他方法不同
foo()
和 foo(undefined)
。在兩種情況下,都會擲回例外。
foo()
擲回例外,並將 optional
設定為 undefined
以供 foo(undefined)
使用。
如果一個參數是可選的,表示如果它不存在,你會給它一個預設值。類似於強制參數,有四種選擇。
首先,檢查 undefined
function
bar
(
arg1
,
arg2
,
optional
)
{
if
(
optional
===
undefined
)
{
optional
=
'default value'
;
}
}
其次,將 optional
解釋為布林值
if
(
!
optional
)
{
optional
=
'default value'
;
}
第三,你可以使用或運算子 ||
(請參閱 邏輯或 (||)),如果它不是假值,它會傳回左運算元。否則,它會傳回右運算元
// Or operator: use left operand if it isn't falsy
optional
=
optional
||
'default value'
;
第四,你可以透過 arguments.length
檢查函式的項數
if
(
arguments
.
length
<
3
)
{
optional
=
'default value'
;
}
再次強調,最後一種方法與其他方法不同
bar(1, 2)
和 bar(1, 2, undefined)
。在兩種情況下,optional
都是 '預設值'
。
bar(1, 2)
的 optional
設為 '預設值'
,而將 bar(1, 2, undefined)
的 optional
保留為 undefined
(即不變更)。
另一個可能性是將可選參數作為 命名參數 傳入,例如物件文字的屬性(請參閱 命名參數)。
在 JavaScript 中,你無法傳遞參考參數;也就是說,如果你將變數傳遞給函式,它的值會被複製並傳遞給函式(傳遞值)。因此,函式無法變更變數。如果你需要這麼做,你必須包裝變數的值(例如,放入陣列中)。
這個範例示範一個會遞增變數的函式
function
incRef
(
numberRef
)
{
numberRef
[
0
]
++
;
}
var
n
=
[
7
];
incRef
(
n
);
console
.
log
(
n
[
0
]);
// 8
如果你將函式 c
作為參數傳遞給另一個函式 f
,那麼你必須注意兩個簽章:
f
預期其參數具有的簽章。 f
可能提供多個參數,而 c
可以決定使用其中幾個(如果有)。
c
的實際簽章。例如,它可能支援可選參數。
如果兩者不同,那麼你可能會得到意外的結果:c
可能有你不知道的可選參數,而且會錯誤地解釋 f
提供的額外引數。
舉例來說,考慮陣列方法 map()
(請參閱 轉換方法),其參數通常是具有單一參數的函式
> [ 1, 2, 3 ].map(function (x) { return x * x }) [ 1, 4, 9 ]
你可以傳遞的一個函式是 parseInt()
(請參閱 透過 parseInt() 取得整數)
> parseInt('1024') 1024
你可能會(錯誤地)認為 map()
只提供單一引數,而 parseInt()
只接受單一引數。那麼你會對以下結果感到驚訝:
> [ '1', '2', '3' ].map(parseInt) [ 1, NaN, NaN ]
map()
預期函式具有下列簽章
function
(
element
,
index
,
array
)
但 parseInt()
具有下列簽章
parseInt
(
string
,
radix
?
)
因此,map()
不僅填入 string
(透過 element
),也填入 radix
(透過 index
)。這表示前一個陣列的值會以下列方式產生
> parseInt('1', 0) 1 > parseInt('2', 1) NaN > parseInt('3', 2) NaN
總之,小心使用不確定的簽章函式和方法。如果您使用它們,通常會明確說明收到哪些參數以及傳遞哪些參數。這是透過回呼來達成
> ['1', '2', '3'].map(function (x) { return parseInt(x, 10) }) [ 1, 2, 3 ]
在程式語言中呼叫 函式(或方法)時,您必須將實際參數(由呼叫者指定)對應到形式參數(函式定義的參數)。有兩種常見的方式可以做到這一點:
命名參數有兩個主要好處:它們會提供函式呼叫中引數的描述,而且它們適用於選用參數。我將先說明好處,然後向您展示如何透過物件文字在 JavaScript 中模擬命名參數。
函式 只要有多於一個參數,您可能會對每個參數的用途感到困惑。例如,假設您有一個函式 selectEntries()
,它會從資料庫傳回條目。針對下列函式呼叫:
selectEntries
(
3
,
20
,
2
);
這三個數字代表什麼意思?Python 支援命名參數,而且它們可以輕鬆找出發生了什麼事
selectEntries
(
start
=
3
,
end
=
20
,
step
=
2
)
# Python syntax
選用位置 參數僅在它們在最後被省略時才適用。在其他任何地方,您都必須插入佔位符,例如 null
,以便其餘參數具有正確的位置。使用選用命名參數時,這不是問題。您可以輕鬆省略其中任何一個。以下是一些範例:
# Python syntax
selectEntries
(
step
=
2
)
selectEntries
(
end
=
20
,
start
=
3
)
selectEntries
()
JavaScript 不支援像 Python 和許多其他語言那樣的名稱參數。但有一個相當優雅的模擬:透過物件文字傳遞名稱參數,作為單一實際參數。當您使用此技術時,selectEntries()
的呼叫看起來像:
selectEntries
({
start
:
3
,
end
:
20
,
step
:
2
});
函式會接收一個具有 start
、end
和 step
屬性的物件。您可以省略其中任何一個
selectEntries
({
step
:
2
});
selectEntries
({
end
:
20
,
start
:
3
});
selectEntries
();
您可以實作 selectEntries()
如下
function
selectEntries
(
options
)
{
options
=
options
||
{};
var
start
=
options
.
start
||
0
;
var
end
=
options
.
end
||
getDbLength
();
var
step
=
options
.
step
||
1
;
...
}
您也可以將位置參數與名稱參數結合使用。慣例上,後者會放在最後:
someFunc
(
posArg1
,
posArg2
,
{
namedArg1
:
7
,
namedArg2
:
true
});