函式 - 可重用程式碼塊

編碼中的另一個重要概念是函式,它允許您將執行單個任務的程式碼儲存在定義的塊中,然後在需要時使用單個簡短命令呼叫該程式碼,而不是多次重複鍵入相同的程式碼。在本文中,我們將探討函式背後的基本概念,例如基本語法、如何呼叫和定義它們、作用域和引數。

先決條件 對 HTML、CSS 和 JavaScript 入門 的基本瞭解。
目標 瞭解 JavaScript 函式背後的基本概念。

我在哪裡可以找到函式?

在 JavaScript 中,您會發現函式無處不在。事實上,我們一直在整個課程中使用函式;我們只是沒有過多地談論它們。然而,現在是時候開始明確地討論函式,並真正探索它們的語法了。

幾乎在您使用 JavaScript 結構(包含一對括號 - ())時,只要不是使用常見的內建語言結構,例如 for 迴圈while 或 do...while 迴圈if...else 語句,您就在使用函式。

內建瀏覽器函式

在本課程中,我們已經多次使用了內置於瀏覽器的函式。

例如,每次我們操作文字字串時

js
const myText = "I am a string";
const newString = myText.replace("string", "sausage");
console.log(newString);
// the replace() string function takes a source string,
// and a target string and replaces the source string,
// with the target string, and returns the newly formed string

或者每次我們運算元組時

js
const myArray = ["I", "love", "chocolate", "frogs"];
const madeAString = myArray.join(" ");
console.log(madeAString);
// the join() function takes an array, joins
// all the array items together into a single
// string, and returns this new string

或者每次我們生成隨機數時

js
const myNumber = Math.random();
// the random() function generates a random number between
// 0 and up to but not including 1, and returns that number

我們都在使用函式

注意:如果需要,請隨意將這些行輸入瀏覽器的 JavaScript 控制檯中,以重新熟悉它們的功能。

JavaScript 語言有許多內建函式,可以讓您做一些有用的事情,而無需自己編寫所有程式碼。事實上,您在呼叫(執行或執行的另一個說法)內建瀏覽器函式時呼叫的一些程式碼無法用 JavaScript 編寫 - 這些函式中的許多呼叫的是後臺瀏覽器程式碼的一部分,這些程式碼主要用低階系統語言(如 C++)編寫,而不是像 JavaScript 這樣的 Web 語言。

請記住,一些內建瀏覽器函式不是核心 JavaScript 語言的一部分 - 有些是在瀏覽器 API 中定義的,這些 API 基於預設語言,以提供更多功能(請參閱 我們課程的早期部分 獲取更多描述)。我們將在以後的模組中更詳細地介紹如何使用瀏覽器 API。

函式與方法

屬於物件一部分的函式稱為方法。您現在不必瞭解結構化 JavaScript 物件的內部工作原理 - 您可以等到我們以後的模組,它將教您有關物件內部工作原理以及如何建立自己的物件的所有知識。現在,我們只想消除有關方法與函式之間可能存在的任何混淆 - 當您檢視網路上的可用相關資源時,您可能會遇到這兩個術語。

到目前為止,我們使用的內建程式碼有兩種形式:函式方法。您可以檢視內建函式的完整列表,以及內建物件及其對應方法的完整列表 在這裡

在課程中,您還看到了很多自定義函式 - 在您的程式碼中定義的函式,而不是在瀏覽器中定義的函式。每當您看到一個自定義名稱後面緊跟著括號時,您就在使用自定義函式。在我們 random-canvas-circles.html 示例(另請參閱完整的 原始碼)中,來自我們 迴圈文章,我們包含了一個自定義的 draw() 函式,它看起來像這樣

js
function draw() {
  ctx.clearRect(0, 0, WIDTH, HEIGHT);
  for (let i = 0; i < 100; i++) {
    ctx.beginPath();
    ctx.fillStyle = "rgb(255 0 0 / 50%)";
    ctx.arc(random(WIDTH), random(HEIGHT), random(50), 0, 2 * Math.PI);
    ctx.fill();
  }
}

此函式在 <canvas> 元素內繪製 100 個隨機圓圈。每次我們想要執行此操作時,我們只需使用以下命令呼叫該函式

