原文链接:https://javascript.info/function-expressions-arrows,translate with ❤️ by zhangbao.
在 JavaScript 中,函数不是一种“神奇的语言结构”,而是一种特殊的值。
我们之前使用的语言称为函数声明:
function sayHi() {
alert( "Hello" );
}
另外一种创建函数的语法称为函数表达式:
看起来是这样的:
let sayHi = function() {
alert( "Hello" );
};
这里,函数创建后,显式的赋值给了一个变量,像使用其他值一样。不论函数时怎么定义的,它也只是存储在变量 sayHi 里的一个值而已。
我们可以使用 alert 来输出这个值:
function sayHi() {
alert( "Hello" );
}
alert( sayHi ); // 显式函数代码
注意,最后一行不是执行函数,因为 sayHi 后面并没有使用圆括号。有一些编程语言,其中提到函数名会导致它的执行,但是 JavaScript 不是这样的。
在 JavaScript 中,函数是一个值,所以我们可以把它当作一个值来处理。上面的代码展示了它的字符串表示法,也就是源代码。
当然,这是一种特殊的价值,从某种意义上说,我们可以称之为 sayHi()。
但它仍然是一个值,所以我们可以像其他类型的值一样处理它。
我们可以将一个函数复制到另一个变量:
function sayHi() { // (1) 创建
alert( "Hello" );
}
let func = sayHi; // (2) 复制
func(); // Hello // (3) 执行副本 (正常执行了)!
sayHi(); // Hello // 这也能正常执行 (为什么不呢)
以下是上面的细节:
(1) 处函数声明创建了一个函数,并将函数赋值给了变量 sayHi。
(2) 处复制了此函数到另一个变量 func。
要注意的是,在 sayHi 之后没有圆括号,如果加了,func = sayHi() 就是在将 sayHi() 的调用结果写入 func,而不是函数 sayHi 本身。
- 现在我们可以通过 sayHi() 和 func() 两个来调用函数了。
注意,我们也可以使用函数表达式来声明 sayHi,在第一行:
let sayHi = function() { ... };
let func = sayHi;
// ...
一切都是一样的,更明显的是,发生了什么,对吗?
⚠️为什么最后有分号?
你可能会有疑问,为什么函数表达式末尾跟了一个分号 ;,而函数声明末尾却没有:
function sayHi() {
// ...
}
let sayHi = function() {
// ...
};
答案很简单:
在代码块和语法结构末尾不需要添加 ;,比如说 if {…}、for {…}、function f {} 等。
函数表达式是在语句里使用的:let sayHi = ..;,这里是作为值,而不是代码块。语句默认的分号是推荐使用的,不管是什么类型的值。所以这里的分号与函数表达式本身没有任何关系,它只是终止语句的一个信号。
回调函数
让我们看更多将函数作为值和使用的函数表达式的例子。
我们写了一个带有 3 个参数的函数 ask(question, yes, no):
question
问题内容。
yes
在回答“Yes”的情况下,执行的函数。
no
在回答“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);
在我们探索如何以更短的方式编写它之前,让我们注意到在浏览器中(在某些情况下,在服务器端)这样的功能是相当流行的。现实生活中的实现和上面的例子之间的主要区别在于,现实生活中的函数使用更复杂的方式与用户交互,而不是简单的 confirm 函数。在浏览器中,这样的函数通常会负责绘制一个好看的问题窗口,但这是另一个故事。
这里的 ask
参数称为回调函数或简称回调。
我们的想法是,我们传递一个函数,并期望它在必要时被“回调”。在我们这个场景里,showOK 成为“Yes”回答后的回调,showCancel 则是成为“no”回答后的回调。
我们可以使用函数表达式来写相同的函数,更短:
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 鼓励的。
⚠️函数是一个表示“行为”的值
通常来讲,像字符串、数值这样的值表示的是数据。
而一个函数可以表示是一个行为。
我们可以将它在变量之间传递,然后在我们需要的时候再去执行它。
函数表达式 VS 函数声明
下面我们来总结一下函数声明和函数表达式之间的不同之处。
首先,是语法:怎么从代码层面上辨别出它们呢?
- 函数声明:一个函数,在主代码流中,作为单独的语句声明。
// 函数声明
function sum(a, b) {
return a + b;
}
- 函数表达式:一个函数,在一个表达式里创建或者再另一个语法结构里。这里,函数是在“赋值表达式”= 的右边创建的。
// 函数表达式
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}` );
};
函数表达式是在执行到达时创建的。也就是说,在代码流运行到 (*) 处的时候,sayHi 才会被创建。太迟了。
当一个函数声明在一个代码块中进行时,它在那个块中到处可见,但在外面不可见。
有时,只需要在那个块中声明一个局部函数就很方便了,但这一功能也可能带来问题。
下面的代码不起作用:(我:开始胡说八道了,可以呀,我测试)
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() { // for age = 16, this "welcome" is never created
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
我们怎么做才能能让 welcome 在 if 之外可见呢?
正确的做法是使用函数表达式,将函数赋值给 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 (…) {…} 这样的函数表达式更容易看懂。函数声明更“引人注目”。
但是如果一个函数声明由于某种原因不适合我们(我们已经见过上面的例子),那么应该使用函数表达式。
箭头函数
还有一种非常简单和简洁的语法来创建函数,这通常比函数表达式要好。它被称为“箭头函数”,因为它看起来是这样的:
let func = (arg1, arg2, ...argN) => expression
这里创建了一个包含参数 arg1..argN 的函数 func,计算右边表达式 expression 的值,然后返回这个值。
也就是说,大致等同于:
let func = function(arg1, arg2, ...argN) {
return expression;
}
但可以更简洁。
比如:
let sum = (a, b) => a + b;
/* 这样的写法是下列的简写形式:
let sum = function(a, b) {
return a + b;
};
*/
alert( sum(1, 2) ); // 3
如果我们只有一个参数,那么括号可以省略,使它更短:
// 等同于
// let double = function(n) { return n * 2 }
let double = n => n * 2;
alert( double(3) ); // 6
如果没有参数,括号应该是空的(但它们应该存在):
let sayHi = () => alert("Hello!");
sayHi();
箭头函数可以像函数表达式一样使用。
例如,下面是重写之后的 welcome():
let age = prompt("What is your age?", 18);
let welcome = (age < 18) ?
() => alert('Hello') :
() => alert("Greetings!");
welcome(); // ok now
箭头函数一开始可能看起来不太熟悉,也不太容易读懂,但是随着眼睛适应了结构,它很快就会改变。
它们对于简单的一行操作非常方便,因为我们太懒了,不能写很多单词。
⚠️多行箭头函数
上面的例子使用了 => 左边的值作为参数,并对右边的表达式进行了计算。
有时我们需要一些更复杂的东西,比如多个表达式或语句。这也是可能的,但是我们应该用花括号括起来。在要使用 return 了:
像这样:
let sum = (a, b) => { // 花括号打开了一个多行函数
let result = a + b;
return result; // 如果我们使用了花括号, 就得使用 return 返回结果
};
alert( sum(1, 2) ); // 3
⚠️更多要来
在这里,我们称赞了箭头函数的简洁。但这还不是全部!箭头函数还有其他有趣的特性。我们会在后面的章节《重访箭头函数》回到他们。
现在,我们已经可以将它们用于单行操作和回调。
总结
函数也是值。可以在代码的任何地方被用来赋值、复制或者声明。
如果该函数被声明为主代码流中的单独语句,那就是所谓的“函数声明”。
如果函数是作为表达式的一部分被创建的。那么他就是“函数表达式”。
函数声明在执行代码块之前处理。它们在块的任何地方都是可见的。
函数表达式是在执行流到达它们时创建的。
在大多数情况下,当我们需要申报一个函数时,函数声明是可取的,因为它在声明本身之前是可见的。这使我们在代码组织中有了更大的灵活性,而且通常可读性更强。
因此,只有当函数声明不适合这项任务时,我们才应该使用函数表达式。在这一章中,我们已经看到了几个例子,将来会看到更多。
箭头函数对于一行程序来说很方便。它们有两种方式:
没有花括号:(…args) => expression:右边是一个表达式:函数求值并返回结果。
有花括号:(…args) => { body }:括号允许我们在函数中写多个语句,但是我们需要显式使用 return 语句返回值。
(完)