原文链接:https://javascript.info/function-basics,translate with ❤️ by zhangbao.

我们通常要要在一个脚本里的许多地方进行同样一类操作。

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

函数是程序的主要“构建块”。它们允许多次调用代码而不需要重复书写代码。

我们已经看到很多内置函数的例子,像 alert(message)、prompt(message, default) 和 confirm(question)。但我们也可以创造属于我们自己的函数。

函数声明

创建函数可以使用函数声明的形式。

像这样:

  1. function showMessage() {
  2. alert( 'Hello everyone!' );
  3. }

首先是 function 关键字,然后紧随其后的是函数名,然后是圆括号里的一列参数(上面例子里是空的),最后是函数代码,称为“函数体”,包在花括号里面的。

函数 - 图1

我们的新函数可以用它的名字来调用:showMessage()。

例如:

  1. function showMessage() {
  2. alert( 'Hello everyone!' );
  3. }
  4. showMessage();
  5. showMessage();

调用 showMessage() 只会就会执行函数里的代码。在这里,我们将看到这条消息两次。

这个例子清楚地说明了函数的主要目的之一:避免代码重复。

如果我们需要改变消息或显示的方式,在一个地方修改代码就足够了:修改输出它的函数。

局部变量

函数内部声明的变量只在该函数中可见。

例如:

  1. function showMessage() {
  2. let message = "Hello, I'm JavaScript!"; // 布局变量
  3. alert( message );
  4. }
  5. showMessage(); // Hello, I'm JavaScript!
  6. alert( message ); // <-- Error! 因为变量是在函数内可用的

外部变量

一个函数也可以访问外部变量,例如:

  1. let userName = 'John';
  2. function showMessage() {
  3. let message = 'Hello, ' + userName;
  4. alert(message);
  5. }
  6. showMessage(); // Hello, John

这个函数可以完全访问外部变量,它也可以修改它。

例如:

  1. let userName = 'John';
  2. function showMessage() {
  3. userName = "Bob"; // (1) 修改外部变量
  4. let message = 'Hello, ' + userName;
  5. alert(message);
  6. }
  7. alert( userName ); // 在函数调用之前是 John
  8. showMessage();
  9. alert( userName ); // 函数修改之后变成 Bob

当本地变量不存在时,就会使用外部变量。所以,如果我们忘记加 let 的话,可能就会导致偶然修改。

如果在函数中声明了相同命名的变量,那么它就会这遮蔽部的。例如,下面代码里,函数使用了局部变量 userName。外部变量会被忽略:

  1. let userName = 'John';
  2. function showMessage() {
  3. let userName = "Bob"; // 声明一个局部变量
  4. let message = 'Hello, ' + userName; // Bob
  5. alert(message);
  6. }
  7. // 函数会创建和使用它自己的 userName 变量
  8. showMessage();
  9. alert( userName ); // John, 没有改变, 函数没有使用这个外部变量

⚠️全局变量

在函数之外声明的变量,例如上例中的外部变量 userName,称为全局变量

全局变量可以从任何函数中看到(除非受到局部变量遮蔽了)。

通常,一个函数声明所有特定于其任务的变量。全局变量只储存项目级的数据,所以当重要的是这些变量在任何地方都可以使用。现代代码很少或没有全局变量,大多数变量都存在于它们的函数中。

参数

我们可以使用参数将任意数据传递给函数(也称为函数参数)。

下面例子里,函数包含两个参数:from 和 text。

  1. function showMessage(from, text) { // 参数: from, text
  2. alert(from + ': ' + text);
  3. }
  4. showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
  5. showMessage('Ann', "What's up?"); // Ann: What's up? (**)

当在 () 和 (*) 处调用函数的时候,指定的值会赋值给局部变量 from 和 text。函数会使用它们。

再举一个例子:我们有一个变量 from,将它传递给函数。请注意:函数修改了 from,但是修改对外并不可见,因为函数总是得到一个值的副本:

  1. function showMessage(from, text) {
  2. from = '*' + from + '*'; // make "from" look nicer
  3. alert( from + ': ' + text );
  4. }
  5. let from = "Ann";
  6. showMessage(from, "Hello"); // *Ann*: Hello
  7. // "from" 还是一样的, 函数仅修改了本地副本
  8. alert( from ); // Ann

