我們經常需要在腳本的許多地方執行很相似的操作。
例如,當訪問者登錄、注銷或者在其他地方時,我們需要顯示壹條好看的信息。
函數是程序的主要“構建模塊”。函數使該段代碼可以被調用很多次,而不需要寫重複的代碼。
我們已經看到了內建函數的示例,如 alert(message)、prompt(message, default) 和 confirm(question)。但我們也可以創建自己的函數。
使用 函數聲明 創建函數。
看起來就像這樣:
function showMessage() {
alert( 'Hello everyone!' );
}function 關鍵字首先出現,然後是 函數名,然後是括號之間的 參數 列表(用逗號分隔,在上述示例中爲空,我們將在接下來的示例中看到),最後是花括號之間的代碼(即“函數體”)。
function name(parameter1, parameter2, ... parameterN) {
...body...
}我們的新函數可以通過名稱調用:showMessage()。
例如:
function showMessage() {
alert( 'Hello everyone!' );
}
showMessage();
showMessage();調用 showMessage() 執行函數的代碼。這裏我們會看到顯示兩次消息。
這個例子清楚地演示了函數的主要目的之壹:避免代碼重複。
如果我們需要更改消息或其顯示方式,只需在壹個地方修改代碼:輸出它的函數。
在函數中聲明的變量只在該函數內部可見。
例如:
function showMessage() {
let message = "Hello, I'm JavaScript!"; // 局部變量
alert( message );
}
showMessage(); // Hello, I'm JavaScript!
alert( message ); // <-- 錯誤!變量是函數的局部變量函數也可以訪問外部變量,例如:
let userName = 'John';
function showMessage() {
let message = 'Hello, ' + userName;
alert(message);
}
showMessage(); // Hello, John函數對外部變量擁有全部的訪問權限。函數也可以修改外部變量。
例如:
let userName = 'John';
function showMessage() {
userName = "Bob"; // (1) 改變外部變量
let message = 'Hello, ' + userName;
alert(message);
}
alert( userName ); // John 在函數調用之前
showMessage();
alert( userName ); // Bob,值被函數修改了只有在沒有局部變量的情況下才會使用外部變量。
如果在函數內部聲明了同名變量,那麽函數會 遮蔽 外部變量。例如,在下面的代碼中,函數使用局部的 userName,而外部變量被忽略:
let userName = 'John';
function showMessage() {
let userName = "Bob"; // 聲明壹個局部變量
let message = 'Hello, ' + userName; // Bob
alert(message);
}
// 函數會創建並使用它自己的 userName
showMessage();
alert( userName ); // John,未被更改,函數沒有訪問外部變量。全局變量
任何函數之外聲明的變量,例如上述代碼中的外部變量 userName,都被稱爲 全局 變量。
全局變量在任意函數中都是可見的(除非被局部變量遮蔽)。
減少全局變量的使用是壹種很好的做法。現代的代碼有很少甚至沒有全局變量。大多數變量存在于它們的函數中。但是有時候,全局變量能夠用于存儲項目級別的數據。
我們可以通過參數將任意數據傳遞給函數。
在如下示例中,函數有兩個參數:from 和 text。
function showMessage(from, text) { // 參數:from 和 text
alert(from + ': ' + text);
}
showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
showMessage('Ann', "What's up?"); // Ann: What's up? (**)當函數在 (*) 和 (**) 行中被調用時,給定值被複制到了局部變量 from 和 text。然後函數使用它們進行計算。
這裏還有壹個例子:我們有壹個變量 from,並將它傳遞給函數。請注意:函數會修改 from,但在函數外部看不到更改,因爲函數修改的是複制的變量值副本:
function showMessage(from, text) {
from = '*' + from + '*'; // 讓 "from" 看起來更優雅
alert( from + ': ' + text );
}
let from = "Ann";
showMessage(from, "Hello"); // *Ann*: Hello
// "from" 值相同,函數修改了壹個局部的副本。
alert( from ); // Ann當壹個值被作爲函數參數(parameter)傳遞時,它也被稱爲 參數(argument)。
換壹種方式,我們把這些術語搞清楚:
參數(parameter)是函數聲明中括號內列出的變量(它是函數聲明時的術語)。
參數(argument)是調用函數時傳遞給函數的值(它是函數調用時的術語)。
我們聲明函數時列出它們的參數(parameters),然後調用它們傳遞參數(arguments)。
在上面的例子中,我們可以說:“函數 showMessage 被聲明,並且帶有兩個參數(parameters),隨後它被調用,兩個參數(arguments)分別爲 from 和 "Hello"”。
如果壹個函數被調用,但有參數(argument)未被提供,那麽相應的值就會變成 undefined。
例如,之前提到的函數 showMessage(from, text) 可以只使用壹個參數(argument)調用:
showMessage("Ann");那不是錯誤,這樣調用將輸出 "*Ann*: undefined"。因爲參數 text 的值未被傳遞,所以變成了 undefined。
我們可以使用 = 爲函數聲明中的參數指定所謂的“默認”(如果對應參數的值未被傳遞則使用)值:
function showMessage(from, text = "no text given") {
alert( from + ": " + text );
}
showMessage("Ann"); // Ann: no text given現在如果 text 參數未被傳遞,它將會得到值 "no text given"。
這裏 "no text given" 是壹個字符串,但它可以是更複雜的表達式,並且只會在缺少參數時才會被計算和分配。所以,這也是可能的:
function showMessage(from, text = anotherFunction()) {
// anotherFunction() 僅在沒有給定 text 時執行
// 其運行結果將成爲 text 的值
}默認參數的計算
在 JavaScript 中,每次函數在沒帶個別參數的情況下被調用,默認參數會被計算出來。
在上面的例子中,如果傳遞了參數 text,那麽 anotherFunction() 就不會被調用。
如果沒傳遞參數 text,那麽 anotherFunction() 就會被調用。
在 JavaScript 老代碼中的默認參數
幾年前,JavaScript 不支持默認參數的語法。所以人們使用其他方式來設置默認參數。
如今,我們會在舊代碼中看到它們。
例如,顯式地檢查 undefined:
function showMessage(from, text) {
if (text === undefined) {
text = 'no text given';
}
alert( from + ": " + text );
}……或者使用 || 運算符:
function showMessage(from, text) {
// 如果 text 的值爲假值,則分配默認值
// 這樣賦值 text == "" 與 text 無值相同
text = text || 'no text given';
...
}有些時候,將參數默認值的設置放在函數執行(相較更後期)而不是函數聲明時,也行得通。
我們可以通過將參數與 undefined 進行比較,來檢查該參數是否在函數執行期間被傳遞進來:
function showMessage(text) {
// ...
if (text === undefined) { // 如果參數未被傳遞進來
text = 'empty message';
}
alert(text);
}
showMessage(); // empty message……或者我們可以使用 || 運算符:
function showMessage(text) {
// 如果 text 爲 undefined 或者爲假值,那麽將其賦值爲 'empty'
text = text || 'empty';
...
}現代 JavaScript 引擎支持 空值合並運算符 ??,它在大多數假值(例如 0)應該被視爲“正常值”時更具優勢:
function showCount(count) {
// 如果 count 爲 undefined 或 null,則提示 "unknown"
alert(count ?? "unknown");
}
showCount(0); // 0
showCount(null); // unknown
showCount(); // unknown函數可以將壹個值返回到調用代碼中作爲結果。
最簡單的例子是將兩個值相加的函數:
function sum(a, b) {
return a + b;
}
let result = sum(1, 2);
alert( result ); // 3指令 return 可以在函數的任意位置。當執行到達時,函數停止,並將值返回給調用代碼(分配給上述代碼中的 result)。
在壹個函數中可能會出現很多次 return。例如:
function checkAge(age) {
if (age >= 18) {
return true;
} else {
return confirm('Got a permission from the parents?');
}
}
let age = prompt('How old are you?', 18);
if ( checkAge(age) ) {
alert( 'Access granted' );
} else {
alert( 'Access denied' );
}只使用 return 但沒有返回值也是可行的。但這會導致函數立即退出。
例如:
function showMovie(age) {
if ( !checkAge(age) ) {
return;
}
alert( "Showing you the movie" ); // (*)
// ...
}在上述代碼中,如果 checkAge(age) 返回 false,那麽 showMovie 將不會運行到 alert。
空值的 return 或沒有 return 的函數返回值爲 undefined
如果函數無返回值,它就會像返回 undefined 壹樣:
function doNothing() { /* 沒有代碼 */ }
alert( doNothing() === undefined ); // true空值的 return 和 return undefined 等效:
function doNothing() {
return;
}
alert( doNothing() === undefined ); // true不要在 return 與返回值之間添加新行
對于 return 的長表達式,可能妳會很想將其放在單獨壹行,如下所示:
return (some + long + expression + or + whatever * f(a) + f(b))
但這不行,因爲 JavaScript 默認會在 return 之後加上分號。上面這段代碼和下面這段代碼運行流程相同:
return; (some + long + expression + or + whatever * f(a) + f(b))
因此,實際上它的返回值變成了空值。
如果我們想要將返回的表達式寫成跨多行的形式,那麽應該在 return 的同壹行開始寫此表達式。或者至少按照如下的方式放上左括號:
return ( some + long + expression + or + whatever * f(a) + f(b) )
然後它就能像我們預想的那樣正常運行了。
函數就是行爲(action)。所以它們的名字通常是動詞。它應該簡短且盡可能准確地描述函數的作用。這樣讀代碼的人就能清楚地知道這個函數的功能。
壹種普遍的做法是用動詞前綴來開始壹個函數,這個前綴模糊地描述了這個行爲。團隊內部必須就前綴的含義達成壹致。
例如,以 "show" 開頭的函數通常會顯示某些內容。
函數以 XX 開始……
"get…" —— 返回壹個值,
"calc…" —— 計算某些內容,
"create…" —— 創建某些內容,
"check…" —— 檢查某些內容並返回 boolean 值,等。
這類名字的示例:
showMessage(..) // 顯示信息 getAge(..) // 返回 age(gets it somehow) calcSum(..) // 計算求和並返回結果 createForm(..) // 創建表單(通常會返回它) checkPermission(..) // 檢查權限並返回 true/false
有了前綴,只需瞥壹眼函數名,就可以了解它的功能是什麽,返回什麽樣的值。
壹個函數 —— 壹個行爲
壹個函數應該只包含函數名所指定的功能,而不是做更多與函數名無關的功能。
兩個獨立的行爲通常需要兩個函數,即使它們通常被壹起調用(在這種情況下,我們可以創建第三個函數來調用這兩個函數)。
有幾個違反這壹規則的例子:
getAge —— 如果它通過 alert 將 age 顯示出來,那就有問題了(只應該是獲取)。
createForm —— 如果它包含修改文檔的操作,例如向文檔添加壹個表單,那就有問題了(只應該創建表單並返回)。
checkPermission —— 如果它顯示 access granted/denied 消息,那就有問題了(只應執行檢查並返回結果)。
這些例子假設函數名前綴具有通用的含義。妳和妳的團隊可以自定義這些函數名前綴的含義,但是通常都沒有太大的不同。無論怎樣,妳都應該對函數名前綴的含義、帶特定前綴的函數可以做什麽以及不可以做什麽有深刻的了解。所有相同前綴的函數都應該遵守相同的規則。並且,團隊成員應該形成共識。
非常短的函數命名
常用的函數有時會有非常短的名字。
例如,jQuery 框架用 $ 定義壹個函數。LoDash 庫的核心函數用 _ 命名。
這些都是例外,壹般而言,函數名應簡明扼要且具有描述性。
函數應該簡短且只有壹個功能。如果這個函數功能複雜,那麽把該函數拆分成幾個小的函數是值得的。有時候遵循這個規則並不是那麽容易,但這絕對是件好事。
壹個單獨的函數不僅更容易測試和調試 —— 它的存在本身就是壹個很好的注釋!
例如,比較如下兩個函數 showPrimes(n)。它們的功能都是輸出到 n 的 素數。
第壹個變體使用了壹個標簽:
function showPrimes(n) {
nextPrime: for (let i = 2; i < n; i++) {
for (let j = 2; j < i; j++) {
if (i % j == 0) continue nextPrime;
}
alert( i ); // 壹個素數
}
}第二個變體使用附加函數 isPrime(n) 來檢驗素數:
function showPrimes(n) {
for (let i = 2; i < n; i++) {
if (!isPrime(i)) continue;
alert(i); // 壹個素數
}
}
function isPrime(n) {
for (let i = 2; i < n; i++) {
if ( n % i == 0) return false;
}
return true;
}第二個變體更容易理解,不是嗎?我們通過函數名(isPrime)就可以看出函數的行爲,而不需要通過代碼。人們通常把這樣的代碼稱爲 自描述。
因此,即使我們不打算重用它們,也可以創建函數。函數可以讓代碼結構更清晰,可讀性更強。
函數聲明方式如下所示:
function name(parameters, delimited, by, comma) {
/* code */
}作爲參數傳遞給函數的值,會被複制到函數的局部變量。
函數可以訪問外部變量。但它只能從內到外起作用。函數外部的代碼看不到函數內的局部變量。
函數可以返回值。如果沒有返回值,則其返回的結果是 undefined。
爲了使代碼簡潔易懂,建議在函數中主要使用局部變量和參數,而不是外部變量。
與不獲取參數但將修改外部變量作爲副作用的函數相比,獲取參數、使用參數並返回結果的函數更容易理解。
函數命名:
函數名應該清楚地描述函數的功能。當我們在代碼中看到壹個函數調用時,壹個好的函數名能夠讓我們馬上知道這個函數的功能是什麽,會返回什麽。
壹個函數是壹個行爲,所以函數名通常是動詞。
目前有許多優秀的函數名前綴,如 create…、show…、get…、check… 等等。使用它們來提示函數的作用吧。
函數是腳本的主要構建塊。現在我們已經介紹了基本知識,現在我們就可以開始創建和使用函數了。但這只是學習和使用函數的開始。我們將繼續學習更多函數的相關知識,更深入地研究它們的先進特征。
重要程度: 4
如果參數 age 大于 18,那麽下面的函數將返回 true。
否則它將會要求進行確認,並返回確認結果:
function checkAge(age) {
if (age > 18) {
return true;
} else {
// ...
return confirm('父母允許嗎?');
}
}如果 else 被刪除,函數的工作方式會不同嗎?
function checkAge(age) {
if (age > 18) {
return true;
}
// ...
return confirm('父母允許嗎?');
}這兩個變體的行爲是否有區別?
沒有區別。
在這兩種情況下,return confirm('父母允許嗎?') 均只會在 if 條件爲假時執行。
重要程度: 4
如果參數 age 大于 18,那麽下面的函數返回 true。
否則它將會要求進行確認,並返回確認結果:
function checkAge(age) {
if (age > 18) {
return true;
} else {
return confirm('Do you have your parents permission to access this page?');
}
}重寫這個函數並保證效果相同,不使用 if,且只需壹行代碼。
編寫 checkAge 的兩個變體:
使用問號運算符 ?
使用或運算符 ||
使用問號運算符 '?':
function checkAge(age) {
return (age > 18) ? true : confirm('Did parents allow you?');
}使用或運算符 ||(最短的變體):
function checkAge(age) {
return (age > 18) || confirm('Did parents allow you?');
}請注意此處不需要 age > 18 左右的括號。寫上括號是爲了提高可讀性。
重要程度: 1
寫壹個返回數字 a 和 b 中較小的那個數字的函數 min(a,b)。
例如:
min(2, 5) == 2 min(3, -1) == -1 min(1, 1) == 1
使用 if 的解決方案:
function min(a, b) {
if (a < b) {
return a;
} else {
return b;
}
}使用問號運算符 '?' 的解決方案:
function min(a, b) {
return a < b ? a : b;
}P.S. 在 a == b 的情況下,返回什麽都無關緊要。
重要程度: 4
寫壹個函數 pow(x,n),返回 x 的 n 次方。換句話說,將 x 與自身相乘 n 次,返回最終結果。
pow(3, 2) = 3 * 3 = 9 pow(3, 3) = 3 * 3 * 3 = 27 pow(1, 100) = 1 * 1 * ...*1 = 1
創建壹個 web 頁面,提示輸入 x 和 n,然後返回 pow(x,n) 的運算結果。
運行 demo
P.S. 在這個任務中,函數應該只支持自然數 n:從 1 開始的整數。
function pow(x, n) {
let result = x;
for (let i = 1; i < n; i++) {
result *= x;
}
return result;
}
let x = prompt("x?", '');
let n = prompt("n?", '');
if (n < 1) {
alert(`Power ${n} is not supported, use a positive integer`);
} else {
alert( pow(x, n) );
}