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