2022 年 7 月 14 日

函数表达式

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

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

function sayHi() {
  alert( "Hello" );
}

还有一种用于创建函数的语法,称为函数表达式

它允许我们在任何表达式的中间创建一个新函数。

例如

let sayHi = function() {
  alert( "Hello" );
};

这里我们可以看到变量 sayHi 获取一个值,新函数,创建为 function() { alert("Hello"); }

由于函数创建发生在赋值表达式的上下文中(在 = 的右侧),因此这是一个函数表达式

请注意,function 关键字后没有名称。对于函数表达式,允许省略名称。

这里我们立即将其分配给变量,因此这些代码示例的含义相同:“创建一个函数并将其放入变量 sayHi 中”。

在稍后会遇到的更高级的情况下,可以创建函数并立即调用或安排稍后执行,不存储在任何地方,因此保持匿名。

函数是一个值

让我们重申一下:无论如何创建函数,函数都是一个值。以上两个示例都将函数存储在sayHi变量中。

我们甚至可以使用alert打印出该值

function sayHi() {
  alert( "Hello" );
}

alert( sayHi ); // shows the function code

请注意,最后一行不会运行该函数,因为sayHi后面没有括号。在某些编程语言中,任何提到函数名称都会导致其执行,但 JavaScript 并非如此。

在 JavaScript 中,函数是一个值,因此我们可以将其作为值处理。上面的代码显示了它的字符串表示形式,即源代码。

当然,函数是一个特殊值,因为我们可以像sayHi()一样调用它。

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

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

function sayHi() {   // (1) create
  alert( "Hello" );
}

let func = sayHi;    // (2) copy

func(); // Hello     // (3) run the copy (it works)!
sayHi(); // Hello    //     this still works too (why wouldn't it)

以下是上面发生的详细情况

  1. 函数声明(1)创建函数并将其放入名为sayHi的变量中。
  2. (2)行将其复制到变量func中。请再次注意:sayHi后面没有括号。如果有,则func = sayHi()会将调用的结果sayHi()写入func,而不是函数sayHi本身。
  3. 现在可以将函数同时称为sayHi()func()

我们也可以使用函数表达式在第一行声明sayHi

let sayHi = function() { // (1) create
  alert( "Hello" );
};

let func = sayHi;
// ...

一切都将正常工作。

为什么结尾有分号?

你可能会想,为什么函数表达式在结尾有分号;,但函数声明没有

function sayHi() {
  // ...
}

let sayHi = function() {
  // ...
};

答案很简单:函数表达式在此处创建为function(…) {…}在赋值语句内:let sayHi = …;。建议在语句末尾使用分号;,它不是函数语法的组成部分。

分号将用于更简单的赋值,例如let sayHi = 5;,它也用于函数赋值。

回调函数

我们来看更多有关将函数作为值传递和使用函数表达式的示例。

我们将编写一个带有三个参数的函数 ask(question, yes, no)

question
问题的文本
yes
如果答案为“是”则运行的函数
no
如果答案为“否”则运行的函数

该函数应询问 question,并根据用户的答案调用 yes()no()

function ask(question, yes, no) {
  if (confirm(question)) yes()
  else no();
}

function showOk() {
  alert( "You agreed." );
}

function showCancel() {
  alert( "You canceled the execution." );
}

// usage: functions showOk, showCancel are passed as arguments to ask
ask("Do you agree?", showOk, showCancel);

在实践中,此类函数非常有用。实际 ask 与上述示例之间的主要区别在于,实际函数使用比简单 confirm 更复杂的方式与用户交互。在浏览器中,此类函数通常会绘制一个美观的提问窗口。但那是另一个故事了。

ask 的参数 showOkshowCancel 称为回调函数或简称回调

其思想是,我们传递一个函数,并期望在必要时稍后“回调”它。在我们的示例中,showOk 成为“是”答案的回调,showCancel 成为“否”答案的回调。

我们可以使用函数表达式来编写一个等效的、更短的函数

function ask(question, yes, no) {
  if (confirm(question)) yes()
  else no();
}

ask(
  "Do you agree?",
  function() { alert("You agreed."); },
  function() { alert("You canceled the execution."); }
);

在此,函数直接在 ask(...) 调用中声明。它们没有名称,因此被称为匿名。此类函数在 ask 外部不可访问(因为它们未分配给变量),但这正是我们在此处所需要的。

此类代码非常自然地出现在我们的脚本中,它符合 JavaScript 的精神。

函数是一个表示“动作”的值

字符串或数字等常规值表示数据

函数可以被视为动作

我们可以将其传递给变量,并在需要时运行。

函数表达式与函数声明

