正如我們所知,JavaScript 中的函數是一個值。
JavaScript 中的每個值都有類型。函數是什麼類型?
在 JavaScript 中,函數是物件。
可以將函數想像成可呼叫的「動作物件」。我們不僅可以呼叫它們,還可以將它們視為物件:新增/移除屬性、傳遞參考等。
「名稱」屬性
函數物件包含一些可用的屬性。
例如,函數的名稱可作為「名稱」屬性存取
function sayHi() {
alert("Hi");
}
alert(sayHi.name); // sayHi
有點有趣的是,名稱指派邏輯很聰明。即使函數在沒有名稱的情況下建立,然後立即指派,它也會指派正確的名稱
let sayHi = function() {
alert("Hi");
};
alert(sayHi.name); // sayHi (there's a name!)
如果指派是透過預設值完成,它也會運作
function f(sayHi = function() {}) {
alert(sayHi.name); // sayHi (works!)
}
f();
在規範中,此功能稱為「情境名稱」。如果函式未提供名稱,則在指定時會從情境中找出。
物件方法也有名稱
let user = {
sayHi() {
// ...
},
sayBye: function() {
// ...
}
}
alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye
不過沒有什麼神奇的事。有些情況無法找出正確的名稱。在這種情況下,名稱屬性會是空的,如下所示
// function created inside array
let arr = [function() {}];
alert( arr[0].name ); // <empty string>
// the engine has no way to set up the right name, so there is none
然而,實際上大多數函式都有名稱。
「長度」屬性
還有另一個內建屬性「長度」,用於傳回函式參數的數量,例如
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}
alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2
在此處,我們可以看到 rest 參數並未計算在內。
length
屬性有時用於對其他函式進行運算的函式中的內省。
例如,在以下程式碼中,ask
函式接受一個要詢問的question
,以及任意數量的要呼叫的handler
函式。
使用者提供答案後,函式會呼叫處理常式。我們可以傳遞兩種處理常式
- 零參數函式,僅在使用者提供肯定答案時呼叫。
- 有參數函式,在任何情況下都會呼叫,並傳回答案。
若要正確呼叫handler
,我們會檢查handler.length
屬性。
構想是,我們有一個簡單的無參數處理常式語法,適用於肯定情況(最常見的變體),但也能支援通用處理常式
function ask(question, ...handlers) {
let isYes = confirm(question);
for(let handler of handlers) {
if (handler.length == 0) {
if (isYes) handler();
} else {
handler(isYes);
}
}
}
// for positive answer, both handlers are called
// for negative answer, only the second one
ask("Question?", () => alert('You said yes'), result => alert(result));
這是所謂多型的特殊情況,會根據參數的類型或在我們的案例中根據length
,以不同的方式處理參數。這個構想在 JavaScript 函式庫中確實有用。
自訂屬性
我們也可以新增自己的屬性。
在此處,我們新增counter
屬性來追蹤總呼叫次數
function sayHi() {
alert("Hi");
// let's count how many times we run
sayHi.counter++;
}
sayHi.counter = 0; // initial value
sayHi(); // Hi
sayHi(); // Hi
alert( `Called ${sayHi.counter} times` ); // Called 2 times
指定給函式的屬性,例如sayHi.counter = 0
,不會在函式內定義一個區域變數counter
。換句話說,屬性counter
和變數let counter
是兩個不相關的事物。
我們可以將函式視為物件,在其中儲存屬性,但這不會對其執行產生任何影響。變數不是函式屬性,反之亦然。這些只是平行的世界。
函式屬性有時可以取代閉包。例如,我們可以改寫章節變數範圍、閉包中的計數器函式範例,以使用函式屬性
function makeCounter() {
// instead of:
// let count = 0
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
count
現在直接儲存在函式中,而不是儲存在其外部詞法環境中。
這樣做比使用閉包好還是差?
主要的差別在於,如果 count
的值存在於外部變數中,則外部程式碼無法存取它。只有巢狀函式可以修改它。如果它與函式繫結,則可以這樣做
function makeCounter() {
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
let counter = makeCounter();
counter.count = 10;
alert( counter() ); // 10
因此,實作的選擇取決於我們的目標。
命名函式表達式
命名函式表達式,或 NFE,是具有名稱的函式表達式的術語。
例如,我們來取一個普通的函式表達式
let sayHi = function(who) {
alert(`Hello, ${who}`);
};
並為其加上一個名稱
let sayHi = function func(who) {
alert(`Hello, ${who}`);
};
我們在此達到了什麼?附加的 "func"
名稱的目的是什麼?
首先,我們注意到,我們仍然有一個函式表達式。在 function
之後加上名稱 "func"
並不會使其成為函式宣告,因為它仍然是作為賦值表達式的一部分建立的。
加上這樣的名稱也不會破壞任何東西。
函式仍然可用作 sayHi()
let sayHi = function func(who) {
alert(`Hello, ${who}`);
};
sayHi("John"); // Hello, John
名稱 func
有兩個特殊之處,這就是原因
- 它允許函式在內部參照自身。
- 它在函式外部不可見。
例如,如果未提供 who
,函式 sayHi
以下會使用 "Guest"
再次呼叫自身
let sayHi = function func(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
func("Guest"); // use func to re-call itself
}
};
sayHi(); // Hello, Guest
// But this won't work:
func(); // Error, func is not defined (not visible outside of the function)
我們為什麼使用 func
?也許只對巢狀呼叫使用 sayHi
?
實際上,在大多數情況下,我們都可以
let sayHi = function(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
sayHi("Guest");
}
};
該程式碼的問題在於 sayHi
可能會在外層程式碼中變更。如果函式改指派給另一個變數,程式碼將開始產生錯誤
let sayHi = function(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
sayHi("Guest"); // Error: sayHi is not a function
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Error, the nested sayHi call doesn't work any more!
這是因為函式從其外層詞彙環境中取得 sayHi
。沒有本地的 sayHi
,因此使用外層變數。在呼叫的那一刻,外層 sayHi
為 null
。
我們可以放入函式表達式的選用名稱旨在解決這類問題。
我們使用它來修正我們的程式碼
let sayHi = function func(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
func("Guest"); // Now all fine
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Hello, Guest (nested call works)
現在它可以運作,因為名稱 "func"
是函式本地的。它不會從外部取得(也不會在那裡可見)。規格保證它將始終參照目前的函式。
外層程式碼仍然有其變數 sayHi
或 welcome
。而 func
是「內部函式名稱」,函式可以可靠地呼叫自己的方式。
此處所述的「內部名稱」功能僅適用於函式表達式,不適用於函式宣告。對於函式宣告,沒有語法可以加入「內部」名稱。
有時,當我們需要可靠的內部名稱時,這便是將函式宣告改寫為命名函式表達式形式的原因。
摘要
函式是物件。
我們在此介紹函式的屬性
name
– 函式名稱。通常從函式定義中取得,但如果沒有,JavaScript 會嘗試從內容中猜測(例如指派)。length
– 函式定義中的參數數量。不計算 rest 參數。
如果函式宣告為函式表達式(不在主程式碼流程中),且帶有名稱,則稱為命名函式表達式。名稱可以在函式內部用於參照函式本身,例如遞迴呼叫等。
此外,函式可以攜帶其他屬性。許多知名的 JavaScript 函式庫大量使用此功能。
它們會建立一個「主」函式,並將許多其他「輔助」函式附加到其中。例如,jQuery 函式庫會建立一個名為 $
的函式。lodash 函式庫會建立一個函式 _
,然後將 _.clone
、_.keyBy
和其他屬性新增到其中(當您想進一步了解這些屬性時,請參閱文件)。實際上,它們這樣做是為了減少對全域空間的污染,讓單一函式庫只提供一個全域變數。這會降低命名衝突的可能性。
因此,函式本身可以執行有用的工作,也可以在屬性中攜帶許多其他功能。
留言
<code>
標籤,若要插入多行程式碼,請將它們包覆在<pre>
標籤中,若要插入超過 10 行的程式碼,請使用沙盒 (plnkr、jsbin、codepen…)