参考学习链接: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 
 2- let myVar; 
 console.log(myVar); // 输出: undefined
- ** - NaN**:- NaN表示一个非数值(Not-a-Number)。
- 它是一个特殊的值,用来表示一个不可能的或未定义的数学运算结果。
- 任何涉及 NaN的操作都会返回NaN。
 - 示例: - 1 
 2- let 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 | 1and0 | 
| 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
 4- let x = 1; 
 x = -x;
 console.log( x ); // -1,一元负号运算符生效
- 如果一个运算符拥有两个运算元,那么它是 二元运算符。 
 例如,减号还存在二元运算符形式:- 1 
 2- let 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
 3- let counter = 2; 
 counter++; // 和 counter = counter + 1 效果一样,但是更简洁
 console.log( counter ); // 3
- 自减 - --将变量与 1 相减:- 1 
 2
 3- let 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
 4- let counter = 0; 
 counter++;
 ++counter;
 console.log( counter ); // 2,以上两行作用相同
- 如果我们想要对变量进行自增操作,并且 需要立刻使用自增后的值,那么我们需要使用前置形式: - 1 
 2- let counter = 0; 
 console.log( ++counter ); // 1
- 如果我们想要将一个数加一,但是我们想使用其自增之前的值,那么我们需要使用后置形式: - 1 
 2- let 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
 5- let firstName = ""; 
 let lastName = "";
 let nickName = "超级虎哥";
 console.log( firstName || lastName || nickName || "孙悟空"); // 超级虎哥- 如果所有变量的值都为假,结果就是 - "孙悟空"。
- 短路求值(Short-circuit evaluation)。 - 或运算符 - ||的另一个用途是所谓的“短路求值”。- 这指的是, - ||对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。- 如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,那么这一特性的重要性就变得显而易见了。 - 在下面这个例子中,只会打印第二条信息: - 1 
 2- true || 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 | 



