函数 - 可重用的代码块

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

¥Another essential concept in coding is functions, which allow you to store a piece of code that does a single task inside a defined block, and then call that code whenever you need it using a single short command — rather than having to type out the same code multiple times. In this article we'll explore fundamental concepts behind functions such as basic syntax, how to invoke and define them, scope, and parameters.

先决条件: 对 HTML、CSS 和 JavaScript 第一步 有基本了解。
目标: 了解 JavaScript 函数背后的基本概念。

我在哪里可以找到函数?

¥Where do I find functions?

在 JavaScript 中,你会发现函数无处不在。事实上,到目前为止,我们在整个课程中一直在使用函数;我们只是没有过多谈论它们。然而,现在是我们开始明确讨论函数并真正探索它们的语法的时候了。

¥In JavaScript, you'll find functions everywhere. In fact, we've been using functions all the way through the course so far; we've just not been talking about them very much. Now is the time, however, for us to start talking about functions explicitly, and really exploring their syntax.

几乎任何时候,当你使用带有一对括号(())的 JavaScript 结构时,并且你没有使用常见的内置语言结构(如 for 循环while 或 do...while 循环if...else 语句),你就在使用函数。

¥Pretty much anytime you make use of a JavaScript structure that features a pair of parentheses — () — and you're not using a common built-in language structure like a for loop, while or do...while loop, or if...else statement, you are making use of a function.

内置浏览器功能

¥Built-in browser functions

在本课程中,我们经常使用浏览器内置的功能。

¥We've used functions built into the browser a lot in this course.

每次我们操作一个文本字符串,例如:

¥Every time we manipulated a text string, for example:

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

或者每次我们操作一个数组时:

¥Or every time we manipulated an array:

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

或者每次我们生成一个随机数:

¥Or every time we generate a random number:

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

我们正在使用一个函数!

¥We were using a function!

注意:如果需要,请随意将这些行输入到浏览器的 JavaScript 控制台中,以重新熟悉它们的功能。

¥Note: Feel free to enter these lines into your browser's JavaScript console to re-familiarize yourself with their functionality, if needed.

JavaScript 语言有许多内置函数,使你可以做有用的事情,而不必自己编写所有代码。事实上,当你调用(运行或执行的一个花哨词)内置浏览器函数时调用的一些代码无法用 JavaScript 编写 - 其中许多函数正在调用后台浏览器代码的一部分, 它主要是用 C++ 等底层系统语言编写的,而不是 JavaScript 等 Web 语言。

¥The JavaScript language has many built-in functions to allow you to do useful things without having to write all that code yourself. In fact, some of the code you are calling when you invoke (a fancy word for run, or execute) a built-in browser function couldn't be written in JavaScript — many of these functions are calling parts of the background browser code, which is written largely in low-level system languages like C++, not web languages like JavaScript.

请记住,某些内置浏览器功能不是核心 JavaScript 语言的一部分 - 有些被定义为浏览器 API 的一部分,它们构建在默认语言之上以提供更多功能(有关更多说明,请参阅 我们课程的早期部分)。我们将在后面的模块中更详细地介绍如何使用浏览器 API。

¥Bear in mind that some built-in browser functions are not part of the core JavaScript language — some are defined as part of browser APIs, which build on top of the default language to provide even more functionality (refer to this early section of our course for more descriptions). We'll look at using browser APIs in more detail in a later module.

函数与方法

¥Functions versus methods

作为对象一部分的函数称为方法。你还不需要了解结构化 JavaScript 对象的内部工作原理 - 你可以等到我们后面的模块,它将教你有关对象的内部工作原理的所有信息,以及如何创建自己的对象。目前,我们只是想消除有关方法与功能的任何可能的混淆 - 当你查看网络上可用的相关资源时,你可能会遇到这两个术语。

¥Functions that are part of objects are called methods. You don't need to learn about the inner workings of structured JavaScript objects yet — you can wait until our later module that will teach you all about the inner workings of objects, and how to create your own. For now, we just wanted to clear up any possible confusion about method versus function — you are likely to meet both terms as you look at the available related resources across the Web.

到目前为止,我们使用的内置代码有两种形式:功能和方法。你可以查看内置函数的完整列表,以及内置对象及其相应的方法 此处

¥The built-in code we've made use of so far comes in both forms: functions and methods. You can check the full list of the built-in functions, as well as the built-in objects and their corresponding methods here.

到目前为止,你还在课程中看到了很多自定义函数 - 在代码中定义的函数,而不是在浏览器内部定义的函数。每当你看到后面带有括号的自定义名称时,你都在使用自定义函数。在 循环文章random-canvas-circles.html 示例(另请参阅完整的 源代码)中,我们包含了一个自定义 draw() 函数,如下所示:

