2022 年 10 月 14 日

函数

我们经常需要在脚本的许多地方执行类似的操作。

例如,当访问者登录、注销,或者在其他地方时,我们需要显示一条漂亮的提示消息。

函数是程序的主要“构建块”。它们允许在不重复的情况下多次调用代码。

我们已经看到了内置函数的示例,例如 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)称为全局变量。

全局变量对任何函数都可见(除非被局部变量遮蔽)。

尽量减少使用全局变量是一种好习惯。现代代码很少或没有全局变量。大多数变量驻留在其函数中。不过,有时它们可用于存储项目级数据。

参数

我们可以使用参数将任意数据传递给函数。

在下面的示例中,函数有两个参数:fromtext

function showMessage(from, text) { // parameters: from, text
  alert(from + ': ' + text);
}

showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
showMessage('Ann', "What's up?"); // Ann: What's up? (**)

当在行(*)(**)中调用函数时,给定的值将复制到局部变量fromtext。然后函数使用它们。

这里还有另一个示例:我们有一个变量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 代码中的默认参数

几年前,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消息(只应执行检查并返回结果),则会很糟糕。

这些示例假定前缀的常见含义。您和您的团队可以自由地就其他含义达成一致,但通常它们差别不大。无论如何,您都应该牢固地理解前缀的含义,前缀函数可以做什么和不能做什么。所有相同前缀的函数都应遵守规则。团队应共享知识。

超短函数名称

非常频繁使用的函数有时具有超短名称。

例如,jQuery框架定义了一个带有$的函数。Lodash库的核心函数名为_

这些是例外。通常函数名称应简洁且具有描述性。

函数 == 注释

函数应简短且只做一件事。如果事情很大,也许值得将函数拆分为几个较小的函数。有时遵循此规则可能并不容易,但这绝对是一件好事。

单独的函数不仅更容易测试和调试,其存在本身就是一个很好的注释!

例如,比较下面的两个函数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… 等。使用它们来暗示函数的作用。

函数是脚本的主要构建模块。现在我们已经介绍了基础知识,所以我们实际上可以开始创建和使用它们了。但这只是道路的开始。我们将多次回到它们,深入了解其高级特性。

任务

重要性:4

如果参数 age 大于 18,则以下函数返回 true

否则,它会要求确认并返回其结果

function checkAge(age) {
  if (age > 18) {
    return true;
  } else {
    // ...
    return confirm('Did parents allow you?');
  }
}

如果删除 else,函数是否会以不同的方式工作?

function checkAge(age) {
  if (age > 18) {
    return true;
  }
  // ...
  return confirm('Did parents allow you?');
}

这两个变体的行为是否有任何不同?

没有区别!

在这两种情况下,return confirm('Did parents allow you?') 都在 if 条件为假时执行。

重要性:4

如果参数 age 大于 18,则以下函数返回 true

否则,它会要求确认并返回其结果。

function checkAge(age) {
  if (age > 18) {
    return true;
  } else {
    return confirm('Did parents allow you?');
  }
}

重写它,执行相同操作,但不用 if,在一行中完成。

制作两个 checkAge 变体

  1. 使用问号运算符 ?
  2. 使用 OR ||

使用问号运算符 '?'

function checkAge(age) {
  return (age > 18) ? true : confirm('Did parents allow you?');
}

使用 OR ||(最短变体)

function checkAge(age) {
  return (age > 18) || confirm('Did parents allow you?');
}

请注意,这里不需要围绕 age > 18 的括号。它们的存在是为了提高可读性。

重要性:1

编写一个函数 min(a,b),它返回两个数字 ab 中的最小值。

例如

min(2, 5) == 2
min(3, -1) == -1
min(1, 1) == 1

使用 if 的解决方案

function min(a, b) {
  if (a < b) {
    return a;
  } else {
    return b;
  }
}

使用问号运算符 '?' 的解决方案

function min(a, b) {
  return a < b ? a : b;
}

附注:在相等的情况下 a == b,返回什么并不重要。

重要性:4

编写一个函数 pow(x,n),它返回 xn 次方。或者换句话说,将 x 乘以它本身 n 次并返回结果。

pow(3, 2) = 3 * 3 = 9
pow(3, 3) = 3 * 3 * 3 = 27
pow(1, 100) = 1 * 1 * ...* 1 = 1

创建一个网页,提示输入 xn,然后显示 pow(x,n) 的结果。

运行演示

附注:在此任务中,函数仅支持 n 的自然值:从 1 开始的整数。

function pow(x, n) {
  let result = x;

  for (let i = 1; i < n; i++) {
    result *= x;
  }

  return result;
}

let x = prompt("x?", '');
let n = prompt("n?", '');

if (n < 1) {
  alert(`Power ${n} is not supported, use a positive integer`);
} else {
  alert( pow(x, n) );
}
教程地图

评论

在评论之前阅读此内容...
  • 如果您有改进建议,请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行代码,请用 <pre> 标签将它们包装起来,对于超过 10 行的代码,请使用沙箱(plnkrjsbincodepen…)