在 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)
创建函数并将其放入名为sayHi
的变量中。 - 第
(2)
行将其复制到变量func
中。请再次注意:sayHi
后面没有括号。如果有,则func = sayHi()
会将调用的结果sayHi()
写入func
,而不是函数sayHi
本身。 - 现在可以将函数同时称为
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
的参数 showOk
和 showCancel
称为回调函数或简称回调。
其思想是,我们传递一个函数,并期望在必要时稍后“回调”它。在我们的示例中,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
我们可以做什么让 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(…) {…};
更容易。函数声明更“引人注目”。
…但如果出于某种原因函数声明不适合我们,或者我们需要条件声明(我们刚刚看到一个示例),那么应该使用函数表达式。
摘要
- 函数是值。它们可以在代码的任何位置分配、复制或声明。
- 如果函数在主代码流中作为单独语句声明,则称为“函数声明”。
- 如果函数作为表达式的一部分创建,则称为“函数表达式”。
- 函数声明在执行代码块之前处理。它们在块中的任何位置都是可见的。
- 函数表达式在执行流到达它们时创建。
在大多数情况下,当我们需要声明一个函数时,函数声明是首选,因为它在声明本身之前是可见的。这给了我们更多的代码组织灵活性,并且通常更具可读性。
因此,我们应该仅在函数声明不适合任务时才使用函数表达式。我们在本章中看到了一些示例,未来还会看到更多。
评论
<code>
标记,对于多行 - 将它们包装在<pre>
标记中,对于 10 行以上 - 使用沙盒 (plnkr,jsbin,codepen…)