W3Cschool
恭喜您成為首批注冊用戶
獲得88經驗值獎勵
我們已經知道,在 JavaScript 中,函數也是一個值。
而 JavaScript 中的每個值都有一種類型,那么函數是什么類型呢?
在 JavaScript 中,函數的類型是對象。
一個容易理解的方式是把函數想象成可被調用的“行為對象(action object)”。我們不僅可以調用它們,還能把它們當作對象來處理:增/刪屬性,按引用傳遞等。
函數對象包含一些便于使用的屬性。
比如,一個函數的名字可以通過屬性 “name” 來訪問:
function sayHi() {
alert("Hi");
}
alert(sayHi.name); // sayHi
更有趣的是,名稱賦值的邏輯很智能。即使函數被創(chuàng)建時沒有名字,名稱賦值的邏輯也能給它賦予一個正確的名字,然后進行賦值:
let sayHi = function() {
alert("Hi");
};
alert(sayHi.name); // sayHi(有名字!)
當以默認值的方式完成了賦值時,它也有效:
function f(sayHi = function() {}) {
alert(sayHi.name); // sayHi(生效了?。?}
f();
規(guī)范中把這種特性叫做「上下文命名」。如果函數自己沒有提供,那么在賦值中,會根據上下文來推測一個。
對象方法也有名字:
let user = {
sayHi() {
// ...
},
sayBye: function() {
// ...
}
}
alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye
這沒有什么神奇的。有時會出現(xiàn)無法推測名字的情況。此時,屬性 name
會是空,像這樣:
// 函數是在數組中創(chuàng)建的
let arr = [function() {}];
alert( arr[0].name ); // <空字符串>
// 引擎無法設置正確的名字,所以沒有值
而實際上,大多數函數都是有名字的。
還有另一個內建屬性 “l(fā)ength”,它返回函數入參的個數,比如:
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));
這就是所謂的 多態(tài)性 的一個例子 —— 根據參數的類型,或者根據在我們的具體情景下的 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
是毫不相關的兩個東西。
我們可以把函數當作對象,在它里面存儲屬性,但是這對它的執(zhí)行沒有任何影響。變量不是函數屬性,反之亦然。它們之間是平行的。
函數屬性有時會用來替代閉包。例如,我們可以使用函數屬性將 變量作用域,閉包 章節(jié)中 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
現(xiàn)在 count
被直接存儲在函數里,而不是它外部的詞法環(huán)境。
那么它和閉包誰好誰賴?
兩者最大的不同就是如果 count
的值位于外層(函數)變量中,那么外部的代碼無法訪問到它,只有嵌套的那些函數可以修改它。而如果它是綁定到函數的,那么就可以這樣:
function makeCounter() {
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
let counter = makeCounter();
counter.count = 10;
alert( counter() ); // 10
所以,選擇哪種實現(xiàn)方式取決于我們的需求是什么。
命名函數表達式(NFE,Named Function Expression),指帶有名字的函數表達式的術語。
例如,讓我們寫一個普通的函數表達式:
let sayHi = function(who) {
alert(`Hello, ${who}`);
};
然后給它加一個名字:
let sayHi = function func(who) {
alert(`Hello, ${who}`);
};
我們這里得到了什么嗎?為它添加一個 "func"
名字的目的是什么?
首先請注意,它仍然是一個函數表達式。在 function
后面加一個名字 "func"
沒有使它成為一個函數聲明,因為它仍然是作為賦值表達式中的一部分被創(chuàng)建的。
添加這個名字當然也沒有打破任何東西。
函數依然可以通過? 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(在函數外不可見)
我們?yōu)槭裁词褂?nbsp;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 不再有效!
發(fā)生這種情況是因為該函數從它的外部詞法環(huán)境獲取 sayHi
。沒有局部的 sayHi
了,所以使用外部變量。而當調用時,外部的 sayHi
是 null
。
我們給函數表達式添加的可選的名字,正是用來解決這類問題的。
讓我們使用它來修復我們的代碼:
let sayHi = function func(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
func("Guest"); // 現(xiàn)在一切正常
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Hello, Guest(嵌套調用有效)
現(xiàn)在它可以正常運行了,因為名字 func
是函數局部域的。它不是從外部獲取的(而且它對外部也是不可見的)。規(guī)范確保它只會引用當前函數。
外部代碼仍然有該函數的 sayHi
或 welcome
變量。而且 func
是一個“內部函數名”,是函數可以可靠地調用自身的方式。
函數聲明沒有這個東西
這里所講的“內部名”特性只針對函數表達式,而不是函數聲明。對于函數聲明,沒有用來添加“內部”名的語法。
有時,當我們需要一個可靠的內部名時,這就成為了你把函數聲明重寫成函數表達式的理由了。
函數的類型是對象。
我們介紹了它們的一些屬性:
name
? —— 函數的名字。通常取自函數定義,但如果函數定義時沒設定函數名,JavaScript 會嘗試通過函數的上下文猜一個函數名(例如把賦值的變量名取為函數名)。length
? —— 函數定義時的入參的個數。Rest 參數不參與計數。如果函數是通過函數表達式的形式被聲明的(不是在主代碼流里),并且附帶了名字,那么它被稱為命名函數表達式(Named Function Expression)。這個名字可以用于在該函數內部進行自調用,例如遞歸調用等。
此外,函數可以帶有額外的屬性。很多知名的 JavaScript 庫都充分利用了這個功能。
它們創(chuàng)建一個“主”函數,然后給它附加很多其它“輔助”函數。例如,jQuery 庫創(chuàng)建了一個名為 $
的函數。lodash 庫創(chuàng)建一個 _
函數,然后為其添加了 _.add
、_.keyBy
以及其它屬性(想要了解更多內容,參查閱
docs)。實際上,它們這么做是為了減少對全局空間的污染,這樣一個庫就只會有一個全局變量。這樣就降低了命名沖突的可能性。
所以,一個函數本身可以完成一項有用的工作,還可以在自身的屬性中附帶許多其他功能。
修改 makeCounter()
代碼,使得 counter 可以進行減一和設置值的操作:
counter()
? 應該返回下一個數字(與之前的邏輯相同)。counter.set(value)
? 應該將 ?count
? 設置為 ?value
?。counter.decrease()
? 應該把 ?count
? 減 1。P.S. 你可以使用閉包或者函數屬性來保持當前的計數,或者兩種都寫。
function makeCounter() {
let count = 0;
// ... your code ...
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
counter.set(10); // set the new count
alert( counter() ); // 10
counter.decrease(); // decrease the count by 1
alert( counter() ); // 10 (instead of 11)
該解決方案在局部變量中使用 count
,而進行加法操作的方法是直接寫在 counter
中的。它們共享同一個外部詞法環(huán)境,并且可以訪問當前的 count
。
function makeCounter() {
let count = 0;
function counter() {
return count++;
}
counter.set = value => count = value;
counter.decrease = () => count--;
return counter;
}
寫一個函數 ?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. 提示:你可能需要創(chuàng)建自定義對象來為你的函數提供基本類型轉換。
sum
? 的結果必須是函數。==
? 比較時必須轉換成數字。函數是對象,所以轉換規(guī)則會按照 對象 —— 原始值轉換 章節(jié)所講的進行,我們可以提供自己的方法來返回數字。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
來實現(xiàn)轉換。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: