参考学习链接:https://zh.javascript.info/
关于 JavaScript 的一些基本常识
如何把代码运行的结果显示出来?
- 方法一 浏览器弹窗 alert、prompt 和 confirm
alert
它会显示一条信息,并等待用户按下 “OK”。
例如:
1 | alert("Hello"); |
弹出的这个带有信息的小窗口被称为 模态窗。“modal” 意味着用户不能与页面的其他部分(例如点击其他按钮等)进行交互,直到他们处理完窗口。在上面示例这种情况下 —— 直到用户点击“确定”按钮。
prompt
函数接收两个参数:
1 | result = prompt(title, [default]); |
浏览器会显示一个带有文本消息的模态窗口,还有 input 框和确定/取消按钮。
title
显示给用户的文本
default
可选的第二个参数,指定 input 框的初始值。
访问者可以在提示输入栏中输入一些内容,然后按“确定”键。然后我们在 result
中获取该文本。或者他们可以按取消键或按 Esc 键取消输入,然后我们得到 null
作为 result
。
prompt
将返回用户在 input
框内输入的文本,如果用户取消了输入,则返回空值 null
。
语法:
1 | result = confirm(question); |
confirm
函数显示一个带有 question
以及确定和取消两个按钮的模态窗口。
点击确定返回 true
,点击取消返回 false
。
例如:
1 | let isBoss = confirm("Are you the boss?"); |
- 方法二 控制台日志 console.log
推荐使用方法二。
1 | console.log(1+2); //输出 3 |
注释
//
:单行注释,在双斜杠的后面的语句全部被当作注释,机器不会翻译,给人看的。
/* */
:这两个符号之间的内容全部被当作注释。与上面双斜杠不同的是,这个适用于多行注释。
现代模式/严格模式 use strict
- 我暂时不清楚它的用途。等将来懂了再回来填坑。
- 用法
全文启动严格模式:将 "use strict"
语句放在整个 JavaScript
文件的第一行。不放在第一行,是无效的。
严格模式一旦开启,没有回头路,只能手动删除语句才能关闭。
- 备注
现代 JavaScript 支持 “class” 和 “module” —— 高级语言结构(本教程后续章节会讲到),它们会自动启用 use strict
。因此,如果我们使用它们,则无需添加 "use strict"
指令。
三种特殊值
在 JavaScript 中,null
、undefined
和 NaN
都是表示“无”或“不是值”的特殊值,但它们各自有不同的用途和含义:
**
null
**:null
是一个故意设计的值,用来表示变量的空值(empty value)。- 它代表一个空对象指针,即没有任何对象可以指向。
- 通常用来表示一个变量没有指向任何对象,或者一个函数没有返回任何对象。
示例:
1
let myVar = null;
**
undefined
**:undefined
表示一个变量已被声明,但没有被赋值。- 它表示变量当前没有值。
- 当函数没有返回值时,也会默认返回
undefined
。
示例:
1
2let myVar;
console.log(myVar); // 输出: undefined**
NaN
**:NaN
表示一个非数值(Not-a-Number)。- 它是一个特殊的值,用来表示一个不可能的或未定义的数学运算结果。
- 任何涉及
NaN
的操作都会返回NaN
。
示例:
1
2let myVar = 0 / 0; // NaN
console.log(myVar); // 输出: NaN
区别
类型:
null
是Object
类型,尽管它表示空值。undefined
是Undefined
类型。NaN
是Number
类型,尽管它表示非数值。
使用场景:
null
通常用于表示空值或故意设置为“无”的情况。undefined
通常用于表示变量尚未初始化。NaN
用于表示数学运算的无效结果。
相等性:
null
和undefined
相等,因为null == undefined
返回true
。NaN
不等于任何值,包括它自己,即NaN == NaN
返回false
。
转换:
- 当
null
和undefined
参与运算时,它们会被转换为0
。 NaN
在运算时通常会导致结果为NaN
。
- 当
检查
- 使用
typeof
运算符可以区分null
和undefined
,因为typeof null
返回"object"
(这是一个历史遗留问题),而typeof undefined
返回"undefined"
。 - 使用
isNaN()
函数可以检查一个值是否为NaN
,但要注意它会把非数字字符串转换为NaN
,所以最好与typeof
结合使用,或者使用Number.isNaN()
方法进行严格检查。
在编程时,应该根据具体场景选择合适的值来表示“无”或“未定义”的状态。
变量
在 JS 中,所有可以由我们自主命名的内容,都可以认为是一个 标识符,像 变量名
函数名
类名
…
使用 变量名
需要遵循如下的命名规范:
变量名
只能含有 字母、数字、下划线、**$**,且不能以数字开头。
例如,定义一个变量,名字为 QQ
,它的值为 123456
。
1 | let QQ = "123456"; |
- 一般使用 驼峰命名法 为变量命名
第一个单词小写,后面每个单词开头大写,例如 myName
、myVeryLongName
。
- 在
JavaScript
中,严格区分大小写。 - 常量命名时,字母一般全部大写。
例如,众所周知,圆周率 PI
约等于 3.14
,
1 | const PI = "3.14" ; |
例如定义某人的生日,
1 | const BIRTHDAY = "2000.10.1" |
有一些英文单词被 JavaScript
官方已经使用,你不能再使用,这种被称作 保留字
。保留字 | MDN
数据类型
JavaScript
中共计 8
种数据类型,其中 7
种是原始数据类型,最后 1
种是数据结构(也称作 对象object
)。
文本串类型
引号 (quotes)
在 js 中的文本串,可以被 单引号
、双引号
、反引号
进行插入使用。
1 | let wenben = '你好,世界' ; //单引号插入文本 |
其中,单引号和双引号的作用一模一样。反引号 可以把语法类似这种的 ${ 代码 }
代码插入文本串中进行运行。
1 | console.log( `我的名字是 ${ 660 +6 }`); //输出 我的名字是 666 |
反引号 还能允许文本串自带 换行。
1 | console.log( ` |
单引号和双引号来自语言创建的古老时代,当时没有考虑到多行字符串的需要。反引号出现较晚,因此更通用。
特殊字符 / 转义字符
所有的特殊字符都以反斜杠字符 \
开始。它也被称为“转义字符”。
反斜杠 \
在 JavaScript 中用于正确读取 字符串,然后消失。内存中的字符串没有 \
。
所以要想显示转义字符的效果,记得使用引号将转义字符括住。
字符 | 描述 |
---|---|
\n |
换行 |
\' , \" |
引号 |
\\ |
反斜线 |
\t |
制表符 |
\xXX |
具有给定十六进制 Unicode XX 的 Unicode 字符,例如:'\x7A' 和 'z' 相同。 |
\uXXXX |
以 UTF-16 编码的十六进制代码 XXXX 的 Unicode 字符,例如 \u00A9 —— 是版权符号 © 的 Unicode。它必须正好是 4 个十六进制数字。 |
\u{X…XXXXXX} (1 到 6 个十六进制字符) |
具有给定 UTF-32 编码的 Unicode 符号。一些罕见的字符用两个 Unicode 符号编码,占用 4 个字节。这样我们就可以插入长代码了。 |
1 | console.log("Hello\nWorld"); // Hello 和 World 中间的 \n 相当于换行 |
字符串长度 length
1 | let wenben = "abcde" ; |
访问字符
代码格式 string[i]
,代表访问字符串 string 的第 i+1
个字符。
1 | let wenben = "你好世界" ; |
遍历文本 对字符串中的每一个字符进行逐个访问的过程
1 | let wenben = "你好世界" ; |
会发现方法 2 更简洁。是的,在 JavaScript 中常用 for ... of
循环来遍历字符串、数组等。
字符串是不可变的
在 JavaScript 中,字符串不可更改。改变字符是不可能的。
我们证明一下为什么不可能:
1 | let str = 'Hi'; |
通常的解决方法是创建一个新的字符串,并将其分配给 str 而不是以前的字符串。
例如:
1 | let str = 'Hi'; |
在接下来的章节,我们将看到更多相关示例。
字母大小写转换
[toLowerCase()
] 和 [toUpperCase()]
方法可以改变字符串中英文字母的大小写。
1 | let wenben = "你好,世jie" ; |
在字符串查找是否包含另一个字符串
此功能又称为 ”在字符串中查找子字符串“。
在 JavaScript 中,有很多方法能实现这个功能。
- 老旧方法 str.indexOf(substring, pos)
它从给定位置 pos
开始(默认从左向右),在 str
中查找 substr
,如果没有找到,则返回 -1
,否则返回匹配成功的位置。
第二个参数 pos 是可选的,不加 pos,默认从头开始匹配,返回第一次匹配成功的位置。
1 | let str = "这是一个测试字符串,测试一下,看看是否能找到所有的测试。"; |
1 | let str = "这是一个测试字符串,测试一下,看看是否能找到所有的测试。"; |
还有一个类似的方法 str.lastIndexOf(substr, position)
,它从字符串的末尾开始搜索到开头。
它会以相反的顺序列出这些事件。
- 现代方法
includes,startsWith,endsWith
现代的方法 str.includes(substr, pos)
根据 str
中是否包含 substr
来返回 true/false
。pos
为可选参数
如果我们 需要检测匹配,但不需要它的位置,那么这是正确的选择:
1 | console.log( "Widget with id".includes("Widget") ); // true |
string.startWith(substr)
检测文本串 string
的字符,是否以子文本串 substr
开头 。
string.endWith(substr)
检测文本串 string
的字符,是否以子文本串 substr
结尾 。
1 | console.log( "Widget".startsWith("Wid") ); // true,"Widget" 以 "Wid" 开始 |
获取子字符串
JavaScript 中有三种获取字符串的方法:substring
、substr
和 slice
。
方法一 str.slice(start [, end])
【推荐使用】
返回字符串从 start
到(但不包括)end
的部分。
例如:
1 | let str = "stringify"; |
如果没有第二个参数,slice
会一直运行到字符串末尾:
1 | let str = "stringify"; |
方法二 str.substring(start [, end])
返回字符串从 start
到(但不包括)end
的部分。
这与 slice
几乎相同,但它允许 start
大于 end
。
例如:
1 | let str = "stringify"; |
不支持负参数(不像 slice),它们被视为 0。
方法三 str.substr(start [, length])
返回字符串从 start 开始的给定 length 的部分。
与以前的方法相比,这个允许我们指定 length 而不是结束位置:
1 | let str = "stringify"; |
我们回顾一下这些方法,以免混淆:
方法 | 选择方式 | 负值参数 |
---|---|---|
slice(start, end) 【推荐使用】 |
从 start 到 end(不含 end) | 允许 |
substring(start, end) | 从 start 到 end(不含 end) | 负值被视为 0 |
substr(start, length) | 从 start 开始获取长为 length 的字符串 | 允许 start 为负数 |
使用哪一个?
它们都可用于获取子字符串。正式一点来讲,substr 有一个小缺点:它不是在 JavaScript 核心规范中描述的,而是在附录 B 中。附录 B 的内容主要是描述因历史原因而遗留下来的仅浏览器特性。因此,理论上非浏览器环境可能无法支持 substr,但实际上它在别的地方也都能用。
相较于其他两个变体,slice 稍微灵活一些,它允许以负值作为参数并且写法更简短。
因此仅仅记住这三种方法中的 slice 就足够了。
数字类型
科学计数法
12345
可以表示为 1.2345e4
,这里的 e4
相当于 10 的 4 次方。
0.008314
可以表示为 8.314e-3
。
数字进制转换
常见的进制有:2 进制,10 进制,16 进制,32 进制。
进制的范围:2
到 36
。默认 10
,毕竟大家都是人类,学的都是十进制 。
为什么进制最大是 36 进制呢?
因为 0~9 在加上 26 个英文字母一共 36,在进制中,大小写是等效的。
给定一个数字 num
,想把它转成另一个进制 base
,需要使用方法 toString
。
num.toString(base)
1 | let number = 123456789; |
请注意
123456789..toString(36)
中的两个点不是打错了。如果我们想直接在一个数字上调用一个方法,比如上面例子中的toString
,那么我们需要在它后面放置两个点..
。如果我们放置一个点:
123456789.toString(36)
,那么就会出现一个 error,因为 JavaScript 语法隐含了第一个点之后的部分为小数部分。如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。也可以写成
(123456789).toString(36)
。
舍入
函数 toFixed(n) 将数字精确到到小数点后 n
位进行四舍五入,并以字符串形式返回结果。
我们可以使用一元加号或 Number()
调用,将其转换为数字,例如 + num.toFixed(5)
。
1 | //这是一个具有 5 位小数的数字。 |
当然 数字函数 中支持更多对数字进行的操作。
检测变量是不是数字
isFinite 和 isNaN
isNaN(value)
将其参数转换为数字,然后测试它是否为 NaN
:
isNaN
函数用来检查其参数是否是非数值(NaN)。- 如果参数是 NaN,或者在将参数转换为数值时无法转换(例如字符串),则返回
true
。 isNaN
会先尝试将传入的参数转换为数字,如果转换失败,才判断为 NaN。
1 | a = 123; |
**isFinite
**:
isFinite
函数用来检查其参数是否是一个有限数值。- 如果参数是有限数值,即不是无穷大(Infinity 或 -Infinity),也不是 NaN,则返回
true
。 isFinite
不会尝试将参数转换为数字,它只检查参数本身的值。
1 | isFinite(Infinity); // false |
区别
- 检查范围:
isNaN
专门用来检查 NaN。isFinite
用来检查数值是否有限,即不是无穷大或 NaN。
- 类型转换:
isNaN
会进行类型转换,尝试将参数转换为数值。isFinite
不会进行类型转换,只检查传入的原始值。
- 用途:
- 当你需要确定一个值是否是 NaN 时,使用
isNaN
。 - 当你需要确定一个值是否是有限数值时,使用
isFinite
。
- 当你需要确定一个值是否是 NaN 时,使用
注意事项
isNaN
函数在没有参数的情况下会返回false
,这是与其他全局函数(如typeof
和isFinite
)不同的行为。- 由于
isNaN
的类型转换特性,如果你想严格检查一个值是否为 NaN,应该使用Number.isNaN()
方法。
在实际编程中,选择 isNaN
还是 isFinite
取决于你要解决的问题和你要检查的值的类型。
- 请注意,在所有数字函数中,包括
isFinite
,空字符串或仅有空格的字符串均被视为0
。
提取文本串中开头的数字
现实生活中,我们经常会有带有单位的值,例如 CSS 中的 "100px"
或 "12pt"
。并且,在很多国家,货币符号是紧随金额之后的,所以我们有 "19¥"
,并希望从中提取出一个数值。
这就是 parseInt
和 parseFloat
的作用。
注意事项
parseInt
和parseFloat
都不会自动转换字符串中的数据类型。如果字符串的第一个字符不能被转换,它们会立即返回NaN
。- 如果没有提供基数给
parseInt
,它将尝试根据字符串来判断基数。例如,如果字符串以"0x"
开头,它将尝试将字符串解析为十六进制数。 - 如果使用
parseInt
解析字符串时提供了基数,但字符串不符合该基数的数值格式,它会返回解析到的错误结果或NaN
。 parseFloat
不能解析科学记数法表示的数字,而parseInt
可以。- 它们都会忽略字符串前面的空格,直到找到第一个非数字字符为止。
在实际使用中,你应该根据需要转换的数值类型来选择合适的函数。如果你需要整数,使用 parseInt
;如果你需要浮点数,使用 parseFloat
。
1 | parseInt("10"); // 10 |
数字函数
学生生涯中常见的基础函数,都覆盖到了。
例如随机数、三角函数、幂函数、对数、标准圆周率等等。
变量的数据类型转换
- 转换成字符串
- 转换成数字
- 转换成布尔
大多数情况下,运算符和函数会自动将赋予它们的值转换为正确的类型。
比如,console.log
会自动将任何值都转换为字符串以进行显示。算术运算符会将值转换为数字。
在某些情况下,我们需要将值主动地转换为我们期望的类型。
转换成字符串
我们也可以显式地调用 String(value)
来将 value
转换为字符串类型:
1 | let value = true; |
字符串转换最明显。false
变成 "false"
,null
变成 "null"
等。
转换成数字
在算术函数和表达式中,会自动进行 number 类型转换。
比如,当把除法 /
用于非 number 类型:
1 | console.log( "6" / "2" ); // 3, string 类型的值被自动转换成 number 类型后进行计算 |
我们也可以使用 Number(value)
显式地将这个 value
转换为 number 类型。
1 | let str = "123"; |
当我们从 string 类型源(如文本表单)中读取一个值,但期望输入一个数字时,通常需要进行显式转换。
如果该字符串不是一个有效的数字,转换的结果会是 NaN
。例如:
1 | let age = Number("an arbitrary string instead of a number"); |
number 类型转换规则:
值 | 变成…… |
---|---|
undefined |
NaN |
null |
0 |
true 和 false |
1 and 0 |
string |
去掉首尾空白字符(空格、换行符 \n 、制表符 \t 等)后的纯数字字符串中含有的数字。如果剩余字符串为空,则转换结果为 0 。否则,将会从剩余字符串中“读取”数字。当类型转换出现 error 时返回 NaN 。 |
例子:
1 | console.log( Number(" 123 ") ); // 123 |
请注意 null
和 undefined
在这有点不同:null
变成数字 0
,undefined
变成 NaN
。
大多数数学运算符都执行这种转换,我们将在下一节中进行介绍。
转换成布尔(boolean)类型
它发生在逻辑运算中(稍后我们将进行条件判断和其他类似的东西),但是也可以通过调用 Boolean(value) 显式地进行转换。
转换规则如下:
- 直观上为“空”的值(如
0
、空字符串、null
、undefined
和NaN
)将变为false
。 - 其他值变成
true
。
比如:
1 | console.log( Boolean(1) ); // true |
请注意:包含 0 的字符串 "0"
是 true
一些编程语言(比如 PHP)视 "0"
为 false
。但在 JavaScript 中,非空的字符串总是 true
。
1 | console.log( Boolean("0") ); // true |
总结
有三种常用的类型转换:转换为 string 类型、转换为 number 类型和转换为 boolean 类型。
字符串转换 —— 转换发生在输出内容的时候,也可以通过 String(value)
进行显式转换。原始类型值的 string 类型转换通常是很明显的。
数字型转换 —— 转换发生在进行算术操作时,也可以通过 Number(value)
进行显式转换。
数字型转换遵循以下规则:
值 | 变成…… |
---|---|
undefined |
NaN |
null |
0 |
true / false |
1 / 0 |
string |
“按原样读取”字符串,两端的空白字符(空格、换行符 \n 、制表符 \t 等)会被忽略。空字符串变成 0 。转换出错则输出 NaN 。 |
布尔型转换 —— 转换发生在进行逻辑操作时,也可以通过 Boolean(value)
进行显式转换。
布尔型转换遵循以下规则:
值 | 变成…… |
---|---|
0 , null , undefined , NaN , "" |
false |
其他值 | true |
上述的大多数规则都容易理解和记忆。人们通常会犯错误的值得注意的例子有以下几个:
- 对
undefined
进行数字型转换时,输出结果为NaN
,而非0
。 - 对
"0"
和只有空格的字符串(比如:" "
)进行布尔型转换时,输出结果为true
。
https://zh.javascript.info/type-conversions
数学运算
术语:“一元运算符”,“二元运算符”,“运算元”
在正式开始前,我们先简单浏览一下常用术语。
运算元 —— 运算符应用的对象。比如说乘法运算
5 * 2
,有两个运算元:左运算元5
和右运算元2
。有时候人们也称其为“参数”而不是“运算元”。如果一个运算符对应的只有一个运算元,那么它是 一元运算符。比如说一元负号运算符
-
,它的作用是对数字进行正负转换:1
2
3
4let x = 1;
x = -x;
console.log( x ); // -1,一元负号运算符生效如果一个运算符拥有两个运算元,那么它是 二元运算符。
例如,减号还存在二元运算符形式:1
2let x = 1, y = 3;
console.log( y - x ); // 2,二元运算符减号做减运算严格地说,在上面的示例中,我们使用一个相同的符号表征了两个不同的运算符:①负号运算符,即反转符号的一元运算符;②减法运算符,是从另一个数减去一个数的二元运算符。
数学运算
支持以下数学运算:
- 加法
+
, - 减法
-
, - 乘法
*
, - 除法
/
, - 取余
%
, - 求幂
**
.
前四个都很简单,而 %
和 **
则需要说一说。
取余 %
取余运算符是 %
,尽管它看起来很像百分数,但实际并无关联。
a % b
的结果是 a
整除 b
的 余数。
例如:
1 | console.log( 13 % 2 ); // 输出 1, 13 除以 2 的余数 |
求幂 **
求幂运算 a ** b
,将 a
提升至 a
的 b
次幂。
在数学运算中我们将其表示为 a b 。
例如:
1 | console.log( 2 ** 2 ); // 2 ² = 4 |
就像在数学运算中一样,幂运算也适用于非整数。
例如,平方根是指数为 ½ 的幂运算:
1 | console.log( 4 ** (1/2) ); // 2 (1/2 次方与平方根相同) |
用二元运算符 + 连接字符串
通常,加号 +
用于求和。
但是如果加号 +
被应用于字符串,它将合并(连接)各个字符串:
1 | let s = "my" + "string"; |
注意:只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串。
举个例子:
1 | console.log( '1' + 2 ); // "12" |
你看,第一个运算元和第二个运算元,哪个是字符串并不重要,只要其中任意一个是字符串,那么由 +
连接的两个运算元都会默认转换成字符串拼接起来。
下面是一个更复杂的例子:
1 | console.log(2 + 2 + '1' ); // "41",不是 "221" |
在这里,运算符 +
是按顺序工作(加法默认是从左到右)。第一个 +
将两个数字相加,所以返回 4
,然后下一个 +
将字符串 1
加入其中,所以就是 4 + '1' = '41'
。
1 | console.log('1' + 2 + 2); // "122",不是 "14" |
这里,第一个操作数是一个字符串,所以编译器将其他两个操作数也视为了字符串。2
被与 '1'
连接到了一起,也就是像 '1' + 2 = "12"
然后 "12" + 2 = "122"
这样。
二元 +
是唯一一个以这种方式支持字符串的运算符。
其他算术运算符只对数字起作用,并且总是将其运算元转换为数字。
下面是减法和除法运算的示例:
1 | console.log( 6 - '2' ); // 4,将 '2' 转换为数字 |
数字转化,一元运算符 +
加号 +
有两种形式。一种是上面我们刚刚讨论的二元运算符,还有一种是一元运算符。
一元运算符加号,或者说,加号 +
应用于单个值,对数字没有任何作用。
但是如果运算元不是数字,加号 +
则会将其转化为数字。
例如:
1 | // 对数字无效(因为数学里面学过,在数字前面加正号没啥用) |
它的效果和 Number(...)
相同,但是更加简短。
我们经常会有将字符串转化为数字的需求。比如,如果我们正在从 HTML 表单中取值,通常得到的都是字符串。如果我们想对它们求和,该怎么办?
二元运算符加号会把它们合并成字符串:
1 | let apples = "2"; |
如果我们想把它们当做数字对待,我们需要转化它们,然后再求和:
1 | let apples = "2"; |
一元运算符加号首先起作用,它们将字符串转为数字,然后二元运算符加号对它们进行求和。
为什么一元运算符先于二元运算符作用于运算元?
这是由于它们拥有 更高的优先级。
运算符优先级
如果一个表达式拥有多个运算符,执行的顺序则由 优先级 决定。换句话说,所有的运算符中都隐含着优先级顺序。
我们知道在表达式 1 + 2 * 2
中,乘法先于加法计算。
这就是一个优先级问题。乘法比加法拥有 更高的优先级。
圆括号拥有 最高优先级 ,所以如果我们对现有的运算顺序不满意,我们可以使用圆括号来修改运算顺序,就像这样:(1 + 2) * 2
。
不要觉得加多了括号丑,对于新手,为了减轻学习压力以及避免优先级错误,就应该多用括号。
因为括号的优先级是最高的,你只要用了括号,下面这张表中的运算优先级你都可以不用去记。
在 JavaScript 中有众多运算符。每个运算符都有对应的优先级数字。数字越大,越先执行。如果优先级相同,则按照由左至右的顺序执行。
这是一个摘抄自 Mozilla 的 优先级表(你没有必要把这全记住,但要记住一元运算符优先级高于二元运算符):
优先级 | 名称 | 符号 |
---|---|---|
… | … | … |
15 | 一元加号 | + |
15 | 一元负号 | - |
14 | 求幂 | ** |
13 | 乘号 | * |
13 | 除号 | / |
12 | 加号 | + |
12 | 减号 | - |
… | … | … |
2 | 赋值符 | = |
… | … | … |
赋值运算符
我们知道赋值符号 =
也是一个运算符。从优先级表中可以看到它的优先级非常低,只有 2
。
当我们赋值时,比如 x = 2 * 2 + 1
,所有的计算先执行,然后 =
才执行,将计算结果存储到 x
。
1 | let x = 2 * 2 + 1; |
赋值 = 返回一个值
=
是一个运算符,而不是一个有着“魔法”作用的语言结构。
在 JavaScript 中,所有运算符都会返回一个值。这对于 +
和 -
来说是显而易见的,但对于 =
来说也是如此。
语句 x = value
将值 value
写入 x
。
下面是一个在复杂语句中使用赋值的例子:
1 | let a = 1; |
上面这个例子,(a = b + 1)
的结果是给 a
赋值为 3
。然后该值被用于进一步的运算。
不过,请不要写这样的代码。这样的技巧绝对不会使代码变得更清晰或可读。
链式赋值(Chaining assignments)
另一个有趣的特性是链式赋值:
1 | let a, b, c; |
链式赋值从右到左进行计算。
首先,对最右边的表达式 2 + 2
求值,
然后将其赋给左边的变量:c
、b
和 a
。
最后,所有的变量共享一个值。
同样,出于可读性,最好将这种代码分成几行:
1 | c = 2 + 2; |
这样可读性更强,尤其是在快速浏览代码的时候。
原地修改
我们经常需要对一个变量做运算,并将新的结果存储在同一个变量中。
例如:
1 | let n = 2; |
可以使用运算符 +=
和 *=
来缩写这种表示。
1 | let n = 2; |
所有算术和位运算符都有简短的“修改并赋值”运算符:/=
和 -=
等。
这类运算符的优先级与普通赋值运算符的优先级相同,所以它们在大多数其他运算之后执行:
1 | let n = 2; |
自增/自减
对一个数进行加一、减一是最常见的数学运算符之一。
所以,对此有一些专门的运算符:
自增
++
将变量与 1 相加:1
2
3let counter = 2;
counter++; // 和 counter = counter + 1 效果一样,但是更简洁
console.log( counter ); // 3自减
--
将变量与 1 相减:1
2
3let counter = 5;
counter--; // 和 counter = counter - 1 效果一样,但是更简洁
console.log( counter ); // 4
重要:
自增/自减只能应用于变量。试一下,将其应用于普通数字(比如 5++
)则会报错。
运算符 ++
和 --
可以置于变量前,也可以置于变量后。
- 当运算符置于变量后,被称为“后置形式”:
counter++
。 - 当运算符置于变量前,被称为“前置形式”:
++counter
。
两者都做同一件事:将变量 counter
与 1
相加。
那么它们有区别吗?有,但只有当我们使用 ++/--
的返回值时才能看到区别。
详细点说。我们知道,所有的运算符都有返回值。自增/自减也不例外。前置形式返回一个新的值,但后置返回原来的值(做加法/减法之前的值)。
为了直观看到区别,看下面的例子:
1 | let counter = 1; |
(*)
所在的行是前置形式 ++counter
,对 counter
做自增运算,返回的是新的值 2
。因此 console.log
显示的是 2
。
下面让我们看看后置形式:
1 | let counter = 1; |
(*)
所在的行是后置形式 counter++
,它同样对 counter
做加法,但是返回的是 旧值(做加法之前的值)。因此 console.log
显示的是 1
。
总结:
如果自增/自减的值不会被使用,那么两者形式没有区别:
1
2
3
4let counter = 0;
counter++;
++counter;
console.log( counter ); // 2,以上两行作用相同如果我们想要对变量进行自增操作,并且 需要立刻使用自增后的值,那么我们需要使用前置形式:
1
2let counter = 0;
console.log( ++counter ); // 1如果我们想要将一个数加一,但是我们想使用其自增之前的值,那么我们需要使用后置形式:
1
2let counter = 0;
console.log( counter++ ); // 0
自增/自减和其它运算符的对比
++/--
运算符同样可以在表达式内部使用。它们的优先级比绝大部分的算数运算符要高。
举个例子:
1 | let counter = 1; |
与下方例子对比:
1 | let counter = 1; |
尽管从技术层面上来说可行,但是这样的写法会降低代码的可阅读性。在一行上做多个操作 —— 这样并不好。
当阅读代码时,快速的视觉“纵向”扫描会很容易漏掉 counter++
,这样的自增操作并不明显。
我们建议用“一行一个行为”的模式:
1 | let counter = 1; |
位运算符
了解即可,前端开发过程中用的很少。
位运算符把运算元当做 32 位整数,并在它们的二进制表现形式上操作。
这些运算符不是 JavaScript 特有的。大部分的编程语言都支持这些运算符。
下面是位运算符:
- 按位与 (
&
) - 按位或 (
|
) - 按位异或 (
^
) - 按位非 (
~
) - 左移 (
<<
) - 右移 (
>>
) - 无符号右移 (
>>>
)
这些运算符很少被使用,一般是我们需要在最低级别(位)上操作数字时才使用。我们不会很快用到这些运算符,因为在 Web 开发中很少使用它们,但在某些特殊领域中,例如密码学,它们很有用。当你需要了解它们的时候,可以阅读 MDN 上的 位操作符 章节。
逗号运算符
逗号运算符 ,
是最少见最不常使用的运算符之一。有时候它会被用来写更简短的代码,因此为了能够理解代码,我们需要了解它。
逗号运算符能让我们处理多个表达式,使用 ,
将它们分开。每个表达式都运行了,但是只有最后一个的结果会被返回。
举个例子:
1 | let a = (1 + 2, 3 + 4); |
这里,第一个表达式 1 + 2
运行了,但是它的结果被丢弃了。随后计算 3 + 4
,并且该计算结果被返回。
逗号运算符的优先级非常低
请注意逗号运算符的优先级非常低,比 =
还要低,因此上面你的例子中圆括号非常重要。
如果没有圆括号:a = 1 + 2, 3 + 4
会先执行 +
,将数值相加得到 a = 3, 7
,然后赋值运算符 =
执行 a = 3
,然后逗号之后的数值 7
不会再执行,它被忽略掉了。相当于 (a = 1 + 2), 3 + 4
。
为什么我们需要这样一个运算符,它只返回最后一个值呢?
有时候,人们会使用它把几个行为放在一行上来进行复杂的运算。
举个例子:
1 | // 一行上有三个运算符 |
这样的技巧在许多 JavaScript 框架中都有使用,这也是为什么我们提到它。但是通常它并不能提升代码的可读性,使用它之前,我们要想清楚。
值的比较
我们知道,在数学中有很多用于比较大小的运算符。
在 JavaScript 中,它们的编写方式如下:
- 大于 / 小于:
a > b
,a < b
。 - 大于等于 / 小于等于:
a >= b
,a <= b
。 - 检查两个值的相等:
a == b
,请注意双等号==
表示相等性检查,而单等号a = b
表示赋值。 - 检查两个值不相等:不相等在数学中的符号是
≠
,但在 JavaScript 中写成a != b
。
比较的结果是 Boolean 类型
所有比较运算符均返回布尔值:
true
—— 表示“yes(是)”,“correct(正确)”或“the truth(真)”。false
—— 表示“no(否)”,“wrong(错误)”或“not the truth(非真)”。
示例:
1 | console.log( 2 > 1 ); // true(正确) |
和其他类型的值一样,比较的结果可以被赋值给任意变量:
1 | let result = 5 > 4; // 把比较的结果赋值给 result |
字符串比较
在比较字符串的大小时,JavaScript 会使用“字典(dictionary)”或“词典(lexicographical)”顺序进行判定。
换言之,字符串是按字符(母)逐个进行比较的。
例如:
1 | console.log( 'Z' > 'A' ); // true |
字符串的比较算法非常简单:
- 首先比较两个字符串的首位字符大小。
- 如果一方字符较大(或较小),则该字符串大于(或小于)另一个字符串。算法结束。
- 否则,如果两个字符串的首位字符相等,则继续取出两个字符串各自的后一位字符进行比较。
- 重复上述步骤进行比较,直到比较完成某字符串的所有字符为止。
- 如果两个字符串的字符同时用完,那么则判定它们相等,否则未结束(还有未比较的字符)的字符串更大。
在上面的第一个例子中,'Z' > 'A'
比较在算法的第 1 步就得到了结果。
在第二个例子中,字符串 Glow
与 Glee
的比较则需要更多步骤,因为需要逐个字符进行比较:
G
和G
相等。l
和l
相等。o
比e
大,算法停止,第一个字符串大于第二个。
非真正的字典顺序,而是 Unicode 编码顺序
在上面的算法中,比较大小的逻辑与字典或电话簿中的排序很像,但也不完全相同。
比如说,字符串比较对字母大小写是敏感的。大写的 "A"
并不等于小写的 "a"
。哪一个更大呢?实际上小写的 "a"
更大。这是因为在 JavaScript 使用的内部编码表中(Unicode),小写字母的字符索引值更大。我们会在 字符串 这章讨论更多关于字符串的细节。
不同类型间的比较
当对不同类型的值进行比较时,JavaScript 会首先将其转化为数字(number)再判定大小。
例如:
1 | console.log( '2' > 1 ); // true,字符串 '2' 会被转化为数字 2 |
‘2wwww’ 不是一个有效的数字格式,它会被转换成 NaN,在 JavaScript 中,NaN 与任何值(包括它自己)的比较都是不相等的。
对于布尔类型值,true
会被转化为 1
、false
转化为 0
。
例如:
1 | console.log( true == 1 ); // true |
一个有趣的现象
有时候,以下两种情况会同时发生:
- 若直接比较两个值,其结果是相等的。
- 若把两个值转为布尔值,它们可能得出完全相反的结果,即一个是
true
,一个是false
。
例如:
1 | let a = 0; |
对于 JavaScript 而言,这种现象其实挺正常的。因为 JavaScript 会把待比较的值转化为数字后再做比较(因此 "0"
变成了 0
)。若只是将一个变量转化为 Boolean
值,则会使用其他的类型转换规则。
严格相等
普通的相等性检查 ==
存在一个问题,它不能区分出 0
和 false
:
1 | console.log( 0 == false ); // true |
也同样无法区分空字符串和 false
:
1 | console.log( '' == false ); // true |
这是因为在比较不同类型的值时,处于相等判断符号 ==
两侧的值会先被转化为数字。空字符串和 false
也是如此,转化后它们都为数字 0。
如果我们需要区分 0
和 false
,该怎么办?
严格相等运算符 ===
在进行比较时不会做任何的类型转换。
换句话说,如果 a
和 b
属于不同的数据类型,那么 a === b
不会做任何的类型转换而立刻返回 false
。
让我们试试:
1 | console.log( 0 === false ); // false,因为被比较值的数据类型不同 |
同样的,与“不相等”符号 !=
类似,“严格不相等”表示为 !==
。
严格相等的运算符虽然写起来稍微长一些,但是它能够很清楚地显示代码意图,降低你犯错的可能性。
对 null 和 undefined 进行比较
当使用 null
或 undefined
与其他值进行比较时,其返回结果常常出乎你的意料。
当使用严格相等
===
比较二者时它们不相等,因为它们属于不同的类型。
console.log( null === undefined ); // false
当使用非严格相等
==
比较二者时
JavaScript 存在一个特殊的规则,会判定它们相等。它们俩就像“一对恋人”,仅仅等于对方而不等于其他任何的值(只在非严格相等下成立)。console.log( null == undefined ); // true
当使用数学式或其他比较方法
< > <= >=
时:null/undefined
会被转化为数字:null
被转化为0
,undefined
被转化为NaN
。
下面让我们看看,这些规则会带来什么有趣的现象。同时更重要的是,我们需要从中学会如何远离这些特性带来的“陷阱”。
奇怪的结果:null vs 0
通过比较 null
和 0 可得:
1 | console.log( null > 0 ); // (1)false |
为什么会出现这种反常结果,这是因为相等性检查 ==
和普通比较符 > < >= <=
的代码逻辑是相互独立的。
进行值的比较时,null
会被转化为数字,因此它被转化为了 0
。这就是为什么(3)中 null >= 0
返回值是 true,(1)中 null > 0
返回值是 false。
另一方面,undefined
和 null
在相等性检查 ==
中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(2)中 null == 0
会返回 false。
特立独行的 undefined
undefined
不应该被与其他值进行比较:
1 | console.log( undefined > 0 ); // false (1) |
为何它看起来如此厌恶 0?返回值都是 false!
原因如下:
(1)
和(2)
都返回false
是因为undefined
在比较中被转换为了NaN
,而NaN
是一个特殊的数值型值,它与任何值进行比较都会返回false
。(3)
返回false
是因为这是一个相等性检查,而undefined
只与null
相等,不会与其他值相等。
避免问题
我们为何要研究上述示例?我们需要时刻记得这些古怪的规则吗?不,其实不需要。虽然随着代码写得越来越多,我们对这些规则也都会烂熟于胸,但是我们需要更为可靠的方法来避免潜在的问题:
- 除了严格相等
===
外,其他但凡是有undefined/null
参与的比较,我们都需要格外小心。 - 除非你非常清楚自己在做什么,否则永远不要使用
>= > < <=
去比较一个可能为null/undefined
的变量。对于取值可能是null/undefined
的变量,请按需要分别检查它的取值情况。
总结
- 比较运算符始终返回布尔值。
- 字符串的比较,会按照“词典”顺序逐字符地比较大小。
- 当对不同类型的值进行比较时,它们会先被转化为数字(不包括严格相等检查)再进行比较。
- 在非严格相等
==
下,null
和undefined
相等且各自不等于任何其他的值。 - 在使用
>
或<
进行比较时,需要注意变量可能为null/undefined
的情况。比较好的方法是单独检查变量是否等于null/undefined
。
条件分支:if 和 ‘?’
有时我们需要根据不同条件执行不同的操作。
我们可以使用 if
语句和条件运算符 ?
(也称为“问号”运算符)来实现。
“if” 语句
if(...)
语句计算括号里的条件表达式,如果计算结果是 true
,就会执行对应的代码块。
例如:
1 | let year = 2024; |
在上面这个例子中,条件是一个简单的相等性检查(year == 2024
),但它还可以更复杂。
如果有多个语句要执行,我们必须将要执行的代码块封装在大括号内:
1 | let year = 2024; |
建议每次使用 if 语句都用大括号 {}
来包装代码块,即使只有一条语句。这样可以提高代码可读性。
布尔转换
if (…)
语句会计算圆括号内的表达式,并将计算结果转换为布尔型。
让我们回顾一下 类型转换 一章中的转换规则:
- 数字
0
、空字符串""
、null
、undefined
和NaN
都会被转换成false
。因为它们被称为“假值(falsy)”。 - 其他值被转换为
true
,所以它们被称为“真值(truthy)”。
所以,下面这个条件下的代码永远不会执行:
1 | if (0) { // 0 是假值(falsy) |
……但下面的条件 —— 始终有效:
1 | if (1) { // 1 是真值(truthy) |
我们也可以将预先计算的布尔值传入 if
语句,像这样:
1 | let year = 2024; |
“else” 语句
if
语句有时会包含一个可选的 “else” 块。如果判断条件不成立,就会执行它内部的代码。
例如:
1 | let year = 2025; |
多个条件:“else if”
有时我们需要测试一个条件的几个变体。我们可以通过使用 else if
子句实现。
例如:
1 | let year = 2022); |
在上面的代码中,JavaScript 先检查 year < 2024
。如果条件不符合,就会转到下一个条件 year > 2024
。如果这个条件也不符合,则会显示最后一个 console.log
。
可以有更多的 else if
块。结尾的 else
是可选的。
条件运算符 ‘?’
有时我们需要根据一个条件去赋值一个变量。
如下所示:
1 | let accessAllowed; |
所谓的“条件”或“问号”运算符让我们可以更简短地达到目的。
这个运算符通过问号 ?
表示。有时它被称为三元运算符,被称为“三元”是因为该运算符中有三个操作数。实际上它是 JavaScript 中唯一一个有这么多操作数的运算符。
语法:
1 | let result = condition ? value1 : value2; |
计算(condition)结果,如果结果为真,则返回 value1
,否则返回 value2
。
例如:
1 | let age =24; |
技术上讲,我们可以省略 age > 18
外面的括号。问号运算符的优先级较低,所以它会在比较运算符 >
后执行。
下面这个示例会执行和前面那个示例相同的操作:
1 | // 比较运算符 "age > 18" 首先执行 |
但括号可以使代码可读性更强,所以我们建议使用它们。
请注意:
在上面的例子中,你可以不使用问号运算符,因为比较本身就返回 true/false
:
1 | // 下面代码同样可以实现 |
多个 ‘?’
使用一系列问号 ?
运算符可以返回一个取决于多个条件的值。
例如:
1 | let age = 18; |
- 第一个问号检查
age < 3
。 - 如果为真 — 返回
'Hi, baby!'
。否则,会继续执行冒号":"
后的表达式,检查age < 18
。 - 如果为真 — 返回
'Hello!'
。否则,会继续执行下一个冒号":"
后的表达式,检查age < 100
。 - 如果为真 — 返回
'Greetings!'
。否则,会继续执行最后一个冒号":"
后面的表达式,返回'What an unusual age!'
。
这是使用 if..else
实现上面的逻辑的写法:
1 | if (age < 3) { |
‘?’ 的非常规使用
有时可以使用问号 ?
来代替 if
语句:
1 | console.log('今年是哪一年?'); |
根据条件 year =='2024'
,要么执行 ?
后面的第一个表达式并显示对应内容,要么执行第二个表达式并显示对应内容。
在这里我们不是把结果赋值给变量。而是根据条件执行不同的代码。
不建议这样使用问号运算符。
这种写法比 if
语句更短,对一些程序员很有吸引力。但它的可读性差。
下面是使用 if
语句实现相同功能的代码,进行下比较:
1 | console.log('今年是哪一年?'); |
因为我们的眼睛垂直扫描代码。所以,跨越几行的代码块比长而水平的代码更易于理解。
问号 ?
的作用是根据条件返回其中一个值。请正确使用它。当需要执行不同的代码分支时,请使用 if
。
逻辑运算符
JavaScript 中有四个逻辑运算符:||
(或),&&
(与),!
(非),??
(空值合并运算符)。本文我们先介绍前三个,在下一篇文章中再详细介绍 ??
运算符。
虽然它们被称为“逻辑”运算符,但这些运算符却可以被应用于任意类型的值,而不仅仅是布尔值。它们的结果也同样可以是任意类型。
||(或)
两个竖线符号表示“或”运算符:
1 | result = a || b; |
在传统的编程中,逻辑 或
仅能够操作布尔值。如果参与运算的任意一个参数为 true
,返回的结果就为 true
,否则返回 false
。
在 JavaScript 中,逻辑运算符更加灵活强大。但是,首先让我们看一下操作数是布尔值的时候发生了什么。
下面是四种可能的逻辑组合:
1 | console.log( true || true ); // true |
正如我们所见,除了两个操作数都是 false
的情况,结果都是 true
。
如果操作数不是布尔值,那么它将会被转化为布尔值来参与运算。
例如,数字 1
被作为 true
处理,数字 0
则被作为 false
:
1 | if (1 || 0) { // 工作原理相当于 if( true || false ) |
大多数情况下,逻辑或 ||
会被用在 if
语句中,用来测试是否有 任何 给定的条件为 true
。
例如:
1 | let hour = 9; |
我们可以传入更多的条件:
1 | let hour = 12; |
或运算寻找第一个真值
上文提到的逻辑处理多少有些传统了。下面让我们看看 JavaScript 的“附加”特性。
拓展的算法如下所示。
给定多个参与或运算的值:
1 | result = value1 || value2 || value3; |
或运算符 ||
做了如下的事情:
- 从左到右依次计算操作数。
- 处理每一个操作数时,都将其转化为布尔值。如果结果是
true
,就停止计算,返回这个操作数的初始值。 - 如果所有的操作数都被计算过(也就是,转换结果都是
false
),则返回最后一个操作数。
返回的值是操作数的初始形式,不会做布尔转换。
换句话说,一个或运算 ||
的链,将返回第一个真值,如果不存在真值,就返回该链的最后一个值。
例如:
1 | console.log( 1 || 0 ); // 1(1 是真值) |
与“纯粹的、传统的、仅仅处理布尔值的或运算”相比,这个规则就引起了一些很有趣的用法。
获取变量列表或者表达式中的第一个真值。
例如,我们有变量
firstName
、lastName
和nickName
,都是可选的(即可以是 undefined,也可以是假值)。我们用或运算
||
来选择有数据的那一个,并显示出来(如果没有设置,则用"Anonymous"
):1
2
3
4
5let firstName = "";
let lastName = "";
let nickName = "超级虎哥";
console.log( firstName || lastName || nickName || "孙悟空"); // 超级虎哥如果所有变量的值都为假,结果就是
"孙悟空"
。短路求值(Short-circuit evaluation)。
或运算符
||
的另一个用途是所谓的“短路求值”。这指的是,
||
对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,那么这一特性的重要性就变得显而易见了。
在下面这个例子中,只会打印第二条信息:
1
2true || console.log("这条信息不会打印");
false || console.log("打印出来了");在第一行中,或运算符
||
在遇到true
时立即停止运算,所以console.log
没有运行。有时,人们利用这个特性,只在左侧的条件为假时才执行命令。
&&(与)
两个 & 符号表示 &&
与运算符:
1 | result = a && b; |
在传统的编程中,当两个操作数都是真值时,与运算返回 true
,否则返回 false
:
1 | console.log( true && true ); // true |
带有 if
语句的示例:
1 | let hour = 12; |
就像或运算一样,与运算的操作数可以是任意类型的值:
1 | if (1 && 0) { // 作为 true && false 来执行 |
与运算寻找第一个假值
给出多个参加与运算的值:
1 | result = value1 && value2 && value3; |
与运算 &&
做了如下的事:
- 从左到右依次计算操作数。
- 在处理每一个操作数时,都将其转化为布尔值。如果结果是
false
,就停止计算,并返回这个操作数的初始值。 - 如果所有的操作数都被计算过(例如都是真值),则返回最后一个操作数。
换句话说,与运算返回第一个假值,如果没有假值就返回最后一个值。
上面的规则和或运算很像。区别就是与运算返回第一个假值,而或运算返回第一个真值。
例如:
1 | // 如果第一个操作数是真值, |
我们也可以在一行代码上串联多个值。查看第一个假值是如何被返回的:
1 | console.log( 1 && 2 && null && 3 ); // null |
如果所有的值都是真值,最后一个值将会被返回:
1 | console.log( 1 && 2 && 3 ); // 3,最后一个值 |
与运算 &&
在 或运算 ||
之前进行
与运算 &&
的优先级 比 或运算 ||
要高。
所以代码 a && b || c && d
跟 &&
表达式加了括号完全一样:(a && b) || (c && d)
。
不要用 ||
或 &&
来取代 if
有时候,有人会将与运算符 &&
作为“简化 if
”的一种方式。
例如:
1 | let x = 1; |
&&
右边的代码只有运算抵达到那里才能被执行。也就是,当且仅当 (x > 0)
为真。
所以我们基本可以类似地得到:
1 | let x = 1; |
虽然使用 &&
写出的变体看起来更短,但 if
更明显,并且往往更具可读性。因此,我们建议根据每个语法结构的用途来使用:如果我们想要 if
,就使用 if
;如果我们想要逻辑与,就使用 &&
。
!(非)
感叹符号 !
表示布尔非运算符。
语法相当简单:
1 | result = !value; |
逻辑非运算符接受一个参数,并按如下运作:
- 将操作数转化为布尔类型:
true/false
。 - 返回相反的值。
例如:
1 | console.log( !true ); // false |
两个非运算 !!
有时候用来将某个值转化为布尔类型:
1 | console.log( !!"non-empty string" ); // true |
也就是,第一个非运算将该值转化为布尔类型并取反,第二个非运算再次取反。最后我们就得到了一个任意值到布尔值的转化。
有一个略显冗长的方式也可以实现同样的效果 —— 一个内建的 Boolean
函数:
1 | console.log( Boolean("non-empty string") ); // true |
非运算符 !
的优先级在所有逻辑运算符里面最高,所以它总是在 &&
和 ||
之前执行。
https://zh.javascript.info/logical-operators
空值合并运算符 ‘ ?? ‘
这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills.
空值合并运算符(nullish coalescing operator)的写法为两个问号 ??
。
由于它对待 null
和 undefined
的方式类似,所以在本文中我们将使用一个特殊的术语对其进行表示。为简洁起见,当一个值既不是 null
也不是 undefined
时,我们将其称为“已定义的(defined)”。
a ?? b
的结果是:
- 如果
a
是已定义的,则结果为a
, - 如果
a
不是已定义的,则结果为b
。
换句话说,如果第一个参数不是 null/undefined
,则 ??
返回第一个参数。否则,返回第二个参数。
空值合并运算符并不是什么全新的东西。它只是一种获得两者中的第一个“已定义的”值的不错的语法。
我们可以使用我们已知的运算符重写 result = a ?? b
,像这样:
1 | result = (a !== null && a !== undefined) ? a : b; |
现在你应该清楚了 ??
的作用。让我们来看看它的使用场景吧。
??
的常见使用场景是提供默认值。
例如,在这里,如果 user
的值不为 null/undefined
则显示 user
,否则显示 匿名
:
1 | let user; |
在下面这个例子中,我们将一个名字赋值给了 user
:
1 | let user = "John"; |
我们还可以使用 ??
序列从一系列的值中选择出第一个非 null/undefined
的值。
假设我们在变量 firstName
、lastName
或 nickName
中存储着一个用户的数据。如果用户决定不填写相应的值,则所有这些变量的值都可能是未定义的。
我们想使用这些变量之一显示用户名,如果这些变量的值都是 null/undefined
,则显示 “匿名”。
让我们使用 ??
运算符来实现这一需求:
1 | let firstName = null; |
与 || 比较
或运算符 ||
可以以与 ??
运算符相同的方式使用。像我们在 上一章 所讲的那样。
例如,在上面的代码中,我们可以用 ||
替换掉 ??
,也可以获得相同的结果:
1 | let firstName = null; |
纵观 JavaScript 发展史,或 ||
运算符先于 ??
出现。它自 JavaScript 诞生就存在了,因此开发者长期将其用于这种目的。
另一方面,空值合并运算符 ??
是最近才被添加到 JavaScript 中的,它的出现是因为人们对 ||
不太满意。
它们之间重要的区别是:
||
返回第一个 真 值。??
返回第一个 已定义的 值。
换句话说,||
无法区分 false
、0
、空字符串 ""
和 null/undefined
。它们都一样 —— 假值(falsy values)。如果其中任何一个是 ||
的第一个参数,那么我们将得到第二个参数作为结果。
不过在实际中,我们可能只想在变量的值为 null/undefined
时使用默认值。也就是说,当该值确实未知或未被设置时。
例如,考虑下面这种情况:
1 | let height = 0; |
height || 100
首先会检查height
是否为一个假值,它是0
,确实是假值。- 所以,
||
运算的结果为第二个参数,100
。
- 所以,
height ?? 100
首先会检查height
是否为null/undefined
,发现它不是。- 所以,结果为
height
的原始值,0
。
- 所以,结果为
实际上,高度 0
通常是一个有效值,它不应该被替换为默认值。所以 ??
运算得到的是正确的结果。
优先级
??
运算符的优先级与 ||
相同,它们的优先级都为 3
,详见:MDN。
这意味着,就像 ||
一样,空值合并运算符在 =
和 ?
运算前计算,但在大多数其他运算(例如 +
和 *
)之后计算。
所以我们可能需要在这样的表达式中添加括号:
1 | let height = null; |
否则,如果我们省略了括号,则由于 *
的优先级比 ??
高,它会先执行,进而导致错误的结果。
1 | // 没有括号 |
?? 与 && 或 || 一起使用
出于安全原因,JavaScript 禁止将 ??
运算符与 &&
和 ||
运算符一起使用,除非使用括号明确指定了优先级。
下面的代码会触发一个语法错误:
1 | let x = 1 && 2 ?? 3; // Syntax error |
这个限制无疑是值得商榷的,它被添加到语言规范中是为了避免人们从 ||
切换到 ??
时的编程错误。
可以明确地使用括号来解决这个问题:
1 | let x = (1 && 2) ?? 3; // 正常工作了 |
总结
空值合并运算符
??
提供了一种从列表中选择第一个“已定义的”值的简便方式。它被用于为变量分配默认值:
1
2// 当 height 的值为 null 或 undefined 时,将 height 的值设置为 100
height = height ?? 100;??
运算符的优先级非常低,仅略高于?
和=
,因此在表达式中使用它时请考虑添加括号。如果没有明确添加括号,不能将其与
||
或&&
一起使用。
循环:while 和 for
我们经常需要重复执行一些操作。循环 是一种重复运行同一代码的方法。
for…of 和 for…in 循环
本文仅涵盖了基础的循环:while
,do..while
和 for(..; ..; ..)
。
如果你阅读本文是为了寻找其他类型的循环,那么:
“while” 循环
while
循环的语法如下:
1 | while (condition) { |
当 condition
为真时,执行循环体的 code
。
例如,以下将循环输出当 i < 3
时的 i
值:
1 | let i = 0; |
循环体的单次执行(一次循环)叫作 一次迭代。上面示例中的循环进行了三次迭代。
如果上述示例中没有 i++
,那么循环(理论上)会永远重复执行下去。实际上,浏览器提供了阻止这种循环的方法,我们可以通过终止进程,来停掉服务器端的 JavaScript。
任何表达式或变量都可以是循环条件,而不仅仅是比较。在 while
中的循环条件会被计算,计算结果会被转化为布尔值。
例如,while (i != 0)
可简写为 while (i)
:
1 | let i = 3; |
单行循环体不需要大括号
如果循环体只有一条语句,则可以省略大括号 {…}
:
1 | let i = 3; |
“do…while” 循环
使用 do..while
语法可以将条件检查移至循环体 下面:
1 | do { |
循环首先执行一次循环体,然后再检查条件,当条件为真时,重复执行循环体。
例如:
1 | let i = 0; |
这种形式的语法很少使用,除非你希望不管条件是否为真,循环体 至少执行一次。通常我们更倾向于使用另一个形式:while(…) {…}
。
“for” 循环
for
循环更加复杂,但它是最常使用的循环形式。
for
循环看起来就像这样:
1 | for (begin; condition; step) { |
我们通过示例来了解一下这三个部分的含义。
下述循环从 i
等于 0
到 3
(但不包括 3
)运行 console.log(i)
:
1 | for (let i = 0; i < 3; i++) { // 结果为 0、1、2 |
我们逐个部分分析 for
循环:
语句段 | ||
---|---|---|
begin | let i = 0 |
进入循环时执行一次。 |
condition | i < 3 |
在每次循环迭代之前检查,如果为 false,停止循环。 |
body(循环体) | console.log(i) |
条件为真时,重复运行。 |
step | i++ |
在每次循环体迭代后执行。 |
一般循环算法的工作原理如下:
1 | 开始运行 |
所以,begin
执行一次,然后进行迭代:每次检查 condition
后,执行 body
和 step
。
如果你这是第一次接触循环,那么回到这个例子,在一张纸上重现它逐步运行的过程,可能会对你有所帮助。
以下是在这个示例中发生的事:
1 | // for (let i = 0; i < 3; i++) console.log(i) |
内联变量声明
这里“计数”变量 i
是在循环中声明的。这叫做“内联”变量声明。这样的变量只在循环中可见。
1 | for (let i = 0; i < 3; i++) { |
除了定义一个变量,我们也可以使用现有的变量:
1 | let i = 0; |
省略语句段
for
循环的任何语句段都可以被省略。
例如,如果我们在循环开始时不需要做任何事,我们就可以省略 begin
语句段。
就像这样:
1 | let i = 0; // 我们已经声明了 i 并对它进行了赋值 |
我们也可以移除 step
语句段:
1 | let i = 0; |
该循环与 while (i < 3)
等价。
实际上我们可以删除所有内容,从而创建一个无限循环:
1 | for (;;) { |
请注意 for
的两个 ;
必须存在,否则会出现语法错误。
可以结合无限循环来等待 setTimeout 的异步执行,等到异步执行完成后,返回一个条件,让无限循环结束(即跳出循环)。
跳出循环
通常条件为假时,循环会终止。
但我们随时都可以使用 break
指令强制退出。
例如,下面这个循环要求用户输入一系列数字,在输入的内容不是数字时“终止”循环。
1 | let sum = 0; |
如果用户输入空行或取消输入,在 (*)
行的 break
指令会被激活。它立刻终止循环,将控制权传递给循环后的第一行,即,console.log
。
根据需要,”无限循环 + break
“ 的组合非常适用于不必在循环开始/结束时检查条件,但需要在中间甚至是主体的多个位置进行条件检查的情况。
继续下一次迭代
continue
指令是 break
的“轻量版”。它不会停掉整个循环。而是停止当前这一次迭代,并强制启动新一轮循环(如果条件允许的话)。
如果我们完成了当前的迭代,并且希望继续执行下一次迭代,我们就可以使用它。
下面这个循环使用 continue
来只输出奇数:
1 | for (let i = 0; i < 10; i++) { |
对于偶数的 i
值,continue
指令会停止本次循环的继续执行,将控制权传递给下一次 for
循环的迭代(使用下一个数字)。因此 console.log
仅被奇数值调用。
continue
指令利于减少嵌套
显示奇数的循环可以像下面这样:
1 | for (let i = 0; i < 10; i++) { |
从技术角度看,它与上一个示例完全相同。当然,我们可以将代码包装在 if
块而不使用 continue
。
但在副作用方面,它多创建了一层嵌套(大括号内的 console.log
调用)。如果 if
中代码有多行,则可能会降低代码整体的可读性。
禁止 break/continue
在 ‘?’ 的右边
请注意非表达式的语法结构不能与三元运算符 ?
一起使用。特别是 break/continue
这样的指令是不允许这样使用的。
例如,我们使用如下代码:
1 | if (i > 5) { |
……用问号重写:
1 | (i > 5) ? console.log(i) : continue; // continue 不允许在这个位置 |
……代码会停止运行,并显示有语法错误。
这是不(建议)使用问号 ?
运算符替代 if
语句的另一个原因。
break/continue 标签
有时候我们需要一次从多层嵌套的循环中跳出来。
例如,下述代码中我们的循环使用了 i
和 j
,从 (0,0)
到 (3,3)
提示坐标 (i, j)
:
1 | for (let i = 0; i < 3; i++) { |
我们需要提供一种方法,以在用户取消输入时来停止这个过程。
在 input
之后的普通 break
只会打破内部循环。这还不够 —— 标签可以实现这一功能!
标签 是在循环之前带有冒号的标识符:
1 | labelName: for (...) { |
break <labelName>
语句跳出循环至标签处:
1 | outer: for (let i = 0; i < 3; i++) { |
上述代码中,break outer
向上寻找名为 outer
的标签并跳出当前循环。
因此,控制权直接从 (*)
转至 console.log('Done!')
。
我们还可以将标签移至单独一行:
1 | outer: |
continue
指令也可以与标签一起使用。在这种情况下,执行跳转到标记循环的下一次迭代。
标签并不允许“跳到”所有位置
标签不允许我们跳到代码的任意位置。
例如,这样做是不可能的:
1 | break label; // 跳转至下面的 label 处(无效) |
break
指令必须在代码块内。从技术上讲,任何被标记的代码块都有效,例如:
1 | label: { |
……尽管 99.9% 的情况下 break
都被用在循环内,就像在上面那些例子中我们看到的那样。
continue
只有在循环内部才可行。
总结
我们学习了三种循环:
while
—— 每次迭代之前都要检查条件。do..while
—— 每次迭代后都要检查条件。for (;;)
—— 每次迭代之前都要检查条件,可以使用其他设置。
通常使用 while(true)
来构造“无限”循环。这样的循环和其他循环一样,都可以通过 break
指令来终止。
如果我们不想在当前迭代中做任何事,并且想要转移至下一次迭代,那么可以使用 continue
指令。
break/continue
支持循环前的标签。
标签是 break/continue
跳出嵌套循环以转到外部的唯一方法。
https://zh.javascript.info/while-for
“switch” 语句
switch
语句可以替代多个 if
判断。
switch
语句为多分支选择的情况提供了一个更具描述性的方式。
语法
switch
语句有至少一个 case
代码块和一个可选的 default
代码块。
就像这样:
1 | switch(x) { |
- 比较
x
值与第一个case
(也就是value1
)是否严格相等,然后比较第二个case
(value2
)以此类推。 - 如果相等,
switch
语句就执行相应case
下的代码块,直到遇到最靠近的break
语句(或者直到switch
语句末尾)。 - 如果没有符合的 case,则执行
default
代码块(如果default
存在)。
举个例子
switch
的例子(高亮的部分是执行的 case
部分):
1 | let a = 2 + 2; |
这里的 switch
从第一个 case
分支开始将 a
的值与 case
后的值进行比较,第一个 case
后的值为 3
匹配失败。
然后比较 4
。匹配,所以从 case 4
开始执行直到遇到最近的 break
。
如果没有 break
,程序将不经过任何检查就会继续执行下一个 case
。
无 break
的例子:
1 | let a = 2 + 2; |
在上面的例子中我们会看到连续执行的三个 console.log
:
1 | console.log( '太小啦' ); |
任何表达式都可以成为 switch/case
的参数
switch
和 case
都允许任意表达式。
比如:
1 | let a = "1"; |
这里 +a
返回 1
,这个值跟 case
中 b + 1
相比较,然后执行对应的代码。
“case” 分组
共享同一段代码的几个 case
分支可以被分为一组:
比如,如果我们想让 case 3
和 case 5
执行同样的代码:
1 | let a = 3; |
现在 3
和 5
都显示相同的信息。
switch/case
有通过 case 进行“分组”的能力,其实是 switch 语句没有 break
时的副作用。因为没有 break
,case 3
会从 (*)
行执行到 case 5
。
类型很关键
强调一下,这里的相等是严格相等。被比较的值必须是相同的类型才能进行匹配。
比如,我们来看下面的代码:
1 | let arg = prompt("Enter a value?") |
- 在
prompt
对话框输入0
、1
,第一个console.log
弹出。 - 输入
2
,第二个console.log
弹出。 - 但是输入
3
,因为prompt
的结果是字符串类型的"3"
,不严格相等===
于数字类型的3
,所以case 3
不会执行!因此case 3
部分是一段无效代码。所以会执行default
分支。
函数
我们经常需要在脚本的许多地方执行很相似的操作。
例如,当访问者登录、注销或者在其他地方时,我们需要显示一条好看的信息。
函数是程序的主要“构建模块”。函数使该段代码可以被调用很多次,而不需要写重复的代码。
我们已经看到了内建函数的示例,如 alert(message)
、prompt(message, default)
和 confirm(question)
。但我们也可以创建自己的函数。
函数声明
使用 函数声明 创建函数。
看起来就像这样:
1 | function showMessage() { |
function
关键字首先出现,然后是 函数名,然后是括号之间的 参数 列表(用逗号分隔,在上述示例中为空,我们将在接下来的示例中看到),最后是花括号之间的代码(即“函数体”)。
1 | function name(parameter1, parameter2, ... parameterN) { |
我们的新函数可以通过名称调用:showMessage()
。
例如:
1 | function showMessage() { |
调用 showMessage()
执行函数的代码。这里我们会看到显示两次消息。
这个例子清楚地演示了函数的主要目的之一:避免代码重复。
如果我们需要更改消息或其显示方式,只需在一个地方修改代码:输出它的函数。
局部变量
在函数中声明的变量只在该函数内部可见。
例如:
1 | function showMessage() { |
外部变量
函数也可以访问外部变量,例如:
1 | let userName = 'John'; |
函数对外部变量拥有全部的访问权限。函数也可以修改外部变量。
例如:
1 | let userName = 'John'; |
只有在没有局部变量的情况下才会使用外部变量。
如果在函数内部声明了同名变量,那么函数会 遮蔽 外部变量。例如,在下面的代码中,函数使用局部的 userName,而外部变量被忽略:
1 | let userName = 'John'; |
全局变量
任何函数之外声明的变量,例如上述代码中的外部变量 userName
,都被称为 全局 变量。
全局变量在任意函数中都是可见的(除非被局部变量遮蔽)。
减少全局变量 的使用是一种很好的做法。现代的代码有很少甚至没有全局变量。大多数变量存在于它们的函数中。但是有时候,全局变量能够用于存储项目级别的数据。
参数
我们可以通过参数将任意数据传递给函数。
在如下示例中,函数有两个参数:from 和 text。
1 | function showMessage(from, text) { // 参数:from 和 text |
当函数在 (*) 和 (**) 行中被调用时,给定值被复制到了局部变量 from
和 text
。然后函数使用它们进行计算。
这里还有一个例子:我们有一个变量 from
,并将它传递给函数。请注意:函数会修改 from
,但在函数外部看不到更改,因为函数修改的是复制的变量值副本:
1 | function showMessage(from, text) { |
当一个值被作为函数参数 (parameter)
传递时,它也被称为 参数 (argument)
。
换一种方式,我们把这些术语搞清楚:
函数参数(parameter)是函数声明中括号内列出的变量(它是函数声明时的术语)。
参数(argument)是调用函数时传递给函数的值(它是函数调用时的术语)。
我们声明函数时列出它们的函数参数 (parameters)
,然后调用它们传递参数 (arguments)
。
在上面的例子中,我们可以说:“函数 showMessage
被声明,并且带有两个参数 (parameters)
,随后它被调用,两个参数 (arguments)
分别为 from
和 “Hello””。
默认值
如果一个函数被调用,但有参数 (argument)
未被提供,那么相应的值就会变成 undefined
。
例如,之前提到的函数 showMessage(from, text)
可以只使用一个参数 (argument)
调用:
showMessage("Ann");
那不是错误,这样调用将输出 “Ann: undefined”。因为参数 text
的值未被传递,所以变成了 undefined
。
我们可以使用 =
为函数声明中的参数指定所谓的“默认”(如果对应参数的值未被传递则使用)值:
1 | function showMessage(from, text = "这里是默认的第二项值") { |
现在因为 text
参数未被传递,它将会得到默认值 “这里是默认的第二项值”。
这里 “这里是默认的第二项值” 是一个字符串,但它可以是更复杂的表达式,并且只会在缺少参数时才会被计算和分配。所以,这也是可能的:
1 | function showMessage(from, text = anotherFunction()) { |
默认参数的计算
在 JavaScript
中,每次函数在没带个别参数的情况下被调用,默认参数会被计算出来。
在上面的例子中,如果传递了参数 text
,那么 anotherFunction()
就不会被调用。
如果没传递参数 text
,那么 anotherFunction()
就会被调用。
在 JavaScript
老代码中的默认参数
几年前,JavaScript
不支持默认参数的语法。所以人们使用其他方式来设置默认参数。
如今,我们会在旧代码中看到它们。
例如,显式地检查 undefined
:
1 | function showMessage(from, text) { |
……或者使用 || 运算符:
1 | function showMessage(from, text) { |
后备的默认参数
有些时候,将参数默认值的设置放在函数执行(相较更后期)而不是函数声明时,也行得通。
我们可以通过将参数与 undefined
进行比较,来检查该参数是否在函数执行期间被传递进来:
1 | function showMessage(text) { |
……或者我们可以使用 || 运算符:
1 | function showMessage(text) { |
现代 JavaScript
引擎支持 空值合并运算符 ??
,它在大多数假值(例如 0)应该被视为“正常值”时更具优势:
1 | function showCount(count) { |
返回值
函数可以将一个值返回到调用代码中作为结果。
最简单的例子是将两个值相加的函数:
1 | function sum(a, b) { |
指令 return
可以在函数的任意位置。当执行到达时,函数停止,并将值返回给调用代码(分配给上述代码中的 result
)。
在一个函数中可能会出现很多次 return
。例如:
1 | function checkAge(age) { |
只使用 return
但没有返回值也是可行的。但这会导致函数立即退出。
例如:
1 | function showMovie(age) { |
在上述代码中,如果 checkAge(age)
返回 false
,那么 showMovie
将不会运行到 console.log
。
空值的 return
或没有 return
的函数返回值为 undefined
如果函数无返回值,它就会像返回 undefined
一样:
1 | function doNothing() { /* 没有代码 */ } |
空值的 return
和 return undefined
等效:
1 | function doNothing() { |
不要在 return
与返回值之间添加新行
对于 return
的长表达式,可能你会很想将其放在单独一行,如下所示:
1 | return |
但这不行,因为 JavaScript
默认会在 return
之后加上分号。上面这段代码和下面这段代码运行流程相同:
1 | return; |
因此,实际上它的返回值变成了空值。
如果我们想要将返回的表达式写成跨多行的形式,那么应该在 return
的同一行开始写此表达式。或者至少按照如下的方式放上左括号:
1 | return ( |
然后它就能像我们预想的那样正常运行了。
函数命名
函数就是行为 (action)
。所以它们的名字通常是动词。它应该简短且尽可能准确地描述函数的作用。这样读代码的人就能清楚地知道这个函数的功能。
一种普遍的做法是用动词前缀来开始一个函数,这个前缀模糊地描述了这个行为。团队内部必须就前缀的含义达成一致。
例如,以 "show"
开头的函数通常会显示某些内容。
函数以 XX 开始……
"get…"
—— 返回一个值,"calc…"
—— 计算某些内容,"create…"
—— 创建某些内容,"check…"
—— 检查某些内容并返回 boolean
值,等。
这类名字的示例:
1 | showMessage(..) // 显示信息 |
有了前缀,只需瞥一眼函数名,就可以了解它的功能是什么,返回什么样的值。
一个函数 —— 一个行为
一个函数应该只包含函数名所指定的功能,而不是做更多与函数名无关的功能。
两个独立的行为通常需要两个函数,即使它们通常被一起调用(在这种情况下,我们可以创建第三个函数来调用这两个函数)。
有几个违反这一规则的例子:
getAge
—— 如果它通过 console.log
将 age
显示出来,那就有问题了(只应该是获取)。createForm
—— 如果它包含修改文档的操作,例如向文档添加一个表单,那就有问题了(只应该创建表单并返回)。checkPermission
—— 如果它显示 access granted/denied
消息,那就有问题了(只应执行检查并返回结果)。
这些例子假设函数名前缀具有通用的含义。你和你的团队可以自定义这些函数名前缀的含义,但是通常都没有太大的不同。无论怎样,你都应该对函数名前缀的含义、带特定前缀的函数可以做什么以及不可以做什么有深刻的了解。所有相同前缀的函数都应该遵守相同的规则。并且,团队成员应该形成共识。
非常短的函数命名
常用的函数有时会有非常短的名字。
例如,jQuery
框架用 $
定义一个函数。LoDash
库的核心函数用 _
命名。
这些都是例外,一般而言,函数名应简明扼要且具有描述性。
函数 == 注释
函数应该简短且只有一个功能。如果这个函数功能复杂,那么把该函数拆分成几个小的函数是值得的。有时候遵循这个规则并不是那么容易,但这绝对是件好事。
一个单独的函数不仅更容易测试和调试 —— 它的存在本身就是一个很好的注释!
例如,比较如下两个函数 showPrimes(n)
。它们的功能都是输出到 n
的 素数。
第一个变体使用了一个标签:
1 | function showPrimes(n) { |
第二个变体使用附加函数 isPrime(n)
来检验素数:
1 | function showPrimes(n) { |
第二个变体更容易理解,不是吗?我们通过函数名 (isPrime)
就可以看出函数的行为,而不需要通过代码。人们通常把这样的代码称为 自描述。
因此,即使我们不打算重用它们,也可以创建函数。函数可以让代码结构更清晰,可读性更强。
总结
函数声明方式如下所示:
1 | function name(parameters, delimited, by, comma) { |
作为参数传递给函数的值,会被复制到函数的局部变量。
函数可以访问外部变量。但它只能从内到外起作用。函数外部的代码看不到函数内的局部变量。
函数可以返回值。如果没有返回值,则其返回的结果是 undefined。
为了使代码简洁易懂,建议在函数中主要使用局部变量和参数,而不是外部变量。
与不获取参数但将修改外部变量作为副作用的函数相比,获取参数、使用参数并返回结果的函数更容易理解。
函数命名:
函数名应该清楚地描述函数的功能。当我们在代码中看到一个函数调用时,一个好的函数名能够让我们马上知道这个函数的功能是什么,会返回什么。
一个函数是一个行为,所以函数名通常是动词。
目前有许多优秀的函数名前缀,如 create…、show…、get…、check… 等等。使用它们来提示函数的作用吧。
函数是脚本的主要构建块。现在我们已经介绍了基本知识,现在我们就可以开始创建和使用函数了。但这只是学习和使用函数的开始。我们将继续学习更多函数的相关知识,更深入地研究它们的先进特征。
函数表达式
在 JavaScript
中,函数不是“神奇的语言结构”,而是一种特殊的值。
我们在前面章节使用的语法称为 函数声明:
1 | function sayHi() { |
另一种创建函数的语法称为 函数表达式。
它允许我们在任何表达式的中间创建一个新函数。
例如:
1 | let sayHi = function() { |
在这里我们可以看到变量 sayHi
得到了一个值,新函数 function() { console.log("Hello")
; }。
由于函数创建发生在赋值表达式的上下文中(在 = 的右侧),因此这是一个 函数表达式。
请注意,在上述代码中 function
关键字后面没有函数名。函数表达式允许省略函数名。
这里我们立即将它赋值给变量,所以上面的两个代码示例的含义是一样的:
“创建一个函数并将其放入变量 sayHi
中”。
在更多更高阶的情况下,稍后我们会遇到,可以创建一个函数并立即调用,或者安排稍后执行,而不是存储在任何地方,因此保持匿名。
函数是一个值
重申一次:无论函数是如何创建的,函数都是一个值。上面的两个示例都在 sayHi
变量中存储了一个函数。
我们还可以用 console.log
显示这个变量的值:
1 | function sayHi() { |
注意,最后一行代码并不会运行函数,因为 sayHi
后没有括号。在某些编程语言中,只要提到函数的名称都会导致函数的调用执行,但 JavaScript
可不是这样。
在 JavaScript
中,函数是一个值,所以我们可以把它当成值对待。上面代码显示了一段字符串值,即函数的源码。
的确,在某种意义上说一个函数是一个特殊值,我们可以像 sayHi()
这样调用它。
但它依然是一个值,所以我们可以像使用其他类型的值一样使用它。
我们可以复制函数到其他变量:
1 | function sayHi() { // (1) 创建 |
解释一下上段代码发生的细节:
(1) 行声明创建了函数,并把它放入到变量 sayHi
。
(2) 行将 sayHi
复制到了变量 func
。请注意:sayHi
后面没有括号。如果有括号,func = sayHi()
会把 sayHi()
的调用结果写进 func
,而不是 sayHi 函数
本身。
现在函数可以通过 sayHi()
和 func()
两种方式进行调用。
我们也可以在第一行中使用函数表达式来声明 sayHi
:
1 | let sayHi = function() { // (1) 创建 |
这两种声明的函数是一样的。
为什么这里末尾会有个分号?
你可能想知道,为什么函数表达式结尾有一个分号 ;
,而函数声明没有:
1 | function sayHi() { |
答案很简单:这里函数表达式是在赋值语句 let sayHi = ...;
中以 function(…) {…}
的形式创建的。建议在语句末尾加上分号 ;
,它不是函数语法的一部分。
分号用于更简单的赋值,例如 let sayHi = 5;
,它也用于函数赋值。
回调函数
让我们多举几个例子,看看如何将函数作为值来传递以及如何使用函数表达式。
我们写一个包含三个参数的函数 ask(question, yes, no)
:
question
关于问题的文本yes
当回答为 “Yes” 时,要运行的脚本no
当回答为 “No” 时,要运行的脚本
函数需要提出 question(问题)
,并根据用户的回答,调用 yes()
或 no()
:
1 | function ask(question, yes, no) { |
在实际开发中,这样的函数是非常有用的。实际开发与上述示例最大的区别是,实际开发中的函数会通过更加复杂的方式与用户进行交互,而不是通过简单的 confirm
。在浏览器中,这样的函数通常会绘制一个漂亮的提问窗口。但这是另外一件事了。
ask
的两个参数值 showOk
和 showCancel
可以被称为 回调函数 或简称 回调。
主要思想是我们传递一个函数,并期望在稍后必要时将其“回调”。在我们的例子中,showOk
是回答 “yes” 的回调,showCancel
是回答 “no” 的回调。
我们可以使用函数表达式来编写一个等价的、更简洁的函数:
1 | function ask(question, yes, no) { |
这里直接在 ask(...)
调用内进行函数声明。这两个函数没有名字,所以叫 匿名函数。这样的函数在 ask
外无法访问(因为没有对它们分配变量),不过这正是我们想要的。
这样的代码在我们的脚本中非常常见,这正符合 JavaScript
语言的思想。
一个函数是表示一个“行为”的值。字符串或数字等常规值代表 数据
。
函数可以被视为一个 行为(action)
。
我们可以在变量之间传递它们,并在需要时运行。
函数表达式 vs 函数声明
让我们来总结一下函数声明和函数表达式之间的主要区别。
- 首先是语法:如何通过代码对它们进行区分。
函数声明:在主代码流中声明为单独的语句的函数:
1 | // 函数声明 |
函数表达式:在一个表达式中或另一个语法结构中创建的函数。下面这个函数是在赋值表达式 = 右侧创建的:
1 | // 函数表达式 |
- 更细微的差别是,
JavaScript
引擎会在 什么时候 创建函数。
函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。
一旦代码执行到赋值表达式 let sum = function… 的右侧,此时就会开始创建该函数,并且可以从现在开始使用(分配,调用等)。
函数声明则不同。
在函数声明被定义之前,它就可以被调用。
例如,一个全局函数声明对整个脚本来说都是可见的,无论它被写在这个脚本的哪个位置。
这是内部算法的缘故。当 JavaScript
准备 运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数。我们可以将其视为“初始化阶段”。
在处理完所有函数声明后,代码才被执行。所以运行时能够使用这些函数。
例如下面的代码会正常工作:
1 | sayHi("John"); // Hello, John |
函数声明 sayHi
是在 JavaScript
准备运行脚本时被创建的,在这个脚本的任何位置都可见。
……如果它是一个函数表达式,它就不会工作:
1 | sayHi("John"); // error! |
函数表达式在代码执行到它时才会被创建。只会发生在 (*) 行。为时已晚。
- 函数声明的另外一个特殊的功能是它们的块级作用域。
严格模式下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。
例如,想象一下我们需要依赖于在代码运行过程中获得的变量 age
声明一个函数 welcome()
。并且我们计划在之后的某个时间使用它。
如果我们使用函数声明,则以下代码无法像预期那样工作:
1 | let age = prompt("What is your age?", 18); |
这是因为函数声明只在它所在的代码块中可见。
下面是另一个例子:
1 | let age = 16; // 拿 16 作为例子 |
我们怎么才能让 welcome
在 if
外可见呢?
正确的做法是使用函数表达式,并将 welcome
赋值给在 if 外声明的变量,并具有正确的可见性。
下面的代码可以如愿运行:
1 | let age = prompt("What is your age?", 18); |
或者我们可以使用问号运算符 ? 来进一步对代码进行简化:
1 | let age = prompt("What is your age?", 18); |
- 什么时候选择函数声明与函数表达式?
根据经验,当我们需要声明一个函数时,优先考虑函数声明语法。它能够为组织代码提供更多的灵活性。因为我们可以在声明这些函数之前调用这些函数。
这对代码可读性也更好,因为在代码中查找 function f(…) {…}
比 let f = function(…) {…}
更容易。函数声明更“醒目”。
……但是,如果由于某种原因而导致函数声明不适合我们(我们刚刚看过上面的例子),那么应该使用函数表达式。
总结
函数是值。它们可以在代码的任何地方被分配,复制或声明。
如果函数在主代码流中被声明为单独的语句,则称为“函数声明”。
如果该函数是作为表达式的一部分创建的,则称其“函数表达式”。
在执行代码块之前,内部算法会先处理函数声明。所以函数声明在其被声明的代码块内的任何位置都是可见的。
函数表达式在执行流程到达时创建。
在大多数情况下,当我们需要声明一个函数时,最好使用函数声明,因为函数在被声明之前也是可见的。这使我们在代码组织方面更具灵活性,通常也会使得代码可读性更高。
所以,仅当函数声明不适合对应的任务时,才应使用函数表达式。在本章中,我们已经看到了几个例子,以后还会看到更多的例子。
箭头函数,基础知识
创建函数还有另外一种非常简单的语法,并且这种方法通常比函数表达式更好。
它被称为 “箭头函数”,因为它看起来像这样:
let func = (arg1, arg2, ..., argN) => expression;
这里创建了一个函数 func
,它接受参数 arg1..argN
,然后使用参数对右侧的 expression
求值并返回其结果。
换句话说,它是下面这段代码的更短的版本:
1 | let func = function(arg1, arg2, ..., argN) { |
让我们来看一个具体的例子:
1 | let sum = (a, b) => a + b; |
可以看到 (a, b) => a + b
表示一个函数接受两个名为 a
和 b
的参数。在执行时,它将对表达式 a + b
求值,并返回计算结果。
如果我们只有一个参数,还可以省略掉参数外的圆括号,使代码更短。
例如:
1 | let double = n => n * 2; |
如果没有参数,括号则是空的(但括号必须保留):
1 | let sayHi = () => console.log("Hello!"); |
箭头函数可以像函数表达式一样使用。
例如,动态创建一个函数:
1 | let age = prompt("What is your age?", 18); |
一开始,箭头函数可能看起来并不熟悉,也不容易读懂,但一旦我们看习惯了之后,这种情况很快就会改变。
箭头函数对于简单的单行行为 (action)
来说非常方便,尤其是当我们懒得打太多字的时候。
多行的箭头函数
到目前为止,我们看到的箭头函数非常简单。它们从 =>
的左侧获取参数,计算并返回右侧表达式的计算结果。
有时我们需要更复杂一点的函数,比如带有多行的表达式或语句。在这种情况下,我们可以使用花括号将它们括起来。主要区别在于,用花括号括起来之后,需要包含 return
才能返回值(就像常规函数一样)。
就像这样:
1 | let sum = (a, b) => { // 花括号表示开始一个多行函数 |
更多内容
在这里,我们赞扬了箭头函数的简洁性。但还不止这些!
箭头函数还有其他有趣的特性。
为了更深入地学习它们,我们首先需要了解一些 JavaScript
其他方面的知识,因此我们将在后面的 深入理解箭头函数 一章中再继续研究箭头函数。
现在,我们已经可以用箭头函数进行单行行为和回调了。
总结
箭头函数对于简单的操作很方便,特别是对于单行的函数。它具体有两种形式:
不带花括号:
(...args) => expression
—— 右侧是一个表达式:函数计算表达式并返回其结果。如果只有一个参数,则可以省略括号,例如n => n*2
。带花括号:
(...args) => { body }
—— 花括号允许我们在函数中编写多个语句,但是我们需要显式地return
来返回一些内容。
1 | mindmap |