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

在 JavaScript 中,函数不是一种“神奇的语言结构”,而是一种特殊的值。

我们之前使用的语言称为函数声明

  1. function sayHi() {
  2. alert( "Hello" );
  3. }

另外一种创建函数的语法称为函数表达式

看起来是这样的:

  1. let sayHi = function() {
  2. alert( "Hello" );
  3. };

这里,函数创建后,显式的赋值给了一个变量,像使用其他值一样。不论函数时怎么定义的,它也只是存储在变量 sayHi 里的一个值而已。

我们可以使用 alert 来输出这个值:

  1. function sayHi() {
  2. alert( "Hello" );
  3. }
  4. alert( sayHi ); // 显式函数代码

注意,最后一行不是执行函数,因为 sayHi 后面并没有使用圆括号。有一些编程语言,其中提到函数名会导致它的执行,但是 JavaScript 不是这样的。

在 JavaScript 中,函数是一个值,所以我们可以把它当作一个值来处理。上面的代码展示了它的字符串表示法,也就是源代码。

当然,这是一种特殊的价值,从某种意义上说,我们可以称之为 sayHi()。

但它仍然是一个值,所以我们可以像其他类型的值一样处理它。

我们可以将一个函数复制到另一个变量:

  1. function sayHi() { // (1) 创建
  2. alert( "Hello" );
  3. }
  4. let func = sayHi; // (2) 复制
  5. func(); // Hello // (3) 执行副本 (正常执行了)!
  6. sayHi(); // Hello // 这也能正常执行 (为什么不呢)

以下是上面的细节:

  1. (1) 处函数声明创建了一个函数,并将函数赋值给了变量 sayHi。

  2. (2) 处复制了此函数到另一个变量 func。

要注意的是,在 sayHi 之后没有圆括号,如果加了,func = sayHi() 就是在将 sayHi() 的调用结果写入 func,而不是函数 sayHi 本身。

  1. 现在我们可以通过 sayHi() 和 func() 两个来调用函数了。

注意,我们也可以使用函数表达式来声明 sayHi,在第一行:

  1. let sayHi = function() { ... };
  2. let func = sayHi;
  3. // ...

一切都是一样的,更明显的是,发生了什么,对吗?

⚠️为什么最后有分号?

你可能会有疑问,为什么函数表达式末尾跟了一个分号 ;,而函数声明末尾却没有:

  1. function sayHi() {
  2. // ...
  3. }
  4. let sayHi = function() {
  5. // ...
  6. };

答案很简单:

  • 在代码块和语法结构末尾不需要添加 ;,比如说 if {…}、for {…}、function f {} 等。

  • 函数表达式是在语句里使用的:let sayHi = ..;,这里是作为值,而不是代码块。语句默认的分号是推荐使用的,不管是什么类型的值。所以这里的分号与函数表达式本身没有任何关系,它只是终止语句的一个信号。

回调函数

让我们看更多将函数作为值和使用的函数表达式的例子。

我们写了一个带有 3 个参数的函数 ask(question, yes, no):

question

问题内容。

yes

在回答“Yes”的情况下,执行的函数。

no

在回答“No”的情况下,执行的函数。

函数会询问一个问题 question,然后依据用户会的回答,调用 yes() 或者 no():

  1. function ask(question, yes, no) {
  2. if (confirm(question)) yes()
  3. else no();
  4. }
  5. function showOk() {
  6. alert( "You agreed." );
  7. }
  8. function showCancel() {
  9. alert( "You canceled the execution." );
  10. }
  11. // usage: functions showOk, showCancel are passed as arguments to ask
  12. ask("Do you agree?", showOk, showCancel);

在我们探索如何以更短的方式编写它之前,让我们注意到在浏览器中(在某些情况下,在服务器端)这样的功能是相当流行的。现实生活中的实现和上面的例子之间的主要区别在于,现实生活中的函数使用更复杂的方式与用户交互,而不是简单的 confirm 函数。在浏览器中,这样的函数通常会负责绘制一个好看的问题窗口,但这是另一个故事。

这里的 ask 参数称为回调函数或简称回调

我们的想法是,我们传递一个函数,并期望它在必要时被“回调”。在我们这个场景里,showOK 成为“Yes”回答后的回调,showCancel 则是成为“no”回答后的回调。