¥You've also seen a lot of custom functions in the course so far — functions defined in your code, not inside the browser. Anytime you saw a custom name with parentheses straight after it, you were using a custom function. In our random-canvas-circles.html example (see also the full source code) from our loops article, we included a custom draw() function that looked like this:

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 个随机圆圈。每次我们想要这样做时,我们都可以这样调用该函数:

¥This function draws 100 random circles inside a <canvas> element. Every time we want to do that, we can just invoke the function with this:

js
draw();

而不必每次我们想要重复时都重新编写所有代码。函数可以包含你喜欢的任何代码 - 你甚至可以从函数内部调用其他函数。上面的函数例如调用了 random() 函数 3 次,其定义如下:

¥rather than having to write all that code out again every time we want to repeat it. Functions can contain whatever code you like — you can even call other functions from inside functions. The above function for example calls the random() function three times, which is defined by the following code:

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

我们需要这个函数是因为浏览器内置的 Math.random() 函数只生成 0 到 1 之间的随机十进制数。我们想要一个介于 0 和指定数字之间的随机整数。

¥We needed this function because the browser's built-in Math.random() function only generates a random decimal number between 0 and 1. We wanted a random whole number between 0 and a specified number.

调用函数

¥Invoking functions

你现在可能已经清楚这一点,但为了以防万一,要在定义函数后实际使用该函数,你必须运行(或调用)它。这是通过在代码中的某处包含函数名称并后跟括号来完成的。

¥You are probably clear on this by now, but just in case, to actually use a function after it has been defined, you've got to run — or invoke — it. This is done by including the name of the function in the code somewhere, followed by parentheses.

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

myFunction();
// calls the function once

注意:这种创建函数的形式也称为函数声明。它始终被提升,以便你可以调用函数定义之上的函数,并且它会正常工作。

¥Note: This form of creating a function is also known as function declaration. It is always hoisted so that you can call the function above the function definition and it will work fine.

函数参数

¥Function parameters

有些函数需要在调用它们时指定参数 - 这些值需要包含在函数括号内,以便函数正确完成其工作。

¥Some functions require parameters to be specified when you are invoking them — these are values that need to be included inside the function parentheses, which it needs to do its job properly.

注意:参数有时称为参数、属性,甚至属性。

¥Note: Parameters are sometimes called arguments, properties, or even attributes.

例如,浏览器内置的 Math.random() 函数不需要任何参数。调用时,它总是返回 0 到 1 之间的随机数:

¥As an example, the browser's built-in Math.random() function doesn't require any parameters. When called, it always returns a random number between 0 and 1:

js
const myNumber = Math.random();

然而,浏览器的内置字符串 replace() 函数需要两个参数 - 在主字符串中查找的子字符串,以及用于替换该字符串的子字符串:

¥The browser's built-in string replace() function however needs two parameters — the substring to find in the main string, and the substring to replace that string with:

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

注意:当需要指定多个参数时,用逗号分隔。

¥Note: When you need to specify multiple parameters, they are separated by commas.

可选参数

¥Optional parameters

有时参数是可选的 - 你不必指定它们。如果不这样做,该函数通常会采用某种默认行为。例如,数组 join() 函数的参数是可选的:

¥Sometimes parameters are optional — you don't have to specify them. If you don't, the function will generally adopt some kind of default behavior. As an example, the array join() function's parameter is optional:

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'

如果不包含任何参数来指定连接/定界字符,则默认使用逗号。

¥If no parameter is included to specify a joining/delimiting character, a comma is used by default.

默认参数

¥Default parameters

如果你正在编写函数并希望支持可选参数,则可以通过在参数名称后面添加 = 并后跟默认值来指定默认值:

¥If you're writing a function and want to support optional parameters, you can specify default values by adding = after the name of the parameter, followed by the default value:

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

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

匿名函数和箭头函数

¥Anonymous functions and arrow functions

到目前为止,我们刚刚创建了一个函数,如下所示:

¥So far we have just created a function like so:

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

但你也可以创建一个没有名称的函数:

¥But you can also create a function that doesn't have a name:

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

这称为匿名函数,因为它没有名称。当一个函数期望接收另一个函数作为参数时,你经常会看到匿名函数。在这种情况下,函数参数通常作为匿名函数传递。

¥This is called an anonymous function, because it has no name. You'll often see anonymous functions when a function expects to receive another function as a parameter. In this case, the function parameter is often passed as an anonymous function.

注意:这种创建函数的形式也称为函数表达式。与函数声明不同,函数表达式不会被提升。

¥Note: This form of creating a function is also known as function expression. Unlike function declarations, function expressions are not hoisted.

匿名函数示例

¥Anonymous function example

