Skip to content
This page has been auto-translated and may contain errors.View in English

函数

docs.scrimba.com

假设你在为购物车中的商品定价。你拿一个数字,加上税,四舍五入,然后用货币符号格式化。然后你需要为下一件商品重复操作,再下一件。到处复制这四个步骤是 bug 的温床:在一个地方修复四舍五入,还有三个复本仍然是错的。函数让你为这个步骤块命名一次,然后在需要时随时运行它。本章讲述的是如何编写这些块、向它们传入输入,以及获取返回值。

声明和调用函数

一个**函数**是一个命名的代码块,你可以随时运行。你只写一次步骤,给它们一个名字,然后可以根据这个名字多次运行它。

你可以用 function 关键字创建一个函数:

js
function greet() {
  console.log("Hi there");
}

这是完整的定义,但还没有执行任何操作。编写函数存储这些步骤;它不运行它们。要实际运行它,你需要通过在它的名字后面加上括号来调用它:

js
greet(); // Hi there
greet(); // Hi there

两行,两个问候,来自一个代码块。定义和调用是分开的事情,混淆它们是初学者首先遇到的问题。

一个**函数**是一个代码块的包装,有一个名字,所以你可以按需运行它。你用 function 关键字定义一次,然后根据需要调用多次:

js
function logDivider() {
  console.log("----------");
}

logDivider(); // ----------
logDivider(); // ----------

定义设置步骤但不运行任何东西。函数只有在你用 () 调用它时才运行。单独的名字 logDivider 是函数值;括号是调用它。这个区别很重要,一旦你开始传递函数,因为 logDividerlogDivider() 是两个不同的东西:一个是函数,另一个是运行它的结果。

一个**函数是一个绑定到名字的可重用代码块。这里的 function 关键字创建一个函数声明**:一个命名函数被提升到其作用域的顶部,这意味着你可以在写它的上面一行调用它,它仍然有效。

js
countdown(); // 有效:声明被提升

function countdown() {
  console.log("Ready");
}

那个提升是声明形式特有的;本章后面定义函数的其他方式不共享它。名字和调用是不同的值:countdown 求值为函数本身(你可以存储它、传递它、比较它),而 countdown() 求值为运行函数产生的任何东西。混淆这两个是大部分"为什么这是 undefined"困惑的原因,现在在函数开始被传递为其他函数的参数之前就习惯让它们分开是值得的。

给函数一个清晰的、动词式的名字,说明它做什么(greetlogDivider),就像你在命名变量时采取的那样。一个在十个地方调用的函数的可读性只有其名字那么好。

Juno声明和调用函数 编写函数存储步骤;它不运行它们。要运行它,你通过名字加括号来调用它,像 greet() 一样。这个分割一开始让我困惑:定义是配方,调用是真正做饭。你可以随时调用同一个函数多次。
Juno声明和调用函数 定义函数和调用它是两个独立的行为。function 块设置步骤,在你添加 () 前不运行任何东西。在你的头脑中保持 greetgreet() 的区别:一个是函数值,另一个是运行它的结果。当你第一次将一个函数传到某个地方时,这个差异会引起问题。
Juno声明和调用函数function 关键字创建一个声明,会被提升,所以你可以在它所在位置上面调用它。这是这种形式独有的;表达式和箭头函数不会以同样的方式被提升。而 countdown 是函数,countdown() 是其结果,当你开始将函数作为值传递时,这个区别变得越来越不重要。

参数和返回值

当你可以向函数传入一些东西时,函数更有用。你传入的值是**参数**:列在括号中的名字,代表你调用它时传入的任何东西。

js
function greet(name) {
  console.log("Hi " + name);
}

greet("张三"); // Hi 张三
greet("李四"); // Hi 李四

name 是参数。当你调用 greet("张三") 时,文本 "张三" 为该次运行插入到 name 中。一个函数,不同的结果,取决于你给它什么。

到目前为止,这些函数打印一些东西。通常你想要一个函数将值交回,以便你以后可以使用它。那是 return 做的:

js
function double(number) {
  return number * 2;
}

const result = double(5);
console.log(result); // 10

return 将值从函数发送出去。double(5) 变成 10,你可以存储它、打印它或继续传递它。打印在屏幕上显示值;返回将其交回到你的代码以继续使用。

函数通过**参数接收输入:括号中的名字。你在调用点传递的值是填充它们的参数**。

js
function priceWithTax(price, rate) {
  return price + price * rate;
}

const total = priceWithTax(40, 0.2);
console.log(total); // 48

pricerate 是参数;400.2 是这次调用的参数。return 语句将值交回给调用函数的人,所以 priceWithTax(40, 0.2) 求值为 48,你可以在 total 中存储它。

打印和返回之间的区别值得确定。调用 console.log 的函数显示你某些东西并求值为 undefined;使用 return 的函数产生你的程序可以继续使用的值。return 也立即结束函数:它后面的任何行都不运行。如果你完全遗漏 return,函数仍然返回 undefined,当你期望得到值但什么都没有得到时,这是常见的惊喜。

参数是函数的命名输入;参数是在调用点绑定到它们的具体值。return 语句产生函数的输出值并立即结束执行,所以它也充当早期退出:

js
function classify(score) {
  if (score < 0) {
    return "invalid";
  }
  if (score >= 60) {
    return "pass";
  }
  return "fail";
}

console.log(classify(72)); // pass
console.log(classify(-3)); // invalid

每个 return 既产生值又停止函数,所以一旦一个分支触发,它下面的任何东西都不运行。那个早期退出的模式(先验证和退出,最后处理主要情况)让你远离深层嵌套的条件语句