我们可以使用函数表达式来写相同的函数,更短:

  1. function ask(question, yes, no) {
  2. if (confirm(question)) yes()
  3. else no();
  4. }
  5. ask(
  6. "Do you agree?",
  7. function() { alert("You agreed."); },
  8. function() { alert("You canceled the execution."); }
  9. );

这里,函数是在 ask(…) 调用时声明的。他们没有名字,也成为匿名函数。这样的函数是无法在 ask 之外调用的(因为没有赋值给一个变量),但这正是我们需要的。

这样的代码很自然地出现在我们的脚本中,这是在 JavaScript 鼓励的。

⚠️函数是一个表示“行为”的值

通常来讲,像字符串、数值这样的值表示的是数据

而一个函数可以表示是一个行为

我们可以将它在变量之间传递,然后在我们需要的时候再去执行它。

函数表达式 VS 函数声明

下面我们来总结一下函数声明和函数表达式之间的不同之处。

首先,是语法:怎么从代码层面上辨别出它们呢?

  • 函数声明:一个函数,在主代码流中,作为单独的语句声明。
  1. // 函数声明
  2. function sum(a, b) {
  3. return a + b;
  4. }
  • 函数表达式:一个函数,在一个表达式里创建或者再另一个语法结构里。这里,函数是在“赋值表达式”= 的右边创建的。
  1. // 函数表达式
  2. let sum = function(a, b) {
  3. return a + b;
  4. };

更微妙的区别是,函数是在何时由 JavaScript 引擎创建。

函数表达式是在执行流到达它的时候才创建,并且从那时才是可用的。

一旦执行流进入到赋值表达式 let sum = function… 的右边时,在这里函数才被创建,然后才开始可用(赋值、调用等)。

函数声明在整个脚本/代码块都是可用的。

换句话说,当 JavaScript 准备运行脚本或代码块时,它首先查找其中的函数声明并创建函数。我们可以把它看作是一个“初始化阶段”。

在处理完所有的函数声明之后,执行才会进行。

因此,声明为函数声明的函数可以比定义的更早调用。

例如,这样就行:

  1. sayHi("John"); // Hello, John
  2. function sayHi(name) {
  3. alert( `Hello, ${name}` );
  4. }

函数声明 sayHi 在 JavaScript 准备执行脚本的时候就创建了,在各处都是可见的。

如果是函数表达式的话,就不行了:

  1. sayHi("John"); // error!
  2. let sayHi = function(name) { // (*) no magic any more
  3. alert( `Hello, ${name}` );
  4. };

函数表达式是在执行到达时创建的。也就是说,在代码流运行到 (*) 处的时候,sayHi 才会被创建。太迟了。

当一个函数声明在一个代码块中进行时,它在那个块中到处可见,但在外面不可见。

有时,只需要在那个块中声明一个局部函数就很方便了,但这一功能也可能带来问题。

下面的代码不起作用:(我:开始胡说八道了,可以呀,我测试)

  1. let age = prompt("What is your age?", 18);
  2. // conditionally declare a function
  3. if (age < 18) {
  4. function welcome() {
  5. alert("Hello!");
  6. }
  7. } else {
  8. function welcome() {
  9. alert("Greetings!");
  10. }
  11. }
  12. // ...use it later
  13. welcome(); // Error: welcome is not defined

那是因为函数声明只在它所在的代码块中可见。

这是另一个例子:

  1. let age = 16; // take 16 as an example
  2. if (age < 18) {
  3. welcome(); // \ (runs)
  4. // |
  5. function welcome() { // |
  6. alert("Hello!"); // | Function Declaration is available
  7. } // | everywhere in the block where it's declared
  8. // |
  9. welcome(); // / (runs)
  10. } else {
  11. function welcome() { // for age = 16, this "welcome" is never created
  12. alert("Greetings!");
  13. }
  14. }
  15. // Here we're out of curly braces,
  16. // so we can not see Function Declarations made inside of them.
  17. welcome(); // Error: welcome is not defined

我们怎么做才能能让 welcome 在 if 之外可见呢?

正确的做法是使用函数表达式,将函数赋值给 welcome 这个在 if 语句之外声明的变量,这样就能保证可见性了。

现在就可以按照预想工作了:

  1. let age = prompt("What is your age?", 18);
  2. let welcome;
  3. if (age < 18) {
  4. welcome = function() {
  5. alert("Hello!");
  6. };
  7. } else {
  8. welcome = function() {
  9. alert("Greetings!");
  10. };
  11. }
  12. welcome(); // ok now