例如,假设你希望在用户在文本框中键入内容时运行一些代码。为此,你可以调用文本框的 addEventListener() 函数。该函数期望你向其传递(至少)两个参数:

¥For example, let's say you want to run some code when the user types into a text box. To do this you can call the addEventListener() function of the text box. This function expects you to pass it (at least) two parameters:

  • 要监听的事件的名称,在本例中为 keydown
  • 事件发生时运行的函数。

当用户按下某个键时,浏览器将调用你提供的函数,并向其传递一个包含有关此事件的信息的参数,包括用户按下的特定键:

¥When the user presses a key, the browser will call the function you provided, and will pass it a parameter containing information about this event, including the particular key that the user pressed:

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

textBox.addEventListener("keydown", logKey);

你可以将匿名函数传递给 addEventListener(),而不是定义单独的 logKey() 函数:

¥Instead of defining a separate logKey() function, you can pass an anonymous function into addEventListener():

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

箭头函数

¥Arrow functions

如果你传递这样的匿名函数,则可以使用另一种形式,称为箭头函数。你写的是 (event) =>,而不是 function(event)

¥If you pass an anonymous function like this, there's an alternative form you can use, called an arrow function. Instead of function(event), you write (event) =>:

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

如果函数只接受一个参数,则可以省略参数两边的括号:

¥If the function only takes one parameter, you can omit the parentheses around the parameter:

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

最后,如果你的函数仅包含一行 return 语句,你还可以省略大括号和 return 关键字并隐式返回表达式。在以下示例中,我们使用 Arraymap() 方法将原始数组中的每个值加倍:

¥Finally, if your function contains only one line that's a return statement, you can also omit the braces and the return keyword and implicitly return the expression. In the following example, we're using the map() method of Array to double every value in the original array:

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

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

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

map() 方法依次获取数组中的每个项目,并将其传递给给定的函数。然后,它获取该函数返回的值并将其添加到新数组中。

¥The map() method takes each item in the array in turn, passing it into the given function. It then takes the value returned by that function and adds it to a new array.

因此,在上面的示例中,item => item * 2 是相当于以下的箭头函数:

¥So in the example above, item => item * 2 is the arrow function equivalent of:

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

你可以使用相同的简洁语法来重写 addEventListener 示例。

¥You can use the same concise syntax to rewrite the addEventListener example.

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

在本例中,回调函数隐式返回 console.log() 的值,即 undefined

¥In this case, the value of console.log(), which is undefined, is implicitly returned from the callback function.

我们建议你使用箭头函数,因为它们可以使你的代码更短且更具可读性。要了解更多信息,请参阅 JavaScript 指南中有关箭头函数的部分 和我们的 箭头函数的参考页

¥We recommend that you use arrow functions, as they can make your code shorter and more readable. To learn more, see the section on arrow functions in the JavaScript guide, and our reference page on arrow functions.

注意:箭头函数和普通函数之间存在一些细微的差异。它们超出了本介绍性指南的范围,并且不太可能对我们在这里讨论的情况产生影响。要了解更多信息,请参阅 箭头函数参考文档

¥Note: There are some subtle differences between arrow functions and normal functions. They're outside the scope of this introductory guide and are unlikely to make a difference in the cases we've discussed here. To learn more, see the arrow function reference documentation.

箭头函数实时示例

¥Arrow function live sample

这是我们上面讨论的 "keydown" 示例的完整工作示例:

¥Here's a complete working example of the "keydown" example we discussed above:

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}".`;
});
css
div {
  margin: 0.5rem 0;
}

结果 - 尝试在文本框中输入内容并查看输出:

¥The result - try typing into the text box and see the output:

函数作用域和冲突

¥Function scope and conflicts

我们来谈谈 scope - 处理函数时一个非常重要的概念。当你创建函数时,函数内部定义的变量和其他内容都位于其自己单独的作用域内,这意味着它们被锁定在自己单独的隔间中,无法从函数外部的代码访问。

¥Let's talk a bit about scope — a very important concept when dealing with functions. When you create a function, the variables and other things defined inside the function are inside their own separate scope, meaning that they are locked away in their own separate compartments, unreachable from code outside the functions.

所有函数之外的顶层称为全局作用域。全局范围内定义的值可以从代码中的任何位置访问。

¥The top-level outside all your functions is called the global scope. Values defined in the global scope are accessible from everywhere in the code.

JavaScript 之所以如此设置,有多种原因,但主要是因为安全性和组织性。有时,你不希望从代码中的任何位置都可以访问变量 - 从其他地方调用的外部脚本可能会开始扰乱你的代码并导致问题,因为它们恰好使用与代码其他部分相同的变量名称 ,引发冲突。这可能是恶意的,也可能是偶然的。

