我们经常需要在脚本的许多地方执行类似的操作。
例如,当访问者登录、注销,或者在其他地方时,我们需要显示一条漂亮的提示消息。
函数是程序的主要“构建块”。它们允许在不重复的情况下多次调用代码。
我们已经看到了内置函数的示例,例如 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!"; // local variable
alert( message );
}
showMessage(); // Hello, I'm JavaScript!
alert( message ); // <-- Error! The variable is local to the function
外部变量
函数也可以访问外部变量,例如
let userName = 'John';
function showMessage() {
let message = 'Hello, ' + userName;
alert(message);
}
showMessage(); // Hello, John
函数可以完全访问外部变量。它也可以修改它。
例如
let userName = 'John';
function showMessage() {
userName = "Bob"; // (1) changed the outer variable
let message = 'Hello, ' + userName;
alert(message);
}
alert( userName ); // John before the function call
showMessage();
alert( userName ); // Bob, the value was modified by the function
仅在没有局部变量时才使用外部变量。
如果在函数内声明了同名变量,则它会遮蔽外部变量。例如,在下面的代码中,函数使用局部userName
。外部变量被忽略
let userName = 'John';
function showMessage() {
let userName = "Bob"; // declare a local variable
let message = 'Hello, ' + userName; // Bob
alert(message);
}
// the function will create and use its own userName
showMessage();
alert( userName ); // John, unchanged, the function did not access the outer variable
在任何函数外部声明的变量(例如上面代码中的外部userName
)称为全局变量。
全局变量对任何函数都可见(除非被局部变量遮蔽)。
尽量减少使用全局变量是一种好习惯。现代代码很少或没有全局变量。大多数变量驻留在其函数中。不过,有时它们可用于存储项目级数据。
参数
我们可以使用参数将任意数据传递给函数。
在下面的示例中,函数有两个参数:from
和text
。
function showMessage(from, text) { // parameters: 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 + '*'; // make "from" look nicer
alert( from + ': ' + text );
}
let from = "Ann";
showMessage(from, "Hello"); // *Ann*: Hello
// the value of "from" is the same, the function modified a local copy
alert( from ); // Ann
当将值作为函数参数传递时,它也称为参数。
换句话说,要理清这些术语
- 参数是在函数声明中括号内列出的变量(它是声明时间术语)。
- 参数是在调用函数时传递给函数的值(它是调用时间术语)。
我们声明函数列出它们的 parameters,然后调用它们传递参数。
在上面的示例中,有人可能会说:“函数showMessage
声明了两个参数,然后用两个参数调用:from
和"Hello"
”。
默认值
如果调用了一个函数,但未提供参数,则相应的值将变为 undefined
。
例如,上述函数 showMessage(from, text)
可以使用单个参数调用
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"
。
如果参数存在,但严格等于 undefined
,默认值也会介入,如下所示
showMessage("Ann", undefined); // Ann: no text given
这里 "no text given"
是一个字符串,但它可以是一个更复杂的表达式,只有在缺少参数时才会对其进行求值和赋值。因此,这也是可能的
function showMessage(from, text = anotherFunction()) {
// anotherFunction() only executed if no text given
// its result becomes the value of text
}
在 JavaScript 中,每次在没有相应参数的情况下调用函数时,都会对默认参数进行求值。
在上面的示例中,如果提供了 text
参数,则根本不会调用 anotherFunction()
。
另一方面,每次缺少 text
时都会独立调用它。
几年前,JavaScript 不支持默认参数的语法。因此,人们使用其他方法来指定它们。
如今,我们可以在旧脚本中遇到它们。
例如,对 undefined
的显式检查
function showMessage(from, text) {
if (text === undefined) {
text = 'no text given';
}
alert( from + ": " + text );
}
…或使用 ||
运算符
function showMessage(from, text) {
// If the value of text is falsy, assign the default value
// this assumes that text == "" is the same as no text at all
text = text || 'no text given';
...
}
备用默认参数
有时在函数声明后在稍后的阶段为参数分配默认值是有意义的。
我们可以通过将参数与 undefined
进行比较来检查在函数执行期间是否传递了参数
function showMessage(text) {
// ...
if (text === undefined) { // if the parameter is missing
text = 'empty message';
}
alert(text);
}
showMessage(); // empty message
…或者我们可以使用 ||
运算符
function showMessage(text) {
// if text is undefined or otherwise falsy, set it to 'empty'
text = text || 'empty';
...
}
现代 JavaScript 引擎支持 空值合并运算符 ??
,当大多数假值(例如 0
)应被视为“正常”时,它更好
function showCount(count) {
// if count is undefined or null, show "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('Do you have permission from your 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() { /* empty */ }
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)
)
它将按我们期望的方式工作。
命名函数
函数是动作。因此,它们的名称通常是动词。它应该简短、尽可能准确并描述函数的作用,以便阅读代码的人了解函数的作用。
一种广泛的做法是用一个模糊描述动作的动词前缀开始一个函数。团队内必须就前缀的含义达成一致。
例如,以 "show"
开头的函数通常会显示一些内容。
以…开头的函数
"get…"
– 返回一个值,"calc…"
– 计算一些内容,"create…"
– 创建一些内容,"check…"
– 检查一些内容并返回一个布尔值,等等。
此类名称的示例
showMessage(..) // shows a message
getAge(..) // returns the age (gets it somehow)
calcSum(..) // calculates a sum and returns the result
createForm(..) // creates a form (and usually returns it)
checkPermission(..) // checks a permission, returns true/false
有了前缀,只需看一眼函数名称,就能了解它做什么样的工作以及返回什么样的值。
一个函数应该只做其名称所建议的内容,不要更多。
两个独立的动作通常需要两个函数,即使它们通常一起调用(在这种情况下,我们可以创建一个调用这两个函数的第三个函数)。
违反此规则的一些示例
getAge
– 如果它显示一个带有年龄的alert
(应该只获取),则会很糟糕。createForm
– 如果它修改文档,向其中添加一个表单(应该只创建它并返回),则会很糟糕。checkPermission
– 如果它显示access granted/denied
消息(只应执行检查并返回结果),则会很糟糕。
这些示例假定前缀的常见含义。您和您的团队可以自由地就其他含义达成一致,但通常它们差别不大。无论如何,您都应该牢固地理解前缀的含义,前缀函数可以做什么和不能做什么。所有相同前缀的函数都应遵守规则。团队应共享知识。
函数 == 注释
函数应简短且只做一件事。如果事情很大,也许值得将函数拆分为几个较小的函数。有时遵循此规则可能并不容易,但这绝对是一件好事。
单独的函数不仅更容易测试和调试,其存在本身就是一个很好的注释!
例如,比较下面的两个函数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 ); // a prime
}
}
第二个变体使用附加函数isPrime(n)
来测试素数
function showPrimes(n) {
for (let i = 2; i < n; i++) {
if (!isPrime(i)) continue;
alert(i); // a prime
}
}
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…
等。使用它们来暗示函数的作用。
函数是脚本的主要构建模块。现在我们已经介绍了基础知识,所以我们实际上可以开始创建和使用它们了。但这只是道路的开始。我们将多次回到它们,深入了解其高级特性。
评论
<code>
标签,对于多行代码,请用<pre>
标签将它们包装起来,对于超过 10 行的代码,请使用沙箱(plnkr、jsbin、codepen…)