一.函数
1.函数声明
使用 函数声明 创建函数。
看起来就像这样:
function showMessage() {
alert( 'Hello everyone!' );
}
function
关键字首先出现,然后是 函数名,然后是括号之间的 参数 列表(用逗号分隔,在上述示例中为空,我们将在接下来的示例中看到),最后是花括号之间的代码(即“函数体”)。
function name(parameter1, parameter2, ... parameterN) {
...body...
}
我们的新函数可以通过名称调用:showMessage()
。
例如:
function showMessage() {
alert( 'Hello everyone!' );
}
showMessage();
showMessage();
调用 showMessage()
执行函数的代码。这里我们会看到显示两次消息。
这个例子清楚地演示了函数的主要目的之一:避免代码重复。
如果我们需要更改消息或其显示方式,只需在一个地方修改代码:输出它的函数。
2.局部变量
在函数中声明的变量只在该函数内部可见。
例如:
function showMessage() {
let message = "Hello, I'm JavaScript!"; // 局部变量
alert( message );
}
showMessage(); // Hello, I'm JavaScript!
alert( message ); // <-- 错误!变量是函数的局部变量
3.外部变量
3.外部变量
函数也可以访问外部变量,例如:
let userName = 'John';
function showMessage() {
let message = 'Hello, ' + userName;
alert(message);
}
showMessage(); // Hello, John
函数对外部变量拥有全部的访问权限。函数也可以修改外部变量。
例如:
let userName = 'John';
function showMessage() {
userName = "Bob"; // (1) 改变外部变量
let message = 'Hello, ' + userName;
alert(message);
}
alert( userName ); // John 在函数调用之前
showMessage();
alert( userName ); // Bob,值被函数修改了
只有在没有局部变量的情况下才会使用外部变量。
如果在函数内部声明了同名变量,那么函数会 遮蔽 外部变量。例如,在下面的代码中,函数使用局部的 userName
,而外部变量被忽略:
let userName = 'John';
function showMessage() {
let userName = "Bob"; // 声明一个局部变量
let message = 'Hello, ' + userName; // Bob
alert(message);
}
// 函数会创建并使用它自己的 userName
showMessage();
alert( userName ); // John,未被更改,函数没有访问外部变量。
全局变量
任何函数之外声明的变量,例如上述代码中的外部变量
userName
,都被称为 全局 变量。全局变量在任意函数中都是可见的(除非被局部变量遮蔽)。
减少全局变量的使用是一种很好的做法。现代的代码有很少甚至没有全局变量。大多数变量存在于它们的函数中。但是有时候,全局变量能够用于存储项目级别的数据。
4.参数
我们可以通过参数将任意数据传递给函数。
在如下示例中,函数有两个参数:from
和 text
。
function showMessage(from, text) { // 参数:from 和 text
alert(from + ': ' + text);
}
showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
showMessage('Ann', "What's up?"); // Ann: What's up? (**)
当函数在 (*)
和 (**)
行中被调用时,给定值被复制到了局部变量 from
和 text
。然后函数使用它们进行计算。
这里还有一个例子:我们有一个变量 from
,并将它传递给函数。请注意:函数会修改 from
,但在函数外部看不到更改,因为函数修改的是复制的变量值副本:
function showMessage(from, text) {
from = '*' + from + '*'; // 让 "from" 看起来更优雅
alert( from + ': ' + text );
}
let from = "Ann";
showMessage(from, "Hello"); // *Ann*: Hello
// "from" 值相同,函数修改了一个局部的副本。
alert( from ); // Ann
当一个值被作为函数参数(parameter)传递时,它也被称为 参数(argument)。
换一种方式,我们把这些术语搞清楚:
- 参数(parameter)是函数声明中括号内列出的变量(它是函数声明时的术语)。
- 参数(argument)是调用函数时传递给函数的值(它是函数调用时的术语)。
我们声明函数时列出它们的参数(parameters),然后调用它们传递参数(arguments)。
在上面的例子中,我们可以说:“函数 showMessage
被声明,并且带有两个参数(parameters),随后它被调用,两个参数(arguments)分别为 from
和 "Hello"
”。
5.默认值
如果一个函数被调用,但有参数(argument)未被提供,那么相应的值就会变成 undefined
。
默认参数的计算
在 JavaScript 中,每次函数在没带个别参数的情况下被调用,默认参数会被计算出来。
在上面的例子中,如果传递了参数
text
,那么anotherFunction()
就不会被调用。如果没传递参数
text
,那么anotherFunction()
就会被调用。
在 JavaScript 老代码中的默认参数
几年前,JavaScript 不支持默认参数的语法。所以人们使用其他方式来设置默认参数。
如今,我们会在旧代码中看到它们。
例如,显式地检查 undefined
:
function showMessage(from, text) {
if (text === undefined) {
text = 'no text given';
}
alert( from + ": " + text );
}
……或者使用 ||
运算符:
function showMessage(from, text) {
// 如果 text 的值为假值,则分配默认值
// 这样赋值 text == "" 与 text 无值相同
text = text || 'no text given';
...
}
后备的默认数
有些时候,将参数默认值的设置放在函数执行(相较更后期)而不是函数声明时,也行得通。
我们可以通过将参数与 undefined
进行比较,来检查该参数是否在函数执行期间被传递进来:
function showMessage(text) {
// ...
if (text === undefined) { // 如果参数未被传递进来
text = 'empty message';
}
alert(text);
}
showMessage(); // empty message
……或者我们可以使用 ||
运算符:
function showMessage(text) {
// 如果 text 为 undefined 或者为假值,那么将其赋值为 'empty'
text = text || 'empty';
...
}
现代 JavaScript 引擎支持 空值合并运算符 ??
,它在大多数假值(例如 0
)应该被视为“正常值”时更具优势:
function showCount(count) {
// 如果 count 为 undefined 或 null,则提示 "unknown"
alert(count ?? "unknown");
}
showCount(0); // 0
showCount(null); // unknown
showCount(); // unknown
6.返回值
空值的
return
或没有return
的函数返回值为undefined
如果函数无返回值,它就会像返回
undefined
一样:function doNothing() { /* 没有代码 */ } alert( doNothing() === undefined ); // true
空值的
return
和return undefined
等效:function doNothing() { return; } alert( doNothing() === undefined ); // true
不要在
return
与返回值之间添加新行对于
return
的长表达式,可能你会很想将其放在单独一行,如下所示:return (some + long + expression + or + whatever * f(a) + f(b))
但这不行,因为 JavaScript 默认会在
return
之后加上分号。上面这段代码和下面这段代码运行流程相同:return; (some + long + expression + or + whatever * f(a) + f(b))
因此,实际上它的返回值变成了空值。
如果我们想要将返回的表达式写成跨多行的形式,那么应该在
return
的同一行开始写此表达式。或者至少按照如下的方式放上左括号:return ( some + long + expression + or + whatever * f(a) + f(b) )
然后它就能像我们预想的那样正常运行了。
7.函数命名
函数就是行为(action)。所以它们的名字通常是动词。它应该简短且尽可能准确地描述函数的作用。这样读代码的人就能清楚地知道这个函数的功能。
一种普遍的做法是用动词前缀来开始一个函数,这个前缀模糊地描述了这个行为。团队内部必须就前缀的含义达成一致。
例如,以 "show"
开头的函数通常会显示某些内容。
函数以 XX 开始……
"get…"
—— 返回一个值,"calc…"
—— 计算某些内容,"create…"
—— 创建某些内容,"check…"
—— 检查某些内容并返回 boolean 值,等。
这类名字的示例:
showMessage(..) // 显示信息
getAge(..) // 返回 age(gets it somehow)
calcSum(..) // 计算求和并返回结果
createForm(..) // 创建表单(通常会返回它)
checkPermission(..) // 检查权限并返回 true/false
有了前缀,只需瞥一眼函数名,就可以了解它的功能是什么,返回什么样的值。
一个函数 —— 一个行为
一个函数应该只包含函数名所指定的功能,而不是做更多与函数名无关的功能。
两个独立的行为通常需要两个函数,即使它们通常被一起调用(在这种情况下,我们可以创建第三个函数来调用这两个函数)。
有几个违反这一规则的例子:
getAge
—— 如果它通过alert
将 age 显示出来,那就有问题了(只应该是获取)。createForm
—— 如果它包含修改文档的操作,例如向文档添加一个表单,那就有问题了(只应该创建表单并返回)。checkPermission
—— 如果它显示access granted/denied
消息,那就有问题了(只应执行检查并返回结果)。这些例子假设函数名前缀具有通用的含义。你和你的团队可以自定义这些函数名前缀的含义,但是通常都没有太大的不同。无论怎样,你都应该对函数名前缀的含义、带特定前缀的函数可以做什么以及不可以做什么有深刻的了解。所有相同前缀的函数都应该遵守相同的规则。并且,团队成员应该形成共识。
非常短的函数命名
常用的函数有时会有非常短的名字。
例如,jQuery 框架用
$
定义一个函数。LoDash 库的核心函数用_
命名。这些都是例外,一般而言,函数名应简明扼要且具有描述性。
8.函数 == 注释
函数应该简短且只有一个功能。如果这个函数功能复杂,那么把该函数拆分成几个小的函数是值得的。有时候遵循这个规则并不是那么容易,但这绝对是件好事。
一个单独的函数不仅更容易测试和调试 —— 它的存在本身就是一个很好的注释!
例如,比较如下两个函数 showPrimes(n)
。它们的功能都是输出到 n
的 素数。
第一个变体使用了一个标签:
function showPrimes(n) {
nextPrime: for (let i = 2; i < n; i++) {
for (let j = 2; j < i; j++) {
if (i % j == 0) continue nextPrime;
}
alert( i ); // 一个素数
}
}
第二个变体使用附加函数 isPrime(n)
来检验素数:
function showPrimes(n) {
for (let i = 2; i < n; i++) {
if (!isPrime(i)) continue;
alert(i); // 一个素数
}
}
function isPrime(n) {
for (let i = 2; i < n; i++) {
if ( n % i == 0) return false;
}
return true;
}
第二个变体更容易理解,不是吗?我们通过函数名(isPrime
)就可以看出函数的行为,而不需要通过代码。人们通常把这样的代码称为 自描述。
因此,即使我们不打算重用它们,也可以创建函数。函数可以让代码结构更清晰,可读性更强。
9.总结
函数声明方式如下所示:
function name(parameters, delimited, by, comma) {
/* code */
}
- 作为参数传递给函数的值,会被复制到函数的局部变量。
- 函数可以访问外部变量。但它只能从内到外起作用。函数外部的代码看不到函数内的局部变量。
- 函数可以返回值。如果没有返回值,则其返回的结果是
undefined
。
为了使代码简洁易懂,建议在函数中主要使用局部变量和参数,而不是外部变量。
与不获取参数但将修改外部变量作为副作用的函数相比,获取参数、使用参数并返回结果的函数更容易理解。
函数命名:
- 函数名应该清楚地描述函数的功能。当我们在代码中看到一个函数调用时,一个好的函数名能够让我们马上知道这个函数的功能是什么,会返回什么。
- 一个函数是一个行为,所以函数名通常是动词。
- 目前有许多优秀的函数名前缀,如
create…
、show…
、get…
、check…
等等。使用它们来提示函数的作用吧。
函数是脚本的主要构建块。现在我们已经介绍了基本知识,现在我们就可以开始创建和使用函数了。但这只是学习和使用函数的开始。我们将继续学习更多函数的相关知识,更深入地研究它们的先进特征。
二.箭头函数
创建函数还有另外一种非常简单的语法,并且这种方法通常比函数表达式更好。
它被称为“箭头函数”,因为它看起来像这样:
let func = (arg1, arg2, ..., argN) => expression;
这里创建了一个函数 func
,它接受参数 arg1..argN
,然后使用参数对右侧的 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
可以看到 (a, b) => a + b
表示一个函数接受两个名为 a
和 b
的参数。在执行时,它将对表达式 a + b
求值,并返回计算结果。
-
如果我们只有一个参数,还可以省略掉参数外的圆括号,使代码更短。
例如:
let double = n => n * 2;
// 差不多等同于:let double = function(n) { return n * 2 }
alert( double(3) ); // 6
如果没有参数,括号则是空的(但括号必须保留):
let sayHi = () => alert("Hello!");
sayHi();
箭头函数可以像函数表达式一样使用。
例如,动态创建一个函数:
let age = prompt("What is your age?", 18);
let welcome = (age < 18) ?
() => alert('Hello!') :
() => alert("Greetings!");
welcome();
1.多行的箭头函数
到目前为止,我们看到的箭头函数非常简单。它们从 =>
的左侧获取参数,计算并返回右侧表达式的计算结果。
有时我们需要更复杂一点的函数,比如带有多行的表达式或语句。在这种情况下,我们可以使用花括号将它们括起来。主要区别在于,用花括号括起来之后,需要包含 return
才能返回值(就像常规函数一样)。
就像这样:
let sum = (a, b) => { // 花括号表示开始一个多行函数
let result = a + b;
return result; // 如果我们使用了花括号,那么我们需要一个显式的 “return”
};
alert( sum(1, 2) ); // 3
2.总结
箭头函数对于简单的操作很方便,特别是对于单行的函数。它具体有两种形式:
- 不带花括号:
(...args) => expression
—— 右侧是一个表达式:函数计算表达式并返回其结果。如果只有一个参数,则可以省略括号,例如n => n*2
。 - 带花括号:
(...args) => { body }
—— 花括号允许我们在函数中编写多个语句,但是我们需要显式地return
来返回一些内容。