¥JavaScript is set up like this for various reasons — but mainly because of security and organization. Sometimes you don't want variables to be accessible from everywhere in the code — external scripts that you call in from elsewhere could start to mess with your code and cause problems because they happen to be using the same variable names as other parts of the code, causing conflicts. This might be done maliciously, or just by accident.

例如,假设你有一个 HTML 文件,该文件正在调用两个外部 JavaScript 文件,并且它们都定义了一个使用相同名称的变量和函数:

¥For example, say you have an HTML file that is calling in two external JavaScript files, and both of them have a variable and a function defined that use the same name:

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 声明,因此无法重新分配。

¥Both functions you want to call are called greeting(), but you can only ever access the first.js file's greeting() function (the second one is ignored). In addition, an error results when attempting (in the second.js file) to assign a new value to the name variable — because it was already declared with const, and so can't be reassigned.

注意:你可以查看此示例 在 GitHub 上实时运行(另请参阅 源代码)。

¥Note: You can see this example running live on GitHub (see also the source code).

将部分代码锁定在函数中可以避免此类问题,并且被认为是最佳实践。

¥Keeping parts of your code locked away in functions avoids such problems, and is considered the best practice.

它有点像一个动物园。狮子、斑马、老虎和企鹅都被关在自己的围栏里,只能接触到围栏内的东西 - 就像函数范围一样。如果他们能够进入其他围栏,就会出现问题。充其量,不同的动物在不熟悉的栖息地里会感到非常不舒服 - 狮子或老虎在企鹅的水和冰冷的字段里会感觉很糟糕。最坏的情况是,狮子和老虎可能会尝试吃掉企鹅!

¥It is a bit like a zoo. The lions, zebras, tigers, and penguins are kept in their own enclosures and only have access to the things inside their enclosures — in the same manner as the function scopes. If they were able to get into other enclosures, problems would occur. At best, different animals would feel really uncomfortable inside unfamiliar habitats — a lion or tiger would feel terrible inside the penguins' watery, icy domain. At worst, the lions and tigers might try to eat the penguins!

Four different animals enclosed in their respective habitat in a Zoo

动物园管理员就像全球范围内的人一样 - 他们拥有进入每个围栏、补充食物、照顾生病的动物等的钥匙。

¥The zoo keeper is like the global scope — they have the keys to access every enclosure, restock food, tend to sick animals, etc.

主动学习:发挥范围

¥Active learning: Playing with scope

让我们看一个真实的例子来演示范围界定。

¥Let's look at a real example to demonstrate scoping.

  1. 首先,制作 function-scope.html 示例的本地副本。其中包含两个名为 a()b() 的函数,以及三个变量 — xyz — 其中两个在函数内部定义,一个在全局范围内定义。它还包含第三个函数,称为 output(),它采用单个参数并将其输出到页面上的段落中。
  2. 在浏览器和文本编辑器中打开该示例。
  3. 在浏览器开发者工具中打开 JavaScript 控制台。在 JavaScript 控制台中,输入以下命令:
    js
    output(x);
    
    你应该看到变量 x 的值打印到浏览器视口中。
  4. 现在尝试在控制台中输入以下内容
    js
    output(y);
    output(z);
    
    这两个都应该按照“参考错误:y 未定义”的行向控制台抛出错误。这是为什么?由于函数作用域的原因,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() 调用会将烦人的 参考错误:变量名未定义 错误抛出到控制台中 - 这是因为 output() 调用和它们尝试打印的变量不在同一函数范围内 - 这些变量实际上对这些函数调用不可见。

注意:相同的作用域规则不适用于循环(例如 for() { })和条件块(例如 if () { }) - 它们看起来非常相似,但它们不是同一件事!注意不要混淆这些。

¥Note: The same scoping rules do not apply to loop (e.g. for() { }) and conditional blocks (e.g. if () { }) — they look very similar, but they are not the same thing! Take care not to get these confused.

注意:参考错误:"x" 未定义 错误是你最常遇到的错误之一。如果你收到此错误并且确定已定义了相关变量,请检查它的作用域。

¥Note: The ReferenceError: "x" is not defined error is one of the most common you'll encounter. If you get this error and you are sure that you have defined the variable in question, check what scope it is in.

测试你的技能!

¥Test your skills!

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

¥You've reached the end of this article, but can you remember the most important information? You can find some further tests to verify that you've retained this information before you move on — see Test your skills: Functions. These tests require skills that are covered in the next two articles, so you might want to read those first before trying them.

结论

¥Conclusion

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

¥This article has explored the fundamental concepts behind functions, paving the way for the next one in which we get practical and take you through the steps to building up your own custom function.

也可以看看

¥See also