js
draw();

而不是每次我們想要重複該操作時都不得不再次編寫所有程式碼。函式可以包含您喜歡的任何程式碼 - 您甚至可以在函式內部呼叫其他函式。例如,上面的函式呼叫了 random() 函式三次,該函式由以下程式碼定義

js
function random(number) {
  return Math.floor(Math.random() * number);
}

我們需要此函式是因為瀏覽器的內建 Math.random() 函式只能生成 0 到 1 之間的隨機小數。我們想要 0 到指定數字之間的隨機整數。

呼叫函式

您現在可能已經清楚了這一點,但以防萬一,要真正使用已定義的函式,您必須執行 - 或呼叫 - 它。這是透過在程式碼中的某個位置包含函式名稱,然後加上括號來完成的。

js
function myFunction() {
  alert("hello");
}

myFunction();
// calls the function once

注意:這種建立函式的形式也稱為函式宣告。它總是被提升,因此您可以呼叫函式定義上面的函式,它將正常工作。

函式引數

有些函式需要在您呼叫它們時指定引數 - 這些是需要包含在函式括號內的值,它需要這些值才能正常執行其工作。

注意:引數有時也稱為引數、屬性甚至屬性。

例如,瀏覽器的內建 Math.random() 函式不需要任何引數。當被呼叫時,它總是返回 0 到 1 之間的隨機數

js
const myNumber = Math.random();

但是,瀏覽器的內建字串 replace() 函式需要兩個引數 - 要在主字串中查詢的子字串以及要替換該字串的子字串

js
const myText = "I am a string";
const newString = myText.replace("string", "sausage");

注意:當您需要指定多個引數時,它們之間用逗號隔開。

可選引數

有時引數是可選的 - 您不必指定它們。如果您不指定,該函式通常會採用某種預設行為。例如,陣列 join() 函式的引數是可選的

js
const myArray = ["I", "love", "chocolate", "frogs"];
const madeAString = myArray.join(" ");
console.log(madeAString);
// returns 'I love chocolate frogs'

const madeAnotherString = myArray.join();
console.log(madeAnotherString);
// returns 'I,love,chocolate,frogs'

如果沒有包含引數來指定連線/分隔符,則預設使用逗號。

預設引數

如果您正在編寫一個函式並希望支援可選引數,則可以透過在引數名稱後新增 =,然後新增預設值來指定預設值

js
function hello(name = "Chris") {
  console.log(`Hello ${name}!`);
}

hello("Ari"); // Hello Ari!
hello(); // Hello Chris!

匿名函式和箭頭函式

到目前為止,我們只是這樣建立了一個函式

js
function myFunction() {
  alert("hello");
}

但您也可以建立一個沒有名稱的函式

js
(function () {
  alert("hello");
});

這稱為匿名函式,因為它沒有名稱。當函式期望接收另一個函式作為引數時,您經常會看到匿名函式。在這種情況下,函式引數通常作為匿名函式傳遞。

注意:這種建立函式的形式也稱為函式表示式。與函式宣告不同,函式表示式不會被提升。

匿名函式示例

例如,假設您想在使用者在文字框中輸入時執行一些程式碼。要做到這一點,您可以呼叫文字框的 addEventListener() 函式。此函式期望您向它傳遞(至少)兩個引數

  • 要監聽的事件名稱,在本例中是 keydown
  • 當事件發生時要執行的函式。

當用戶按下鍵時,瀏覽器將呼叫您提供的函式,並將向它傳遞一個包含有關此事件資訊的引數,包括使用者按下的特定鍵

js
function logKey(event) {
  console.log(`You pressed "${event.key}".`);
}

textBox.addEventListener("keydown", logKey);

您可以向 addEventListener() 傳遞一個匿名函式,而不是定義一個單獨的 logKey() 函式

js
textBox.addEventListener("keydown", function (event) {
  console.log(`You pressed "${event.key}".`);
});

箭頭函式

如果您傳遞一個像這樣的匿名函式,您可以使用另一種形式,稱為箭頭函式。而不是 function(event),您寫 (event) =>

js
textBox.addEventListener("keydown", (event) => {
  console.log(`You pressed "${event.key}".`);
});