默认值

如果参数没有提供,那么它的值就是 undefined。

例如,之前提及的函数 showMessage(from, text) 可以在调用时仅提供一个参数:

  1. showMessage("Ann");

不会产生错误。这样的调用会输出 “Ann: undefined”,没有提供 text,所以就认为 text === undefined。

在这种情况下,如果想使用“默认”的 text,我们可以用 = 指定:

  1. function showMessage(from, text = "no text given") {
  2. alert( from + ": " + text );
  3. }
  4. showMessage("Ann"); // Ann: no text given

现在 text 参数没有传递,就会使用默认值 “no text given”。

这里的 “no text given” 是一个字符串,但是可以是更复杂的表达式,它只在参数丢失的情况下才会计算。因此,这样也是可能的:

  1. function showMessage(from, text = anotherFunction()) {
  2. // anotherFunction() 仅在没有提供 text 的情况下才会执行
  3. // 它的返回结果就是作为 text 的值了
  4. }

⚠️旧式的默认参数

旧版本的JavaScript不支持默认参数。所以有其他的方法来支持它们,你可以在旧的脚本中找到它们。

例如,明确检查是否为 undefined:

  1. function showMessage(from, text) {
  2. if (text === undefined) {
  3. text = 'no text given';
  4. }
  5. alert( from + ": " + text );
  6. }

或者是 || 运算符:

  1. function showMessage(from, text) {
  2. // if text is falsy then text gets the "default" value
  3. text = text || 'no text given';
  4. ...
  5. }

返回值

一个函数可以作为结果返回到调用代码中的值。

最简单的例子是一个函数,它求两个值的和:

  1. function sum(a, b) {
  2. return a + b;
  3. }
  4. let result = sum(1, 2);
  5. alert( result ); // 3

指令 return 可以在函数的任何位置使用。当执行到达它时,函数就停止,值返回给调用代码(就是上面的赋值给 result)。

在一个函数里可能会出现很多 return。例如:

  1. function checkAge(age) {
  2. if (age > 18) {
  3. return true;
  4. } else {
  5. return confirm('Got a permission from the parents?');
  6. }
  7. }
  8. let age = prompt('How old are you?', 18);
  9. if ( checkAge(age) ) {
  10. alert( 'Access granted' );
  11. } else {
  12. alert( 'Access denied' );
  13. }

你也可以直接使用 return,后面不用跟值。这导致函数立即退出。

例如:

  1. function showMovie(age) {
  2. if ( !checkAge(age) ) {
  3. return;
  4. }
  5. alert( "Showing you the movie" ); // (*)
  6. // ...
  7. }

上面例子里,如果 checkAge(age) 返回 false,showMovie 函数不会处理到 alert 这一句。

⚠️一个函数直接使用 return 或者不使用,就等同于 return undefined

如果一个函数没有返回值,就等同于返回 undefined:

  1. function doNothing() { /* empty */ }
  2. alert( doNothing() === undefined ); // true

空的 return 也等同于 return undefiend

  1. function doNothing() {
  2. return;
  3. }
  4. alert( doNothing() === undefined ); // true

⚠️不要再 return 和值之间换行

对于一个长的表达式,它可能很容易把它放在一个单独的行中,像这样:

  1. return
  2. (some + long + expression + or + whatever * f(a) + f(b))

这是行不通的,因为 JavaScript 在 return 后会有一个分号。它的工作原理是这样的:

  1. return;
  2. (some + long + expression + or + whatever * f(a) + f(b))

所以,它实际上变成了一个空的返回。我们应该把这个值放在同一行上。

命名函数

函数是操作,所以他们的名字通常是一个动词。它应该简短,但尽可能准确地描述函数的作用。为了让读代码的人更容易理解。

这是一种普遍的做法,用一种语言前缀来开始一个函数,它模糊地描述了动作。团队内部必须就前缀的含义达成一致。

例如,以“show”开头的函数通常显示一些东西。

函数以一下开头:

  • “get…”:返回一个值,

  • “calc…”:计算什么,

  • “create…”:创建什么,

  • “check…”:检查什么,并且返回一个布尔值,等。