在真实代码中重要的区别是在返回的函数和只产生副作用的函数之间(写入控制台、改变自身以外的东西)。console.log 执行副作用,函数求值为 undefinedreturn 产生调用者可以组合的值。没有 return 的函数,或一个空白的 return 后面没有任何东西,产生 undefined。从只记录的函数中寻找返回值是常见的 bug,修复几乎总是 return 值并在调用点记录它。

Juno参数和返回值 参数是括号中的名字,代表你传入的值,所以一个函数可以给出不同的结果。return 将值交回,以便你的代码以后可以使用它。用 console.log 打印在屏幕上显示它;返回将其交回给你。我花了一阵子才感受到这个差异。
Juno参数和返回值 参数是定义中的名字,参数是你在调用时传递的值。return 将值交给调用者并立即结束函数。只记录的函数求值为 undefined,所以如果你期望一个值并什么都没有得到,检查你是否真的返回了它。
Juno参数和返回值return 产生值并退出,这使验证并早期退出成为一个清晰的模式。注意返回值和做副作用(如 console.log)之间的分割:记录函数求值为 undefined。当你寻找结果而什么都没有得到时,你几乎总是记录了你本来是想返回的东西。

箭头函数

有一个更短的方式来写函数,叫做**箭头函数**。你把它存储在一个变量中,用 => 箭头而不是 function 关键字:

js
const add = (a, b) => {
  return a + b;
};

console.log(add(2, 3)); // 5

当函数是像这样的单个表达式时,你可以删除大括号和 return,让箭头为你交回值:

js
const add = (a, b) => a + b;

console.log(add(2, 3)); // 5

两个做同样的事情。你会在现代 JavaScript 中到处看到箭头函数,所以认识形状会有帮助:一个名字,一个 =,输入,一个箭头,和函数体。

**箭头函数**是一个更短的写函数的语法,用 => 存储在一个变量中:

js
const formatPrice = (amount) => `$${amount.toFixed(2)}`;

console.log(formatPrice(9.5)); // $9.50

当函数体是单个表达式时,箭头函数有一个隐式返回:没有大括号,没有 return 关键字,表达式的值自动交回。添加大括号,它的行为像一个正常的函数体,你自己写 return

js
const formatPrice = (amount) => {
  const rounded = amount.toFixed(2);
  return `$${rounded}`;
};

箭头是短的、单一目的函数的习语选择,特别是你传递给其他函数的小函数(你会在数组方法中遇到那些)。对于顶级的、可重用的、命名的例程,一个常规的 function 声明读起来很清楚,工作得很好。当简洁有帮助且函数很小时,选择箭头。

**箭头函数**是用 => 写的紧凑函数表达式。带有单表达式函数体,它有隐式返回(表达式的值被返回,没有 return 关键字);带有大括号函数体,你显式返回,完全像一个常规函数。

js
const applyDiscount = (price, percent) => price * (1 - percent / 100);

console.log(applyDiscount(80, 25)); // 60

定义函数的三种方式共存,区别是实际的,不是化妆性的:

  • 一个函数声明function total() {})被提升,所以它可以在其定义上面被调用。
  • 一个函数表达式const total = function () {})不以相同的方式被提升:变量存在但在行运行前保持 undefined,所以更早调用它会抛出错误。
  • 一个箭头函数const total = () => {})对提升的表现类似,并添加一个下面的关键规则。

设置箭头分开的规则:它们不绑定自己的 this。一个常规函数的 this 由它是如何被调用确定;一个箭头从周围的代码继承 this。这个区别直到你在对象上编写方法时不会造成问题,所以保持事实,推理在那里落地。

两个更多值得使用的工具。默认参数在参数缺失时给一个后备,所以函数保持可预测:

js
const greet = (name = "there") => `Hi ${name}`;

console.log(greet("张三")); // Hi 张三
console.log(greet()); // Hi there

而且付出最多的习惯:偏好纯函数,其输出仅取决于其输入且改变自身外面的任何东西(没有副作用)。上面的 applyDiscount 是纯的:相同的输入,相同的结果,每一次。纯函数是你可以单独测试的、在没有追踪程序其余部分的情况下推理、并重用而没有惊喜的。不是所有东西都可以是纯的(某些东西必须记录、获取和改变状态),但将你的逻辑推向纯函数并在边缘保持副作用是语言中最高回报习惯之一。

Juno箭头函数 箭头函数是一个更短的写函数的方式,存储在一个变量中,用 => 而不是 function 关键字。当函数体是一个表达式时,你可以删除大括号和 return,箭头为你交回值。你会不断看到这个形状,所以快速适应阅读它是值得的。
Juno箭头函数 箭头是函数的短形式,单表达式函数体给你隐式返回:没有大括号,没有 return。添加大括号,你像正常一样写 return。在小的、单一目的函数上选择箭头;一个普通的 function 声明仍然为顶级命名例程读得很好。
Juno箭头函数 声明被提升,表达式和箭头不被提升,箭头也跳过自己的 this,这仅在你写对象方法时重要。对缺少的参数利用默认参数,并将逻辑推向其输出仅取决于其输入的纯函数中。那些是你可以单独测试和重用而不追踪整个程序的。

函数从这里去往何处

函数是你停止重复自己的方式:命名一个步骤块,向它传入输入,得到值回。一旦这点成立,你写的大部分代码变成小函数调用其他函数,每个做一个清晰的工作。

从这里,函数到处显示。你会将它们传递给数组方法来转换数据列表,用它们来组织来自条件语句的分支逻辑,并依靠你从变量开始的命名规范。下一次你发现自己复制一个代码块时,那是将它包装在函数中的信号。