如果函式只接受一個引數,您可以省略引數周圍的括號

js
textBox.addEventListener("keydown", event => {
  console.log(`You pressed "${event.key}".`);
});

最後,如果您的函式只有一行是 return 語句,您也可以省略大括號和 return 關鍵字並隱式返回表示式。在以下示例中,我們使用 Arraymap() 方法將原始陣列中的每個值加倍

js
const originals = [1, 2, 3];

const doubled = originals.map(item => item * 2);

console.log(doubled); // [2, 4, 6]

map() 方法依次獲取陣列中的每個項,並將它傳遞到給定的函式中。然後,它獲取該函式返回的值並將其新增到一個新的陣列中。

因此,在上面的示例中,item => item * 2 等效於箭頭函式

js
function doubleItem(item) {
  return item * 2;
}

您可以使用相同的簡潔語法來重寫 addEventListener 示例。

js
textBox.addEventListener("keydown", (event) =>
  console.log(`You pressed "${event.key}".`),
);

在這種情況下,console.log() 的值(即 undefined)將從回撥函式中隱式返回。

我們建議您使用箭頭函式,因為它們可以使您的程式碼更短、更易讀。要了解更多資訊,請參閱 JavaScript 指南中關於箭頭函式的部分 以及我們 關於箭頭函式的參考頁面

注意:箭頭函式和普通函式之間存在一些細微差別。它們超出了本入門指南的範圍,在本文討論的案例中不太可能產生影響。要了解更多資訊,請參閱 箭頭函式參考文件

箭頭函式即時示例

以下是我們上面討論的“keydown”示例的完整工作示例

HTML

html
<input id="textBox" type="text" />
<div id="output"></div>

JavaScript

js
const textBox = document.querySelector("#textBox");
const output = document.querySelector("#output");

textBox.addEventListener("keydown", (event) => {
  output.textContent = `You pressed "${event.key}".`;
});

結果 - 嘗試在文字框中鍵入,看看輸出

函式作用域和衝突

讓我們談談 作用域 - 處理函式時一個非常重要的概念。當您建立函式時,在函式內部定義的變數和其他事物都在它們自己的單獨作用域中,這意味著它們被鎖定在它們自己的單獨隔間中,無法從函式外部的程式碼訪問。

所有函式之外的頂層稱為全域性作用域。在全域性作用域中定義的值可以在程式碼中的任何地方訪問。

JavaScript 被這樣設定有各種原因 - 但主要是因為安全性 and 組織。有時您不希望變數可以在程式碼中的任何地方訪問 - 您從其他地方呼叫的外部指令碼可能會開始干擾您的程式碼並導致問題,因為它們碰巧使用了與程式碼其他部分相同的變數名,導致衝突。這可能是惡意地進行的,也可能是偶然發生的。

例如,假設您有一個 HTML 檔案,它呼叫了兩個外部 JavaScript 檔案,並且這兩個檔案都定義了一個變數和一個函式,它們使用相同的名稱

html
<!-- Excerpt from my HTML -->
<script src="first.js"></script>
<script src="second.js"></script>
<script>
  greeting();
</script>
js
// first.js
const name = "Chris";
function greeting() {
  alert(`Hello ${name}: welcome to our company.`);
}
js
// second.js
const name = "Zaptec";
function greeting() {
  alert(`Our company is called ${name}.`);
}

您想要呼叫的兩個函式都稱為 greeting(),但您只能訪問 first.js 檔案的 greeting() 函式(第二個函式被忽略)。此外,嘗試(在 second.js 檔案中)將新值賦予 name 變數時會導致錯誤 - 因為它已經用 const 聲明瞭,因此不能重新賦值。

注意:您可以看到這個例子 在 GitHub 上執行(另請參閱 原始碼)。

將程式碼的一部分鎖定在函式中可以避免此類問題,並且被認為是最佳實踐。

這有點像動物園。獅子、斑馬、老虎和企鵝被關在各自的圍欄中,只能接觸到圍欄裡的東西——就像函式作用域一樣。如果它們能夠進入其他圍欄,就會出現問題。最不濟,不同的動物會在不熟悉的棲息地感到非常不舒服——獅子或老虎會在企鵝冰冷的水域中感到難受。最糟糕的是,獅子和老虎可能會試圖吃企鵝!

