我們已經知道,在 JavaScript 中,函數也是壹個值。
而 JavaScript 中的每個值都有壹種類型,那麽函數是什麽類型呢?
在 JavaScript 中,函數的類型是對象。
壹個容易理解的方式是把函數想象成可被調用的“行爲對象(action object)”。我們不僅可以調用它們,還能把它們當作對象來處理:增/刪屬性,按引用傳遞等。
函數對象包含壹些便于使用的屬性。
比如,壹個函數的名字可以通過屬性 “name” 來訪問:
function sayHi() { alert("Hi"); } alert(sayHi.name); // sayHi
更有趣的是,名稱賦值的邏輯很智能。即使函數被創建時沒有名字,名稱賦值的邏輯也能給它賦予壹個正確的名字,然後進行賦值:
let sayHi = function() { alert("Hi"); }; alert(sayHi.name); // sayHi(有名字!)
當以默認值的方式完成了賦值時,它也有效:
function f(sayHi = function() {}) { alert(sayHi.name); // sayHi(生效了!) } f();
規範中把這種特性叫做「上下文命名」。如果函數自己沒有提供,那麽在賦值中,會根據上下文來推測壹個。
對象方法也有名字:
let user = { sayHi() { // ... }, sayBye: function() { // ... } } alert(user.sayHi.name); // sayHi alert(user.sayBye.name); // sayBye
這沒有什麽神奇的。有時會出現無法推測名字的情況。此時,屬性 name
會是空,像這樣:
// 函數是在數組中創建的 let arr = [function() {}]; alert( arr[0].name ); // <空字符串> // 引擎無法設置正確的名字,所以沒有值
而實際上,大多數函數都是有名字的。
還有另壹個內建屬性 “length”,它返回函數入參的個數,比如:
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
有時在操作其它函數的函數中用于做 內省/運行時檢查(introspection)。
比如,下面的代碼中函數 ask
接受壹個詢問答案的參數 question
和可能包含任意數量 handler
的參數 ...handlers
。
當用戶提供了自己的答案後,函數會調用那些 handlers
。我們可以傳入兩種 handlers
:
壹種是無參函數,它僅在用戶給出肯定回答時被調用。
壹種是有參函數,它在兩種情況都會被調用,並且返回壹個答案。
爲了正確地調用 handler
,我們需要檢查 handler.length
屬性。
我們的想法是,我們用壹個簡單的無參數的 handler
語法來處理積極的回答(最常見的變體),但也要能夠提供通用的 handler:
function ask(question, ...handlers) { let isYes = confirm(question); for(let handler of handlers) { if (handler.length == 0) { if (isYes) handler(); } else { handler(isYes); } } } // 對于肯定的回答,兩個 handler 都會被調用 // 對于否定的回答,只有第二個 handler 被調用 ask("Question?", () => alert('You said yes'), result => alert(result));
這就是所謂的 多態性 的壹個例子 —— 根據參數的類型,或者根據在我們的具體情景下的 length
來做不同的處理。這種思想在 JavaScript 的庫裏有應用。
我們也可以添加我們自己的屬性。
這裏我們添加了 counter
屬性,用來跟蹤總的調用次數:
function sayHi() { alert("Hi"); // 計算調用次數 sayHi.counter++; } sayHi.counter = 0; // 初始值 sayHi(); // Hi sayHi(); // Hi alert( `Called ${sayHi.counter} times` ); // Called 2 times
屬性不是變量
被賦值給函數的屬性,比如 sayHi.counter = 0
,不會 在函數內定義壹個局部變量 counter
。換句話說,屬性 counter
和變量 let counter
是毫不相關的兩個東西。
我們可以把函數當作對象,在它裏面存儲屬性,但是這對它的執行沒有任何影響。變量不是函數屬性,反之亦然。它們之間是平行的。
函數屬性有時會用來替代閉包。例如,我們可以使用函數屬性將 變量作用域,閉包 章節中 counter 函數的例子進行重寫:
function makeCounter() { // 不需要這個了 // 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,Named Function Expression),指帶有名字的函數表達式的術語。
例如,讓我們寫壹個普通的函數表達式:
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
有兩個特殊的地方,這就是添加它的原因:
它允許函數在內部引用自己。
它在函數外是不可見的。
例如,下面的函數 sayHi
會在沒有入參 who
時,以 "Guest"
爲入參調用自己:
let sayHi = function func(who) { if (who) { alert(`Hello, ${who}`); } else { func("Guest"); // 使用 func 再次調用函數自身 } }; sayHi(); // Hello, Guest // 但這不工作: func(); // Error, func is not defined(在函數外不可見)
我們爲什麽使用 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,嵌套調用 sayHi 不再有效!
發生這種情況是因爲該函數從它的外部詞法環境獲取 sayHi
。沒有局部的 sayHi
了,所以使用外部變量。而當調用時,外部的 sayHi
是 null
。
我們給函數表達式添加的可選的名字,正是用來解決這類問題的。
讓我們使用它來修複我們的代碼:
let sayHi = function func(who) { if (who) { alert(`Hello, ${who}`); } else { func("Guest"); // 現在壹切正常 } }; let welcome = sayHi; sayHi = null; welcome(); // Hello, Guest(嵌套調用有效)
現在它可以正常運行了,因爲名字 func
是函數局部域的。它不是從外部獲取的(而且它對外部也是不可見的)。規範確保它只會引用當前函數。
外部代碼仍然有該函數的 sayHi
或 welcome
變量。而且 func
是壹個“內部函數名”,是函數可以可靠地調用自身的方式。
函數聲明沒有這個東西
這裏所講的“內部名”特性只針對函數表達式,而不是函數聲明。對于函數聲明,沒有用來添加“內部”名的語法。
有時,當我們需要壹個可靠的內部名時,這就成爲了妳把函數聲明重寫成函數表達式的理由了。
函數的類型是對象。
我們介紹了它們的壹些屬性:
name
—— 函數的名字。通常取自函數定義,但如果函數定義時沒設定函數名,JavaScript 會嘗試通過函數的上下文猜壹個函數名(例如把賦值的變量名取爲函數名)。
length
—— 函數定義時的入參的個數。Rest 參數不參與計數。
如果函數是通過函數表達式的形式被聲明的(不是在主代碼流裏),並且附帶了名字,那麽它被稱爲命名函數表達式(Named Function Expression)。這個名字可以用于在該函數內部進行自調用,例如遞歸調用等。
此外,函數可以帶有額外的屬性。很多知名的 JavaScript 庫都充分利用了這個功能。
它們創建壹個“主”函數,然後給它附加很多其它“輔助”函數。例如,jQuery 庫創建了壹個名爲 $
的函數。lodash 庫創建壹個 _
函數,然後爲其添加了 _.add
、_.keyBy
以及其它屬性(想要了解更多內容,參查閱 docs)。實際上,它們這麽做是爲了減少對全局空間的汙染,這樣壹個庫就只會有壹個全局變量。這樣就降低了命名沖突的可能性。
所以,壹個函數本身可以完成壹項有用的工作,還可以在自身的屬性中附帶許多其他功能。
重要程度: 5
修改 makeCounter()
代碼,使得 counter 可以進行減壹和設置值的操作:
counter()
應該返回下壹個數字(與之前的邏輯相同)。
counter.set(value)
應該將 count
設置爲 value
。
counter.decrease()
應該把 count
減 1。
查看沙箱中的代碼獲取完整使用示例。
P.S. 妳可以使用閉包或者函數屬性來保持當前的計數,或者兩種都寫。
打開帶有測試的沙箱。
該解決方案在局部變量中使用 count
,而進行加法操作的方法是直接寫在 counter
中的。它們共享同壹個外部詞法環境,並且可以訪問當前的 count
。
function makeCounter() { let count = 0; function counter() { return count++; } counter.set = value => count = value; counter.decrease = () => count--; return counter; }
使用沙箱的測試功能打開解決方案。
重要程度: 2
寫壹個函數 sum
,它有這樣的功能:
sum(1)(2) == 3; // 1 + 2 sum(1)(2)(3) == 6; // 1 + 2 + 3 sum(5)(-1)(2) == 6 sum(6)(-1)(-2)(-3) == 0 sum(0)(1)(2)(3)(4)(5) == 15
P.S. 提示:妳可能需要創建自定義對象來爲妳的函數提供基本類型轉換。
打開帶有測試的沙箱。
爲了使整個程序無論如何都能正常工作,sum
的結果必須是函數。
這個函數必須將兩次調用之間的當前值保存在內存中。
根據這個題目,當函數被用于 ==
比較時必須轉換成數字。函數是對象,所以轉換規則會按照 對象 —— 原始值轉換 章節所講的進行,我們可以提供自己的方法來返回數字。
代碼如下:
function sum(a) { let currentSum = a; function f(b) { currentSum += b; return f; } f.toString = function() { return currentSum; }; return f; } alert( sum(1)(2) ); // 3 alert( sum(5)(-1)(2) ); // 6 alert( sum(6)(-1)(-2)(-3) ); // 0 alert( sum(0)(1)(2)(3)(4)(5) ); // 15
請注意 sum
函數只工作壹次,它返回了函數 f
。
然後,接下來的每壹次子調用,f
都會把自己的參數加到求和 currentSum
上,然後 f
自身。
在 f
的最後壹行沒有遞歸。
遞歸是這樣子的:
function f(b) { currentSum += b; return f(); // <-- 遞歸調用 }
在我們的例子中,只是返回了函數,並沒有調用它:
function f(b) { currentSum += b; return f; // <-- 沒有調用自己,只是返回了自己 }
這個 f
會被用于下壹次調用,然後再次返回自己,按照需要重複。然後,當它被用做數字或字符串時 —— toString
返回 currentSum
。我們也可以使用 Symbol.toPrimitive
或者 valueOf
來實現轉換。
使用沙箱的測試功能打開解決方案。