或者我们可以用问号运算符 ? 进一步简化它:

  1. let age = prompt("What is your age?", 18);
  2. let welcome = (age < 18) ?
  3. function() { alert("Hello!"); } :
  4. function() { alert("Greetings!"); };
  5. welcome(); // ok now

⚠️什么时候选择使用函数声明或函数表达式?

一般来说,当我们需要声明一个函数时,首先要考虑的是函数声明语法,这是我们之前用过的。它在如何组织我们的代码方面给予了更多的自由,因为我们可以在声明之前调用这些函数。

以 function f(.,.) {…} 方式的函数声明比 let f = function (…) {…} 这样的函数表达式更容易看懂。函数声明更“引人注目”。

但是如果一个函数声明由于某种原因不适合我们(我们已经见过上面的例子),那么应该使用函数表达式。

箭头函数

还有一种非常简单和简洁的语法来创建函数,这通常比函数表达式要好。它被称为“箭头函数”,因为它看起来是这样的:

  1. let func = (arg1, arg2, ...argN) => expression

这里创建了一个包含参数 arg1..argN 的函数 func,计算右边表达式 expression 的值,然后返回这个值。

也就是说,大致等同于:

  1. let func = function(arg1, arg2, ...argN) {
  2. return expression;
  3. }

但可以更简洁。

比如:

  1. let sum = (a, b) => a + b;
  2. /* 这样的写法是下列的简写形式:
  3. let sum = function(a, b) {
  4. return a + b;
  5. };
  6. */
  7. alert( sum(1, 2) ); // 3

如果我们只有一个参数,那么括号可以省略,使它更短:

  1. // 等同于
  2. // let double = function(n) { return n * 2 }
  3. let double = n => n * 2;
  4. alert( double(3) ); // 6

如果没有参数,括号应该是空的(但它们应该存在):

  1. let sayHi = () => alert("Hello!");
  2. sayHi();

箭头函数可以像函数表达式一样使用。

例如,下面是重写之后的 welcome():

  1. let age = prompt("What is your age?", 18);
  2. let welcome = (age < 18) ?
  3. () => alert('Hello') :
  4. () => alert("Greetings!");
  5. welcome(); // ok now

箭头函数一开始可能看起来不太熟悉,也不太容易读懂,但是随着眼睛适应了结构,它很快就会改变。

它们对于简单的一行操作非常方便,因为我们太懒了,不能写很多单词。

⚠️多行箭头函数

上面的例子使用了 => 左边的值作为参数,并对右边的表达式进行了计算。

有时我们需要一些更复杂的东西,比如多个表达式或语句。这也是可能的,但是我们应该用花括号括起来。在要使用 return 了:

像这样:

  1. let sum = (a, b) => { // 花括号打开了一个多行函数
  2. let result = a + b;
  3. return result; // 如果我们使用了花括号, 就得使用 return 返回结果
  4. };
  5. alert( sum(1, 2) ); // 3

⚠️更多要来

在这里,我们称赞了箭头函数的简洁。但这还不是全部!箭头函数还有其他有趣的特性。我们会在后面的章节《重访箭头函数》回到他们。

现在,我们已经可以将它们用于单行操作和回调。

总结

  • 函数也是值。可以在代码的任何地方被用来赋值、复制或者声明。

  • 如果该函数被声明为主代码流中的单独语句,那就是所谓的“函数声明”。

  • 如果函数是作为表达式的一部分被创建的。那么他就是“函数表达式”。

  • 函数声明在执行代码块之前处理。它们在块的任何地方都是可见的。

  • 函数表达式是在执行流到达它们时创建的。

在大多数情况下,当我们需要申报一个函数时,函数声明是可取的,因为它在声明本身之前是可见的。这使我们在代码组织中有了更大的灵活性,而且通常可读性更强。

因此,只有当函数声明不适合这项任务时,我们才应该使用函数表达式。在这一章中,我们已经看到了几个例子,将来会看到更多。

箭头函数对于一行程序来说很方便。它们有两种方式:

  1. 没有花括号:(…args) => expression:右边是一个表达式:函数求值并返回结果。

  2. 有花括号:(…args) => { body }:括号允许我们在函数中写多个语句,但是我们需要显式使用 return 语句返回值。

(完)