函数——可重用的代码块
编程中另一个基本概念是函数,它允许你将完成单一任务的代码存储在一个定义好的块中,然后通过一个简短的命令在需要时调用该代码,而不是重复多次键入相同的代码。在本文中,我们将探讨函数背后的基本概念,例如基本语法、如何调用和定义它们、作用域和参数。
在哪里可以找到函数?
在 JavaScript 中,函数无处不在。事实上,到目前为止,我们一直在整个课程中使用函数;我们只是没有过多地谈论它们。然而,现在是时候我们开始明确地谈论函数,并真正探索它们的语法了。
几乎任何时候你使用一个带有括号 — ()
— 的 JavaScript 结构,并且你没有使用像 for 循环、while 或 do...while 循环或 if...else 语句这样的常见内置语言结构时,你都在使用一个函数。
内置浏览器函数
在本课程中,我们大量使用了内置于浏览器中的函数。
例如,每次我们操作文本字符串时
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
或者每次我们操作数组时
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
或者每次我们生成随机数时
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 的使用。
函数与方法
作为对象一部分的函数称为方法;你将在模块后面学习对象。目前,我们只是想澄清关于方法与函数可能存在的任何混淆——你很可能会在 Web 上查看相关资源时遇到这两个术语。
到目前为止我们使用的内置代码有两种形式:函数和方法。你可以在我们的 JavaScript 参考中查看内置函数的完整列表,以及内置对象及其对应的方法。
到目前为止,你也在课程中看到了许多自定义函数——在你的代码中定义,而不是在浏览器中定义的函数。每当你看到一个自定义名称后面直接跟着括号时,你都在使用一个自定义函数。在我们的random-canvas-circles.html示例中(另请参阅我们循环文章的完整源代码),我们包含了自定义的draw()
函数,它看起来像这样:
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 个随机圆。每次我们想这样做时,我们只需用这个来调用函数:
draw();
而不是每次想重复时都重新编写所有代码。函数可以包含你喜欢的任何代码——你甚至可以在函数内部调用其他函数。例如,上面的函数调用了三次random()
函数,该函数由以下代码定义:
function random(number) {
return Math.floor(Math.random() * number);
}
我们需要这个函数,因为浏览器的内置 Math.random()
函数只生成 0 到 1 之间的随机小数。我们需要一个 0 到指定数字之间的随机整数。
调用函数
你现在可能已经很清楚这一点了,但以防万一,要真正使用一个函数,在定义它之后,你必须运行或调用它。这是通过在代码中某个地方包含函数名称,然后是括号来完成的。
function myFunction() {
alert("hello");
}
myFunction();
// calls the function once
注意:这种创建函数的形式也称为函数声明。它总是会被提升,因此你可以在函数定义上方调用函数,并且它会正常工作。
函数参数
有些函数在调用时需要指定参数——这些是需要在函数括号内包含的值,函数需要它们才能正常工作。
注意:参数有时被称为实参、属性或甚至特性。
例如,浏览器内置的 Math.random()
函数不需要任何参数。当被调用时,它总是返回 0 到 1 之间的随机数:
const myNumber = Math.random();
然而,浏览器内置的字符串 replace()
函数需要两个参数——在主字符串中查找的子字符串,以及用于替换该字符串的子字符串:
const myText = "I am a string";
const newString = myText.replace("string", "sausage");
注意:当你需要指定多个参数时,它们用逗号分隔。
可选参数
有时参数是可选的——你不需要指定它们。如果你不指定,函数通常会采用某种默认行为。例如,数组 join()
函数的参数是可选的:
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'
如果没有包含参数来指定连接/分隔字符,则默认使用逗号。
默认参数
如果你正在编写一个函数并希望支持可选参数,你可以通过在参数名称后添加=
,然后是默认值来指定默认值:
function hello(name = "Chris") {
console.log(`Hello ${name}!`);
}
hello("Ari"); // Hello Ari!
hello(); // Hello Chris!
匿名函数和箭头函数
到目前为止,我们只创建了这样一个函数:
function myFunction() {
alert("hello");
}
但是你也可以创建一个没有名称的函数:
(function () {
alert("hello");
});
这被称为匿名函数,因为它没有名称。当函数期望接收另一个函数作为参数时,你经常会看到匿名函数。在这种情况下,函数参数通常作为匿名函数传递。
注意:这种创建函数的形式也称为函数表达式。与函数声明不同,函数表达式不会被提升。
匿名函数示例
例如,假设你想在用户在文本框中输入时运行一些代码。为此,你可以调用文本框的 addEventListener()
函数。这个函数期望你传递给它(至少)两个参数:
- 要监听的事件名称,在本例中是
keydown
- 事件发生时要运行的函数。
当用户按下某个键时,浏览器将调用你提供的函数,并向其传递一个包含此事件信息的参数,其中包括用户按下的特定键:
function logKey(event) {
console.log(`You pressed "${event.key}".`);
}
textBox.addEventListener("keydown", logKey);
你可以将匿名函数传递给 addEventListener()
,而不是定义一个单独的 logKey()
函数:
textBox.addEventListener("keydown", function (event) {
console.log(`You pressed "${event.key}".`);
});
箭头函数
如果你像这样传递一个匿名函数,还有另一种你可以使用的形式,称为箭头函数。你将function(event)
写成(event) =>
:
textBox.addEventListener("keydown", (event) => {
console.log(`You pressed "${event.key}".`);
});
如果函数只接受一个参数,你可以省略参数周围的括号:
textBox.addEventListener("keydown", event => {
console.log(`You pressed "${event.key}".`);
});
最后,如果你的函数只包含一个 return
语句,你也可以省略大括号和 return
关键字,并隐式返回表达式。在下面的示例中,我们使用 Array
的 map()
方法将原始数组中的每个值翻倍:
const originals = [1, 2, 3];
const doubled = originals.map(item => item * 2);
console.log(doubled); // [2, 4, 6]
map()
方法依次获取数组中的每个项,并将其传递给给定的函数。然后它获取该函数返回的值并将其添加到新数组中。
因此,在上面的例子中,item => item * 2
等同于以下箭头函数:
function doubleItem(item) {
return item * 2;
}
你可以使用相同的简洁语法重写 addEventListener
示例。
textBox.addEventListener("keydown", (event) =>
console.log(`You pressed "${event.key}".`),
);
在这种情况下,console.log()
的值(即 undefined
)会从回调函数中隐式返回。
我们建议你使用箭头函数,因为它们可以使你的代码更短、更具可读性。要了解更多信息,请参阅 JavaScript 指南中关于箭头函数的部分,以及我们的 箭头函数参考页面。
注意:箭头函数和普通函数之间存在一些细微差别。它们超出了本入门教程的范围,并且在我们此处讨论的情况下不太可能产生差异。要了解更多信息,请参阅箭头函数参考文档。
箭头函数在线示例
这是我们上面讨论的“keydown”示例的完整工作示例:
HTML
<input id="textBox" type="text" />
<div id="output"></div>
JavaScript
const textBox = document.querySelector("#textBox");
const output = document.querySelector("#output");
textBox.addEventListener("keydown", (event) => {
output.textContent = `You pressed "${event.key}".`;
});
结果 - 尝试在文本框中输入并查看输出
函数作用域和冲突
让我们谈谈作用域——这是一个处理函数时非常重要的概念。当你创建一个函数时,函数内部定义的变量和其他内容都处于它们自己独立的作用域中,这意味着它们被锁定在自己独立的隔间中,外部代码无法访问。
所有函数之外的顶层称为全局作用域。在全局作用域中定义的值可以在代码的任何地方访问。
JavaScript 这样设置有各种原因——但主要是出于安全和组织考虑。有时你不希望变量在代码的任何地方都可以访问。你从其他地方调用的外部脚本可能会开始干扰你的代码并导致问题,因为它们恰好使用了与代码其他部分相同的变量名,从而导致冲突。这可能是恶意行为,也可能只是意外。
例如,假设你有一个引用两个外部 JavaScript 文件的 HTML 文件,并且它们都定义了一个使用相同名称的变量和函数:
<!-- Excerpt from the HTML -->
<script src="first.js"></script>
<script src="second.js"></script>
<script>
greeting();
</script>
// first.js
const name = "Chris";
function greeting() {
alert(`Hello ${name}: welcome to our company.`);
}
// second.js
const name = "Zaptec";
function greeting() {
alert(`Our company is called ${name}.`);
}
你可以在 GitHub 上查看这个实时运行的示例(另请参阅源代码)。在阅读下面的解释之前,请将其加载到单独的浏览器选项卡中。
-
当示例在浏览器中渲染时,你将首先看到一个警报框显示
Hello Chris: welcome to our company.
,这意味着第一个脚本文件中定义的greeting()
函数已被内部脚本中的greeting()
调用所调用。 -
然而,第二个脚本根本没有加载和运行,并且控制台打印出错误:
Uncaught SyntaxError: Identifier 'name' has already been declared
。这是因为name
常量已经在first.js
中声明,并且你不能在同一作用域中两次声明相同的常量。由于第二个脚本没有加载,因此无法调用second.js
中的greeting()
函数。 -
如果我们从
second.js
中删除const name = "Zaptec";
这一行并重新加载页面,两个脚本都会执行。此时警报框会显示Our company is called Chris.
函数可以被重新声明,并且源代码顺序中最后的声明将被使用。之前的声明实际上被覆盖了。
将代码的各个部分锁定在函数中可以避免此类问题,这被认为是最佳实践。
这有点像动物园。狮子、斑马、老虎和企鹅都关在自己的围栏里,只能接触到里面的东西——就像函数作用域一样。如果它们能够进入其他围栏,就会出现问题。最好的情况是,不同的动物会在不熟悉的环境中感到非常不舒服——狮子或老虎会在企鹅冰冷的水域中感到非常糟糕。最坏的情况是,狮子和老虎可能会试图吃掉企鹅!
动物园管理员就像全局作用域——他们拥有进入每个围栏、补充食物、照顾生病动物等的钥匙。
玩转作用域
让我们来看一个真实的例子来演示作用域。
-
首先,在本地复制我们的 function-scope.html 示例。它包含两个名为
a()
和b()
的函数,以及三个变量——x
、y
和z
——其中两个在函数内部定义,一个在全局作用域中。它还包含第三个名为output()
的函数,该函数接受一个参数并在页面上的段落中输出它。 -
在浏览器和文本编辑器中打开示例。
-
打开浏览器开发者工具中的 JavaScript 控制台。在 JavaScript 控制台中,输入以下命令:
jsoutput(x);
你应该会在浏览器视口中看到变量
x
的值。 -
现在尝试在控制台中输入以下内容:
jsoutput(y); output(z);
这两个都应该在控制台中抛出类似于“ReferenceError: y is not defined”的错误。这是为什么呢?因为函数作用域:
y
和z
被锁定在a()
和b()
函数内部,所以当从全局作用域调用output()
时,它无法访问它们。 -
但是,当它从另一个函数内部调用时会怎样?尝试编辑
a()
和b()
,使它们看起来像这样:jsfunction a() { const y = 2; output(y); } function b() { const z = 3; output(z); }
保存代码并在浏览器中重新加载,然后尝试从 JavaScript 控制台调用
a()
和b()
函数:jsa(); b();
你应该在浏览器视口中看到
y
和z
的值。这工作正常,因为output()
函数是在其他函数内部调用的,与它正在打印的变量在同一个作用域中定义。output()
本身可以在任何地方使用,因为它是在全局作用域中定义的。 -
现在尝试像这样更新你的代码:
jsfunction a() { const y = 2; output(x); } function b() { const z = 3; output(x); }
-
再次保存并重新加载,然后在 JavaScript 控制台中再次尝试此操作:
jsa(); b();
a()
和b()
调用都应该将 x 的值打印到浏览器视口。这些工作正常,因为尽管output()
调用与x
定义的作用域不同,但x
是一个全局变量,因此在所有代码中都可用。 -
最后,尝试像这样更新你的代码:
jsfunction a() { const y = 2; output(z); } function b() { const z = 3; output(y); }
-
再次保存并重新加载,然后在 JavaScript 控制台中再次尝试此操作:
jsa(); b();
这次,
a()
和b()
调用将在控制台中抛出烦人的 ReferenceError: 变量名未定义 错误——这是因为output()
调用和它们试图打印的变量不在同一个函数作用域中——这些变量对这些函数调用实际上是不可见的。
注意:相同的作用域规则不适用于循环(例如,for() { }
)和条件块(例如,if () { }
)——它们看起来非常相似,但它们不是一回事!请注意不要将这些混淆。
注意:ReferenceError: "x" is not defined 错误是你将遇到的最常见的错误之一。如果你收到此错误并且你确定你已定义了该变量,请检查它位于哪个作用域中。
总结
本文探讨了函数背后的基本概念,为下一篇文章铺平了道路,我们将在其中进行实践,并引导你完成构建自己的自定义函数的步骤。
另见
- 函数详细指南 — 涵盖此处未包含的一些高级功能。
- 函数参考
- 使用函数编写更少的代码,Scrimba MDN 学习合作伙伴 - 一个互动课程,提供了有用的函数介绍。