这类名字的例子:

  1. showMessage(..) // shows a message
  2. getAge(..) // returns the age (gets it somehow)
  3. calcSum(..) // calculates a sum and returns the result
  4. createForm(..) // creates a form (and usually returns it)
  5. checkPermission(..) // checks a permission, returns true/false

有了前缀,对函数名的一瞥就能理解它所做的工作以及它返回的值。

⚠️一个函数,一个行为

一个函数应该按照它的名字来做,不要再做其他的了。

两个独立的动作通常应该有两个功能,即使它们通常被调用在一起(在这种情况下,我们可以做一个调用这两个函数的第三个函数)

打破这一规则的几个例子:

  • getAge:如果展示一个 alert 显示年龄的话就很坏了(应该只是获取)。

  • createForm:如果还修改了文档的话,向其添加表单的话就很坏了(应该只是创建并且返回它)。

  • checkPermission:如果显示 access granted/denied 信息的话就很坏了(应该只执行检查并返回结果)。

这些例子假设前缀的通用含义。他们对你的意义是由你和你的团队决定的。也许你的代码有不同的行为是很正常的。但是你应该对前缀的含义有一个明确的理解,一个前缀的函数可以做什么,不能做什么。所有同前缀的函数都应该遵守规则。团队应该分享这些知识。

⚠️ 极短函数名

经常使用的函数有时会有超短的名称。

例如,jQuery 框架定义了函数 $,LoDash 库将其核心函数命名成 _。

函数等于注释

函数应该是短的,并且只做一件事。如果这个东西很大,也许把这个函数分解成几个小的函数是值得的。有时候遵循这条规则可能并不那么容易,但这绝对是一件好事。

一个单独的函数不仅更容易测试和调试——它的存在是一个很好的注释!

例如,比较下面的两个函数 showPrimes(n),功能是一样的,都是输出 n 以下的所有质数

第一个变体使用一个标签:

  1. function showPrimes(n) {
  2. nextPrime: for (let i = 2; i < n; i++) {
  3. for (let j = 2; j < i; j++) {
  4. if (i % j == 0) continue nextPrime;
  5. }
  6. alert( i ); // 素数
  7. }
  8. }

第二个标题则使用例如另一个函数 isPrime(n) 来测试是否为素数:

  1. function showPrimes(n) {
  2. for (let i = 2; i < n; i++) {
  3. if (!isPrime(i)) continue;
  4. alert(i); // a prime
  5. }
  6. }
  7. // 是素数吗
  8. function isPrime(i) {
  9. for (let j = 2; j < i; j++) {
  10. if ( i % j == 0) return false;
  11. }
  12. return true;
  13. }

第二个变体更容易理解,不是吗?我们看到的不是代码片段而是动作的名称(isPrime)。有时人们把这种代码称为自我描述

所以,即使我们不打算重用它们,也可以创建函数。它们构造代码并使其可读。

总结

函数声明如下:

  1. function name(parameters, delimited, by, comma) {
  2. /* 代码 */
  3. }
  • 作为参数给函数的值会被复制到局部变量里。

  • 一个函数可以访问外部变量。但它只适用于内外。函数外面的代码不能看到它的局部变量。

  • 一个函数可以返回一个值。如果它没有,那么它的结果就是 undefiend。

为了使代码干净且易于理解,建议在函数中使用局部变量和参数,而不是外部变量。

它总是更容易理解一个函数,它获取参数,与它们一起工作,并返回一个结果,而不是一个没有参数的函数,而是将外部变量作为副作用进行修改。

函数命名:

  • 名称应该清楚地描述函数的作用。当我们在代码中看到一个函数调用时,一个好的名称会立即让我们理解它的作用和返回。

  • 函数是一个动作,所以函数名通常是口头的。

  • 有许多众所周知的函数前缀,像 create…,show…,get…,check… 等等。用它们来暗示一个函数的作用。

函数是脚本的主要构建块。现在我们已经介绍了基础知识,所以我们可以开始创建和使用它们。但这只是这条道路的开始。我们将会多次返回他们,更深入地研究他们的高级特性。

(完)