我们来表述函数声明和表达式之间的主要区别。

首先,语法:如何在代码中区分它们。

  • 函数声明:一个作为单独语句声明的函数,在主代码流中

    // Function Declaration
    function sum(a, b) {
      return a + b;
    }
  • 函数表达式:一个在表达式内或在另一个语法结构内创建的函数。在此,函数在“赋值表达式”= 的右侧创建

    // Function Expression
    let sum = function(a, b) {
      return a + b;
    };

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

当执行到达函数表达式时创建该函数,并且仅从那时起才能使用该函数。

一旦执行流传递到赋值右侧 let sum = function… – 现在开始,函数被创建并且可以被使用(赋值、调用等)。

函数声明是不同的。

函数声明可以在其被定义之前被调用。

例如,全局函数声明在整个脚本中都是可见的,无论它在哪里。

这是由于内部算法。当 JavaScript 准备运行脚本时,它首先在其中查找全局函数声明并创建函数。我们可以将其视为“初始化阶段”。

并且在所有函数声明被处理之后,代码被执行。因此它可以访问这些函数。

例如,这起作用

sayHi("John"); // Hello, John

function sayHi(name) {
  alert( `Hello, ${name}` );
}

函数声明 sayHi 在 JavaScript 准备启动脚本时被创建,并且在脚本中的任何地方都是可见的。

…如果它是一个函数表达式,那么它将不起作用

sayHi("John"); // error!

let sayHi = function(name) {  // (*) no magic any more
  alert( `Hello, ${name}` );
};

函数表达式在执行到达它们时被创建。这只会发生在行 (*) 中。太迟了。

函数声明的另一个特殊功能是它们的块作用域。

在严格模式中,当函数声明在代码块中时,它在该块内的任何地方都是可见的。但不在其外部。

例如,让我们想象我们需要声明一个函数 welcome(),它取决于我们在运行时获得的 age 变量。然后我们计划稍后使用它。

如果我们使用函数声明,它将不会按预期工作

let age = prompt("What is your age?", 18);

// conditionally declare a function
if (age < 18) {

  function welcome() {
    alert("Hello!");
  }

} else {

  function welcome() {
    alert("Greetings!");
  }

}

// ...use it later
welcome(); // Error: welcome is not defined

这是因为函数声明仅在其所在的代码块内可见。

这里有另一个示例

let age = 16; // take 16 as an example

if (age < 18) {
  welcome();               // \   (runs)
                           //  |
  function welcome() {     //  |
    alert("Hello!");       //  |  Function Declaration is available
  }                        //  |  everywhere in the block where it's declared
                           //  |
  welcome();               // /   (runs)

} else {

  function welcome() {
    alert("Greetings!");
  }
}

// Here we're out of curly braces,
// so we can not see Function Declarations made inside of them.

welcome(); // Error: welcome is not defined

我们可以做什么让 welcomeif 外部可见?

正确的方法是使用函数表达式并将 welcome 赋值给在 if 外部声明且具有适当可见性的变量。

此代码按预期工作

let age = prompt("What is your age?", 18);

let welcome;

if (age < 18) {

  welcome = function() {
    alert("Hello!");
  };

} else {

  welcome = function() {
    alert("Greetings!");
  };

}

welcome(); // ok now

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

let age = prompt("What is your age?", 18);

let welcome = (age < 18) ?
  function() { alert("Hello!"); } :
  function() { alert("Greetings!"); };

welcome(); // ok now
何时选择函数声明与函数表达式?

作为经验法则,当我们需要声明一个函数时,首先要考虑的是函数声明语法。它提供了更多自由来组织我们的代码,因为我们可以在声明函数之前调用它们。

这对于可读性也更好,因为在代码中查找 function f(…) {…}let f = function(…) {…}; 更容易。函数声明更“引人注目”。

…但如果出于某种原因函数声明不适合我们,或者我们需要条件声明(我们刚刚看到一个示例),那么应该使用函数表达式。

摘要

  • 函数是值。它们可以在代码的任何位置分配、复制或声明。
  • 如果函数在主代码流中作为单独语句声明,则称为“函数声明”。
  • 如果函数作为表达式的一部分创建,则称为“函数表达式”。
  • 函数声明在执行代码块之前处理。它们在块中的任何位置都是可见的。
  • 函数表达式在执行流到达它们时创建。

在大多数情况下,当我们需要声明一个函数时,函数声明是首选,因为它在声明本身之前是可见的。这给了我们更多的代码组织灵活性,并且通常更具可读性。

因此,我们应该仅在函数声明不适合任务时才使用函数表达式。我们在本章中看到了一些示例,未来还会看到更多。

教程地图

评论

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