Four different animals enclosed in their respective habitat in a Zoo

動物園管理員就像全域性作用域——他們擁有訪問每個圍欄的鑰匙,可以補充食物、照顧生病的動物等等。

主動學習:玩轉作用域

讓我們來看一個實際的例子來演示作用域。

  1. 首先,在本地複製我們的 function-scope.html 示例。它包含兩個名為 a()b() 的函式,以及三個變數——xyz——其中兩個在函式內部定義,一個在全域性作用域中。它還包含一個名為 output() 的第三個函式,它接受一個引數並將其輸出到頁面上的一個段落中。
  2. 在瀏覽器和文字編輯器中開啟示例。
  3. 在瀏覽器開發者工具中開啟 JavaScript 控制檯。在 JavaScript 控制檯中,輸入以下命令
    js
    output(x);
    
    您應該看到變數 x 的值列印到瀏覽器視窗中。
  4. 現在嘗試在控制檯中輸入以下內容
    js
    output(y);
    output(z);
    
    這兩行都應該在控制檯中丟擲一個類似於 "ReferenceError: y is not defined" 的錯誤。為什麼?由於函式作用域,yz 被鎖定在 a()b() 函式內部,因此 output() 在從全域性作用域呼叫時無法訪問它們。
  5. 但是,當它從另一個函式內部呼叫時會怎麼樣?嘗試編輯 a()b(),使它們看起來像這樣
    js
    function a() {
      const y = 2;
      output(y);
    }
    
    function b() {
      const z = 3;
      output(z);
    }
    
    儲存程式碼並在瀏覽器中重新載入它,然後嘗試從 JavaScript 控制檯中呼叫 a()b() 函式
    js
    a();
    b();
    
    您應該看到 yz 值列印到瀏覽器視窗中。這可以正常工作,因為 output() 函式是在其他函式內部呼叫的——在定義要列印的變數所在的相同作用域中,在每種情況下。output() 本身可以在任何地方使用,因為它是在全域性作用域中定義的。
  6. 現在嘗試像這樣更新程式碼
    js
    function a() {
      const y = 2;
      output(x);
    }
    
    function b() {
      const z = 3;
      output(x);
    }
    
  7. 儲存並重新載入,然後在 JavaScript 控制檯中再次嘗試
    js
    a();
    b();
    
    a()b() 的呼叫都應該將 x 的值列印到瀏覽器視窗中。這些可以正常工作,因為即使 output() 呼叫不在 x 定義所在的相同作用域中,x 也是一個全域性變數,因此可以在所有程式碼中,任何地方使用。
  8. 最後,嘗試像這樣更新程式碼
    js
    function a() {
      const y = 2;
      output(z);
    }
    
    function b() {
      const z = 3;
      output(y);
    }
    
  9. 儲存並重新載入,然後在 JavaScript 控制檯中再次嘗試
    js
    a();
    b();
    
    這次 a()b() 呼叫會將惱人的 ReferenceError: 變數名 is not defined 錯誤拋到控制檯中——這是因為 output() 呼叫和它們嘗試列印的變數不在同一個函式作用域中——這些變數實際上對這些函式呼叫不可見。

注意:迴圈(例如 for() { })和條件塊(例如 if () { })不適用相同的作用域規則——它們看起來非常相似,但它們不是同一個東西!注意不要將它們混淆。

注意:ReferenceError: "x" is not defined 錯誤是您會遇到的最常見的錯誤之一。如果您遇到此錯誤,並且您確定已定義了相關變數,請檢查它位於哪個作用域中。

測試你的技能!

您已經閱讀完本文,但您還記得最重要的資訊嗎?您可以在繼續之前找到一些進一步的測試來驗證您是否保留了這些資訊——請參閱 測試您的技能:函式。這些測試需要在接下來的兩篇文章中介紹的技能,因此您可能需要先閱讀它們再嘗試測試。

結論

本文探討了函式背後的基本概念,為下一篇文章鋪平了道路,在下一篇文章中我們將深入實踐,並指導您完成構建自定義函式的步驟。

另請參閱