函数 - 可重用代码块

编码中的另一个重要概念是函数,它允许您将执行单个任务的代码存储在定义的块中,然后在需要时使用单个简短命令调用该代码,而不是多次重复键入相同的代码。在本文中,我们将探讨函数背后的基本概念,例如基本语法、如何调用和定义它们、作用域和参数。

先决条件 对 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 错误是您会遇到的最常见的错误之一。如果您遇到此错误,并且您确定已定义了相关变量,请检查它位于哪个作用域中。

测试你的技能!

您已经阅读完本文,但您还记得最重要的信息吗?您可以在继续之前找到一些进一步的测试来验证您是否保留了这些信息——请参阅 测试您的技能:函数。这些测试需要在接下来的两篇文章中介绍的技能,因此您可能需要先阅读它们再尝试测试。

结论

本文探讨了函数背后的基本概念,为下一篇文章铺平了道路,在下一篇文章中我们将深入实践,并指导您完成构建自定义函数的步骤。

另请参阅