参考学习链接: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
2
3
let isBoss = confirm("Are you the boss?");

alert( isBoss ); // 如果“确定”按钮被按下,则显示 true
  • 方法二 控制台日志 console.log

推荐使用方法二。

1
2
console.log(1+2); //输出 3
console.log("你好"); //输出 你好

注释

// :单行注释,在双斜杠的后面的语句全部被当作注释,机器不会翻译,给人看的。

/* */ :这两个符号之间的内容全部被当作注释。与上面双斜杠不同的是,这个适用于多行注释。

现代模式/严格模式 use strict

  • 我暂时不清楚它的用途。等将来懂了再回来填坑。
  • 用法

全文启动严格模式:将 "use strict" 语句放在整个 JavaScript 文件的第一行。不放在第一行,是无效的。

严格模式一旦开启,没有回头路,只能手动删除语句才能关闭。

  • 备注

现代 JavaScript 支持 “class” 和 “module” —— 高级语言结构(本教程后续章节会讲到),它们会自动启用 use strict。因此,如果我们使用它们,则无需添加 "use strict" 指令。

三种特殊值

在 JavaScript 中,nullundefinedNaN 都是表示“无”或“不是值”的特殊值,但它们各自有不同的用途和含义:

  1. **null**:

    • null 是一个故意设计的值,用来表示变量的空值(empty value)。
    • 它代表一个空对象指针,即没有任何对象可以指向。
    • 通常用来表示一个变量没有指向任何对象,或者一个函数没有返回任何对象。

    示例:

    1
    let myVar = null;
  2. **undefined**:

    • undefined 表示一个变量已被声明,但没有被赋值。
    • 它表示变量当前没有值。
    • 当函数没有返回值时,也会默认返回 undefined

    示例:

    1
    2
    let myVar;
    console.log(myVar); // 输出: undefined
  3. **NaN**:

    • NaN 表示一个非数值(Not-a-Number)。
    • 它是一个特殊的值,用来表示一个不可能的或未定义的数学运算结果。
    • 任何涉及 NaN 的操作都会返回 NaN

    示例:

    1
    2
    let myVar = 0 / 0; // NaN
    console.log(myVar); // 输出: NaN

区别

  • 类型

    • nullObject 类型,尽管它表示空值。
    • undefinedUndefined 类型。
    • NaNNumber 类型,尽管它表示非数值。
  • 使用场景

    • null 通常用于表示空值或故意设置为“无”的情况。
    • undefined 通常用于表示变量尚未初始化。
    • NaN 用于表示数学运算的无效结果。
  • 相等性

    • nullundefined 相等,因为 null == undefined 返回 true
    • NaN 不等于任何值,包括它自己,即 NaN == NaN 返回 false
  • 转换

    • nullundefined 参与运算时,它们会被转换为 0
    • NaN 在运算时通常会导致结果为 NaN

检查

  • 使用 typeof 运算符可以区分 nullundefined,因为 typeof null 返回 "object"(这是一个历史遗留问题),而 typeof undefined 返回 "undefined"
  • 使用 isNaN() 函数可以检查一个值是否为 NaN,但要注意它会把非数字字符串转换为 NaN,所以最好与 typeof 结合使用,或者使用 Number.isNaN() 方法进行严格检查。

在编程时,应该根据具体场景选择合适的值来表示“无”或“未定义”的状态。


变量

在 JS 中,所有可以由我们自主命名的内容,都可以认为是一个 标识符,像 变量名 函数名 类名

使用 变量名 需要遵循如下的命名规范:

变量名 只能含有 字母数字下划线、**$**,且不能以数字开头。

例如,定义一个变量,名字为 QQ,它的值为 123456

1
let QQ = "123456";
  • 一般使用 驼峰命名法 为变量命名

第一个单词小写,后面每个单词开头大写,例如 myNamemyVeryLongName

  • 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
2
3
let wenben = '你好,世界' ;	//单引号插入文本
let wenben1 = "你好啊,世界" ; // 双引号插入文本
let wenben2 = `你好,世界啊` ; // 反引号插入文本

其中,单引号和双引号的作用一模一样。反引号 可以把语法类似这种的 ${ 代码 } 代码插入文本串中进行运行。

1
console.log( `我的名字是 ${ 660 +6 }`);  //输出 我的名字是 666

反引号 还能允许文本串自带 换行

1
2
3
4
5
6
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
2
3
4
5
6
7
8
9
console.log("Hello\nWorld"); // Hello 和 World 中间的 \n 相当于换行
console.log('Hello' + '\n' + 'World');// Hello 和 World 中间的 \n 相当于换行。加号用于连接字符串
console.log("Hello\"World"); // Hello 和 World中间的 \" 相当于引号"
console.log("Hello\'World"); // Hello 和 World中间的 \' 相当于引号'
console.log("Hello\\World"); // Hello 和 World中间的 \\ 相当于反斜线\
console.log("Hello\tWorld"); // Hello 和 World中间的 \t 相当于制表符,相当于按下了一次tab键
console.log('\x7A'); // 十六进制 Unicode 字符,输出 z
console.log('\u00A9'); // UTF-16 编码的十六进制 Unicode 字符,输出 ©
console.log('\u{1F60D}'); // UTF-32 编码的十六进制 Unicode 字符,输出 😍

字符串长度 length

1
2
3
4
5
6
let wenben = "abcde" ;
console.log(wenben.length); //输出 5

console.log("你好世界".length) //输出 4

console.log("你好\n世界".length) ; // 输出 5, 因为换行符(\n)占一个字符

访问字符

代码格式 string[i],代表访问字符串 string 的第 i+1 个字符。

1
2
3
4
5
let wenben = "你好世界" ;

console.log(wenben[0]); //输出 "你" 0 代表第一个文本,1 代表第二个文本,以此类推

console.log(wenben[3]); //输出 "界" 3 代表第四位的文本

遍历文本 对字符串中的每一个字符进行逐个访问的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let wenben = "你好世界" ;
//下面使用两种方法进行依次输出 变量【wenben】中的字符

//方法 1
for (let i = 0; i < wenben.length; i++)
{
console.log(wenben[i]);
}

console.log("接下来换另一种方法进行");

//方法 2
for( let text of wenben )
{
console.log (text);
}

会发现方法 2 更简洁。是的,在 JavaScript 中常用 for ... of 循环来遍历字符串、数组等。

字符串是不可变的

在 JavaScript 中,字符串不可更改。改变字符是不可能的。

我们证明一下为什么不可能:

1
2
3
4
let str = 'Hi';

str[0] = 'h'; // error
console.log( str[0] ); // 无法运行

通常的解决方法是创建一个新的字符串,并将其分配给 str 而不是以前的字符串。

例如:

1
2
3
4
5
let str = 'Hi';

str = 'h' + str[1]; // 替换字符串

console.log( str ); // hi

在接下来的章节,我们将看到更多相关示例。

字母大小写转换

[toLowerCase()] 和 [toUpperCase()] 方法可以改变字符串中英文字母的大小写。

1
2
3
4
5
6
7
8
let wenben = "你好,世jie" ;
let wenben1 = "NiHao,shiJie" ;

console.log( '你好,世jie'.toUpperCase() ); // 你好, 世 JIE
console.log( 'NiHao,shiJie'.toLowerCase() ); // nihao, shijie

console.log( wenben.toUpperCase() ); // 你好, 世 JIE
console.log( wenben1.toLowerCase() ); // nihao, shijie

在字符串查找是否包含另一个字符串

此功能又称为 ”在字符串中查找子字符串“

在 JavaScript 中,有很多方法能实现这个功能。

  • 老旧方法 str.indexOf(substring, pos)

它从给定位置 pos 开始(默认从左向右),在 str 中查找 substr,如果没有找到,则返回 -1,否则返回匹配成功的位置。

第二个参数 pos 是可选的,不加 pos,默认从头开始匹配,返回第一次匹配成功的位置。

1
2
3
4
5
let str = "这是一个测试字符串,测试一下,看看是否能找到所有的测试。";
let substring = "测试";
let index = str.indexOf(substring);

console.log(index); // 输出第一次找到“测试”的位置 4
1
2
3
4
5
6
7
8
9
10
11
12
let str = "这是一个测试字符串,测试一下,看看是否能找到所有的测试。";
let substring = "测试";
let index = str.indexOf(substring);
let indices = [];

while (index !== -1)
{
indices.push(index);
index = str.indexOf(substring, index + 1);
}

console.log(indices); // 输出所有找到的子字符串的位置 输出 (3) [4, 10, 25]

还有一个类似的方法 str.lastIndexOf(substr, position),它从字符串的末尾开始搜索到开头。

它会以相反的顺序列出这些事件。

  • 现代方法 includes,startsWith,endsWith

现代的方法 str.includes(substr, pos) 根据 str 中是否包含 substr 来返回 true/falsepos 为可选参数

如果我们 需要检测匹配,但不需要它的位置,那么这是正确的选择:

1
2
3
console.log( "Widget with id".includes("Widget") );    // true

console.log( "Hello".includes("Bye") ); // false

string.startWith(substr) 检测文本串 string 的字符,是否以子文本串 substr 开头 。

string.endWith(substr) 检测文本串 string 的字符,是否以子文本串 substr 结尾 。

1
2
console.log( "Widget".startsWith("Wid") ); // true,"Widget" 以 "Wid" 开始
console.log( "Widget".endsWith("get") ); // true,"Widget" 以 "get" 结束

获取子字符串

JavaScript 中有三种获取字符串的方法:substringsubstrslice

方法一 str.slice(start [, end]) 【推荐使用】
返回字符串从 start 到(但不包括)end 的部分。

例如:

1
2
3
let str = "stringify";
console.log( str.slice(0, 5) ); // 'strin',从 0 到 5 的子字符串(不包括 5)
console.log( str.slice(0, 1) ); // 's',从 0 到 1,但不包括 1,所以只有在 0 处的字符

如果没有第二个参数,slice 会一直运行到字符串末尾:

1
2
3
4
5
6
7
8
9
let str = "stringify";
console.log( str.slice(2) ); // 从第二个位置直到结束

//start/end 也有可能是负值。它们的意思是起始位置从字符串结尾计算:

let str = "stringify";

// 从右边的第四个位置开始,在右边的第一个位置结束
console.log( str.slice(-4, -1) ); // 'gif'

方法二 str.substring(start [, end])

返回字符串从 start 到(但不包括)end 的部分。

这与 slice 几乎相同,但它允许 start 大于 end

例如:

1
2
3
4
5
6
7
8
9
let str = "stringify";

// 这些对于 substring 是相同的
console.log( str.substring(2, 6) ); // "ring"
console.log( str.substring(6, 2) ); // "ring"

// ……但对 slice 是不同的:
console.log( str.slice(2, 6) ); // "ring"(一样)
console.log( str.slice(6, 2) ); // ""(空字符串)

不支持负参数(不像 slice),它们被视为 0。

方法三 str.substr(start [, length])
返回字符串从 start 开始的给定 length 的部分。

与以前的方法相比,这个允许我们指定 length 而不是结束位置:

1
2
3
4
5
6
let str = "stringify";
console.log( str.substr(2, 4) ); // 'ring',从位置 2 开始,获取 4 个字符
第一个参数可能是负数,从结尾算起:

let str = "stringify";
console.log( str.substr(-4, 2) ); // 'gi',从第 4 位获取 2 个字符

我们回顾一下这些方法,以免混淆:

方法 选择方式 负值参数
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 进制。

进制的范围:236 。默认 10,毕竟大家都是人类,学的都是十进制 。

为什么进制最大是 36 进制呢?

因为 0~9 在加上 26 个英文字母一共 36,在进制中,大小写是等效的。

给定一个数字 num ,想把它转成另一个进制 base ,需要使用方法 toString

num.toString(base)

1
2
3
4
5
6
7
8
let number = 123456789;
let number2 = number.toString(36);
console.log (number2); //输出 21i3v9


//当然下面这句也能实现目的
console.log("------这里是分割线--------")
console.log( 123456789..toString(36)); //输出 21i3v9

请注意 123456789..toString(36) 中的两个点不是打错了。如果我们想直接在一个数字上调用一个方法,比如上面例子中的 toString,那么我们需要在它后面放置两个点 ..

如果我们放置一个点:123456789.toString(36),那么就会出现一个 error,因为 JavaScript 语法隐含了第一个点之后的部分为小数部分。如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。

也可以写成 (123456789).toString(36)

舍入

函数 toFixed(n) 将数字精确到到小数点后 n 位进行四舍五入,并以字符串形式返回结果。

我们可以使用一元加号或 Number() 调用,将其转换为数字,例如 + num.toFixed(5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//这是一个具有 5 位小数的数字。
let number = 3.14159;

//保留小数点后 9 位,若位数不足自动添 0
console.log(number.toFixed(9)); //输出 3.141590000

console.log(number.toFixed(2)); //输出 3.14

//括号中什么都不写,则默认括号里面是 0,即四舍五入保留整数部分。
console.log(number.toFixed()); //输出 3

console.log(number.toFixed(3)); //输出 3.142

console.log(number.toFixed(0)); //输出 3

当然 数字函数 中支持更多对数字进行的操作。

检测变量是不是数字

isFinite 和 isNaN

isNaN(value) 将其参数转换为数字,然后测试它是否为 NaN

  • isNaN 函数用来检查其参数是否是非数值(NaN)。
  • 如果参数是 NaN,或者在将参数转换为数值时无法转换(例如字符串),则返回 true
  • isNaN 会先尝试将传入的参数转换为数字,如果转换失败,才判断为 NaN。
1
2
3
4
5
6
7
8
9
10
11
a = 123;
b = "123";
c = "123ww";

console.log(isNaN(NaN)); //true
console.log(isNaN(a)); //false 因为字符串不能转换为数字
console.log(isNaN(b)); //false 因为字符串不能转换为数字
console.log(isNaN(c)); //true

//NaN 与任何值比较都是返回 NaN
console.log(NaN===NaN); //false

**isFinite**:

  • isFinite 函数用来检查其参数是否是一个有限数值。
  • 如果参数是有限数值,即不是无穷大(Infinity 或 -Infinity),也不是 NaN,则返回 true
  • isFinite 不会尝试将参数转换为数字,它只检查参数本身的值。
1
2
3
4
5
6
7
isFinite(Infinity); // false
isFinite(-Infinity); // false
isFinite(NaN); // false
isFinite(0); // true
isFinite("Hello"); // false,因为是非数值
isFinite("123"); // false,因为是非数值
//想看输出值,记得加 console.log

区别

  • 检查范围
    • isNaN 专门用来检查 NaN。
    • isFinite 用来检查数值是否有限,即不是无穷大或 NaN。
  • 类型转换
    • isNaN 会进行类型转换,尝试将参数转换为数值。
    • isFinite 不会进行类型转换,只检查传入的原始值。
  • 用途
    • 当你需要确定一个值是否是 NaN 时,使用 isNaN
    • 当你需要确定一个值是否是有限数值时,使用 isFinite

注意事项

  • isNaN 函数在没有参数的情况下会返回 false,这是与其他全局函数(如 typeofisFinite)不同的行为。
  • 由于 isNaN 的类型转换特性,如果你想严格检查一个值是否为 NaN,应该使用 Number.isNaN() 方法。

在实际编程中,选择 isNaN 还是 isFinite 取决于你要解决的问题和你要检查的值的类型。

  • 请注意,在所有数字函数中,包括 isFinite,空字符串或仅有空格的字符串均被视为 0

提取文本串中开头的数字

现实生活中,我们经常会有带有单位的值,例如 CSS 中的 "100px""12pt"。并且,在很多国家,货币符号是紧随金额之后的,所以我们有 "19¥",并希望从中提取出一个数值。

这就是 parseIntparseFloat 的作用。

注意事项

  • parseIntparseFloat 都不会自动转换字符串中的数据类型。如果字符串的第一个字符不能被转换,它们会立即返回 NaN
  • 如果没有提供基数给 parseInt,它将尝试根据字符串来判断基数。例如,如果字符串以 "0x" 开头,它将尝试将字符串解析为十六进制数。
  • 如果使用 parseInt 解析字符串时提供了基数,但字符串不符合该基数的数值格式,它会返回解析到的错误结果或 NaN
  • parseFloat 不能解析科学记数法表示的数字,而 parseInt 可以。
  • 它们都会忽略字符串前面的空格,直到找到第一个非数字字符为止。

在实际使用中,你应该根据需要转换的数值类型来选择合适的函数。如果你需要整数,使用 parseInt;如果你需要浮点数,使用 parseFloat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
parseInt("10");        // 10
parseInt("10.00"); // 10
parseInt("10.95"); // 10
parseInt("34 45 66"); // 34
parseInt(" 60 "); // 60
parseInt("40 years", 10); // 40
parseInt("Days10", 10); // NaN
parseInt("11", 2); // 3,因为 "11" 是二进制表示


parseFloat("10"); // 10
parseFloat("10.00"); // 10
parseFloat("10.95"); // 10.95
parseFloat("34.45 66"); // 34.45
parseFloat(" 60.123 "); // 60.123
parseFloat("40 years"); // 40
parseFloat("Days10", 10); // NaN

数字函数

数字函数

学生生涯中常见的基础函数,都覆盖到了。

例如随机数、三角函数、幂函数、对数、标准圆周率等等。


变量的数据类型转换

  • 转换成字符串
  • 转换成数字
  • 转换成布尔

大多数情况下,运算符和函数会自动将赋予它们的值转换为正确的类型。

比如,console.log 会自动将任何值都转换为字符串以进行显示。算术运算符会将值转换为数字。

在某些情况下,我们需要将值主动地转换为我们期望的类型。

转换成字符串

我们也可以显式地调用 String(value) 来将 value 转换为字符串类型:

1
2
3
4
5
let value = true;
console.log(typeof value); // boolean

value = String(value); // 现在,值是一个字符串形式的 "true"
console.log(typeof value); // string

字符串转换最明显。false 变成 "false"null 变成 "null" 等。

转换成数字

在算术函数和表达式中,会自动进行 number 类型转换。

比如,当把除法 / 用于非 number 类型:

1
console.log( "6" / "2" ); // 3, string 类型的值被自动转换成 number 类型后进行计算

我们也可以使用 Number(value) 显式地将这个 value 转换为 number 类型。

1
2
3
4
5
6
let str = "123";
console.log(typeof str); // string

let num = Number(str); // 变成 number 类型 123

console.log(typeof num); // number

当我们从 string 类型源(如文本表单)中读取一个值,但期望输入一个数字时,通常需要进行显式转换。

如果该字符串不是一个有效的数字,转换的结果会是 NaN。例如:

1
2
3
let age = Number("an arbitrary string instead of a number");

console.log(age); // NaN,转换失败

number 类型转换规则:

变成……
undefined NaN
null 0
true 和 false 1 and 0
string 去掉首尾空白字符(空格、换行符 \n、制表符 \t 等)后的纯数字字符串中含有的数字。如果剩余字符串为空,则转换结果为 0。否则,将会从剩余字符串中“读取”数字。当类型转换出现 error 时返回 NaN

例子:

1
2
3
4
console.log( Number("   123   ") ); // 123
console.log( Number("123z") ); // NaN(从字符串“读取”数字,读到 "z" 时出现错误)
console.log( Number(true) ); // 1
console.log( Number(false) ); // 0

请注意 nullundefined 在这有点不同:null 变成数字 0undefined 变成 NaN

大多数数学运算符都执行这种转换,我们将在下一节中进行介绍。

转换成布尔(boolean)类型

它发生在逻辑运算中(稍后我们将进行条件判断和其他类似的东西),但是也可以通过调用 Boolean(value) 显式地进行转换。

转换规则如下:

  • 直观上为“空”的值(如 0、空字符串、nullundefinedNaN)将变为 false
  • 其他值变成 true

比如:

1
2
3
4
5
console.log( Boolean(1) ); // true
console.log( Boolean(0) ); // false

console.log( Boolean("hello") ); // true
console.log( Boolean("") ); // false

请注意:包含 0 的字符串 "0"true

一些编程语言(比如 PHP)视 "0"false。但在 JavaScript 中,非空的字符串总是 true

1
2
console.log( Boolean("0") ); // true
console.log( Boolean(" ") ); // 空格,也是 true(任何非空字符串都是 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
2
console.log( 13 % 2 ); // 输出 1,   13 除以 2 的余数
console.log( 20 % 3 ); // 输出 2, 20 除以 3 的余数

求幂 **

求幂运算 a ** b ,将 a 提升至 ab 次幂。

在数学运算中我们将其表示为 a b

例如:

1
2
3
console.log( 2 ** 2 ); // 2 ² = 4
console.log( 2 ** 3 ); // 2 ³ = 8
console.log( 2 ** 4 ); // 2 ⁴ = 16

就像在数学运算中一样,幂运算也适用于非整数。

例如,平方根是指数为 ½ 的幂运算:

1
2
console.log( 4 ** (1/2) ); // 2     (1/2 次方与平方根相同)
console.log( 8 ** (1/3) ); // 2 (1/3 次方与立方根相同)

用二元运算符 + 连接字符串

通常,加号 + 用于求和。

但是如果加号 + 被应用于字符串,它将合并(连接)各个字符串:

1
2
let s = "my" + "string";
console.log(s); // mystring

注意:只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串。

举个例子:

1
2
3
console.log( '1' + 2 ); // "12"
console.log( 2 + '1' ); // "21"
console.log( 2 + '天' ); // "2 天"

你看,第一个运算元和第二个运算元,哪个是字符串并不重要,只要其中任意一个是字符串,那么由 + 连接的两个运算元都会默认转换成字符串拼接起来。

下面是一个更复杂的例子:

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
2
3
console.log( 6 - '2' ); // 4,将 '2' 转换为数字
console.log( '6' / '2' ); // 3,将两个运算元都转换为数字
console.log(12 - "string"); // NaN 带有非数字的文本串无法转为数字

数字转化,一元运算符 +

加号 + 有两种形式。一种是上面我们刚刚讨论的二元运算符,还有一种是一元运算符。

一元运算符加号,或者说,加号 + 应用于单个值,对数字没有任何作用。
但是如果运算元不是数字,加号 + 则会将其转化为数字。

例如:

1
2
3
4
5
6
7
8
9
10
11
// 对数字无效(因为数学里面学过,在数字前面加正号没啥用)
let x = 1;
console.log( +x ); // 1

let y = -2;
console.log( +y ); // -2

// 转化非数字
console.log( +true ); // 1
console.log( +"" ); // 0
console.log(+"11KB"); // NaN 带有非数字的文本串无法转为数字

它的效果和 Number(...) 相同,但是更加简短。

我们经常会有将字符串转化为数字的需求。比如,如果我们正在从 HTML 表单中取值,通常得到的都是字符串。如果我们想对它们求和,该怎么办?

二元运算符加号会把它们合并成字符串:

1
2
3
4
let apples = "2";
let oranges = "3";

console.log( apples + oranges ); // "23",二元运算符加号合并字符串

如果我们想把它们当做数字对待,我们需要转化它们,然后再求和:

1
2
3
4
5
6
7
8
9
10
let apples = "2";
let oranges = "3";

// 在二元运算符加号起作用之前,所有的值都被转化为了数字
console.log( +apples + +oranges ); // 5
//如果你害怕眼花,可以加括号进行区分,括号的运算是最优先的
console.log( (+apples) + (+oranges) ); // 5

// 更长的写法
// console.log( Number(apples) + Number(oranges) ); // 5

一元运算符加号首先起作用,它们将字符串转为数字,然后二元运算符加号对它们进行求和。

为什么一元运算符先于二元运算符作用于运算元?
这是由于它们拥有 更高的优先级

运算符优先级

如果一个表达式拥有多个运算符,执行的顺序则由 优先级 决定。换句话说,所有的运算符中都隐含着优先级顺序。

我们知道在表达式 1 + 2 * 2 中,乘法先于加法计算。
这就是一个优先级问题。乘法比加法拥有 更高的优先级

圆括号拥有 最高优先级 ,所以如果我们对现有的运算顺序不满意,我们可以使用圆括号来修改运算顺序,就像这样:(1 + 2) * 2

不要觉得加多了括号丑,对于新手,为了减轻学习压力以及避免优先级错误,就应该多用括号。
因为括号的优先级是最高的,你只要用了括号,下面这张表中的运算优先级你都可以不用去记。

在 JavaScript 中有众多运算符。每个运算符都有对应的优先级数字。数字越大,越先执行。如果优先级相同,则按照由左至右的顺序执行。

这是一个摘抄自 Mozilla 的 优先级表(你没有必要把这全记住,但要记住一元运算符优先级高于二元运算符):

优先级 名称 符号
15 一元加号 +
15 一元负号 -
14 求幂 **
13 乘号 *
13 除号 /
12 加号 +
12 减号 -
2 赋值符 =

赋值运算符

我们知道赋值符号 = 也是一个运算符。从优先级表中可以看到它的优先级非常低,只有 2

当我们赋值时,比如 x = 2 * 2 + 1,所有的计算先执行,然后 = 才执行,将计算结果存储到 x

1
2
3
let x = 2 * 2 + 1;

console.log( x ); // 5

赋值 = 返回一个值

= 是一个运算符,而不是一个有着“魔法”作用的语言结构。

在 JavaScript 中,所有运算符都会返回一个值。这对于 +- 来说是显而易见的,但对于 = 来说也是如此。

语句 x = value 将值 value 写入 x

下面是一个在复杂语句中使用赋值的例子:

1
2
3
4
5
6
7
let a = 1;
let b = 2;

let c = 3 - (a = b + 1);

console.log( a ); // 3
console.log( c ); // 0

上面这个例子,(a = b + 1) 的结果是给 a 赋值为 3。然后该值被用于进一步的运算。

不过,请不要写这样的代码。这样的技巧绝对不会使代码变得更清晰或可读。

链式赋值(Chaining assignments)

另一个有趣的特性是链式赋值:

1
2
3
4
5
6
7
let a, b, c;

a = b = c = 2 + 2;

console.log( a ); // 4
console.log( b ); // 4
console.log( c ); // 4

链式赋值从右到左进行计算。
首先,对最右边的表达式 2 + 2 求值,
然后将其赋给左边的变量:cba
最后,所有的变量共享一个值。

同样,出于可读性,最好将这种代码分成几行:

1
2
3
c = 2 + 2;
b = c;
a = c;

这样可读性更强,尤其是在快速浏览代码的时候。

原地修改

我们经常需要对一个变量做运算,并将新的结果存储在同一个变量中。

例如:

1
2
3
let n = 2;
n = n + 5;
n = n * 2;

可以使用运算符 +=*= 来缩写这种表示。

1
2
3
4
5
let n = 2;
n += 5; // 现在 n = 7(等同于 n = n + 5)
n *= 2; // 现在 n = 14(等同于 n = n * 2)

console.log( n ); // 14

所有算术和位运算符都有简短的“修改并赋值”运算符:/=-= 等。

这类运算符的优先级与普通赋值运算符的优先级相同,所以它们在大多数其他运算之后执行:

1
2
3
4
5
let n = 2;

n *= 3 + 5;

console.log( n ); // 16 (右边部分先被计算,等同于 n *= 8)

自增/自减

对一个数进行加一、减一是最常见的数学运算符之一。

所以,对此有一些专门的运算符:

  • 自增 ++ 将变量与 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

两者都做同一件事:将变量 counter1 相加。

那么它们有区别吗?有,但只有当我们使用 ++/-- 的返回值时才能看到区别。

详细点说。我们知道,所有的运算符都有返回值。自增/自减也不例外。前置形式返回一个新的值,但后置返回原来的值(做加法/减法之前的值)。

为了直观看到区别,看下面的例子:

1
2
3
4
let counter = 1;
let a = ++counter; // (*)

console.log(a); // 2

(*) 所在的行是前置形式 ++counter,对 counter 做自增运算,返回的是新的值 2。因此 console.log 显示的是 2

下面让我们看看后置形式:

1
2
3
4
let counter = 1;
let a = counter++; // (*) 将 ++counter 改为 counter++

console.log(a); // 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
2
let counter = 1;
console.log( 2 * ++counter ); // 4

与下方例子对比:

1
2
let counter = 1;
console.log( 2 * counter++ ); // 2,因为 counter++ 返回的是“旧值”

尽管从技术层面上来说可行,但是这样的写法会降低代码的可阅读性。在一行上做多个操作 —— 这样并不好。

当阅读代码时,快速的视觉“纵向”扫描会很容易漏掉 counter++,这样的自增操作并不明显。

我们建议用“一行一个行为”的模式:

1
2
3
let counter = 1;
console.log( 2 * counter );
counter++;

位运算符

了解即可,前端开发过程中用的很少。
位运算符把运算元当做 32 位整数,并在它们的二进制表现形式上操作。

这些运算符不是 JavaScript 特有的。大部分的编程语言都支持这些运算符。

下面是位运算符:

  • 按位与 ( & )
  • 按位或 ( | )
  • 按位异或 ( ^ )
  • 按位非 ( ~ )
  • 左移 ( << )
  • 右移 ( >> )
  • 无符号右移 ( >>> )

这些运算符很少被使用,一般是我们需要在最低级别(位)上操作数字时才使用。我们不会很快用到这些运算符,因为在 Web 开发中很少使用它们,但在某些特殊领域中,例如密码学,它们很有用。当你需要了解它们的时候,可以阅读 MDN 上的 位操作符 章节。

逗号运算符

逗号运算符 , 是最少见最不常使用的运算符之一。有时候它会被用来写更简短的代码,因此为了能够理解代码,我们需要了解它。

逗号运算符能让我们处理多个表达式,使用 , 将它们分开。每个表达式都运行了,但是只有最后一个的结果会被返回。

举个例子:

1
2
3
let a = (1 + 2, 3 + 4);

console.log( a ); // 7(3 + 4 的结果)

这里,第一个表达式 1 + 2 运行了,但是它的结果被丢弃了。随后计算 3 + 4,并且该计算结果被返回。

逗号运算符的优先级非常低

请注意逗号运算符的优先级非常低,比 = 还要低,因此上面你的例子中圆括号非常重要。

如果没有圆括号:a = 1 + 2, 3 + 4 会先执行 +,将数值相加得到 a = 3, 7,然后赋值运算符 = 执行 a = 3,然后逗号之后的数值 7 不会再执行,它被忽略掉了。相当于 (a = 1 + 2), 3 + 4

为什么我们需要这样一个运算符,它只返回最后一个值呢?

有时候,人们会使用它把几个行为放在一行上来进行复杂的运算。

举个例子:

1
2
3
4
5
// 一行上有三个运算符
for (a = 1, b = 3, c = a * b; a < 10; a++)
{
...
}

这样的技巧在许多 JavaScript 框架中都有使用,这也是为什么我们提到它。但是通常它并不能提升代码的可读性,使用它之前,我们要想清楚。


值的比较

我们知道,在数学中有很多用于比较大小的运算符。

在 JavaScript 中,它们的编写方式如下:

  • 大于 / 小于:a > ba < b
  • 大于等于 / 小于等于:a >= ba <= b
  • 检查两个值的相等:a == b,请注意双等号 == 表示相等性检查,而单等号 a = b 表示赋值。
  • 检查两个值不相等:不相等在数学中的符号是 ,但在 JavaScript 中写成 a != b

比较的结果是 Boolean 类型

所有比较运算符均返回布尔值:

  • true —— 表示“yes(是)”,“correct(正确)”或“the truth(真)”。
  • false —— 表示“no(否)”,“wrong(错误)”或“not the truth(非真)”。

示例:

1
2
3
console.log( 2 > 1 );  // true(正确)
console.log( 2 == 1 ); // false(错误)
console.log( 2 != 1 ); // true(正确)

和其他类型的值一样,比较的结果可以被赋值给任意变量:

1
2
let result = 5 > 4; // 把比较的结果赋值给 result
console.log( result ); // true

字符串比较

在比较字符串的大小时,JavaScript 会使用“字典(dictionary)”或“词典(lexicographical)”顺序进行判定。

换言之,字符串是按字符(母)逐个进行比较的。

例如:

1
2
3
console.log( 'Z' > 'A' ); // true
console.log( 'Glow' > 'Glee' ); // true
console.log( 'Bee' > 'Be' ); // true

字符串的比较算法非常简单:

  1. 首先比较两个字符串的首位字符大小。
  2. 如果一方字符较大(或较小),则该字符串大于(或小于)另一个字符串。算法结束。
  3. 否则,如果两个字符串的首位字符相等,则继续取出两个字符串各自的后一位字符进行比较。
  4. 重复上述步骤进行比较,直到比较完成某字符串的所有字符为止。
  5. 如果两个字符串的字符同时用完,那么则判定它们相等,否则未结束(还有未比较的字符)的字符串更大。

在上面的第一个例子中,'Z' > 'A' 比较在算法的第 1 步就得到了结果。

在第二个例子中,字符串 GlowGlee 的比较则需要更多步骤,因为需要逐个字符进行比较:

  1. GG 相等。
  2. ll 相等。
  3. oe 大,算法停止,第一个字符串大于第二个。

非真正的字典顺序,而是 Unicode 编码顺序

在上面的算法中,比较大小的逻辑与字典或电话簿中的排序很像,但也不完全相同。

比如说,字符串比较对字母大小写是敏感的。大写的 "A" 并不等于小写的 "a"。哪一个更大呢?实际上小写的 "a" 更大。这是因为在 JavaScript 使用的内部编码表中(Unicode),小写字母的字符索引值更大。我们会在 字符串 这章讨论更多关于字符串的细节。

不同类型间的比较

当对不同类型的值进行比较时,JavaScript 会首先将其转化为数字(number)再判定大小。

例如:

1
2
3
console.log( '2' > 1 ); // true,字符串 '2' 会被转化为数字 2
console.log( '01' == 1 ); // true,字符串 '01' 会被转化为数字 1
console.log( '2wwww' > 1 ); //false,

‘2wwww’ 不是一个有效的数字格式,它会被转换成 NaN,在 JavaScript 中,NaN 与任何值(包括它自己)的比较都是不相等的。

对于布尔类型值,true 会被转化为 1false 转化为 0

例如:

1
2
console.log( true == 1 ); // true
console.log( false == 0 ); // true

一个有趣的现象

有时候,以下两种情况会同时发生:

  • 若直接比较两个值,其结果是相等的。
  • 若把两个值转为布尔值,它们可能得出完全相反的结果,即一个是 true,一个是 false

例如:

1
2
3
4
5
6
7
let a = 0;
console.log( Boolean(a) ); // false

let b = "0";
console.log( Boolean(b) ); // true

console.log(a == b); // true!

对于 JavaScript 而言,这种现象其实挺正常的。因为 JavaScript 会把待比较的值转化为数字后再做比较(因此 "0" 变成了 0)。若只是将一个变量转化为 Boolean 值,则会使用其他的类型转换规则。

严格相等

普通的相等性检查 == 存在一个问题,它不能区分出 0false

1
console.log( 0 == false ); // true

也同样无法区分空字符串和 false

1
console.log( '' == false ); // true

这是因为在比较不同类型的值时,处于相等判断符号 == 两侧的值会先被转化为数字。空字符串和 false 也是如此,转化后它们都为数字 0。

如果我们需要区分 0false,该怎么办?

严格相等运算符 === 在进行比较时不会做任何的类型转换。

换句话说,如果 ab 属于不同的数据类型,那么 a === b 不会做任何的类型转换而立刻返回 false

让我们试试:

1
console.log( 0 === false ); // false,因为被比较值的数据类型不同

同样的,与“不相等”符号 != 类似,“严格不相等”表示为 !==

严格相等的运算符虽然写起来稍微长一些,但是它能够很清楚地显示代码意图,降低你犯错的可能性。

对 null 和 undefined 进行比较

当使用 nullundefined 与其他值进行比较时,其返回结果常常出乎你的意料。

  • 当使用严格相等 === 比较二者时

    它们不相等,因为它们属于不同的类型。
    console.log( null === undefined ); // false

  • 当使用非严格相等 == 比较二者时
    JavaScript 存在一个特殊的规则,会判定它们相等。它们俩就像“一对恋人”,仅仅等于对方而不等于其他任何的值(只在非严格相等下成立)。
    console.log( null == undefined ); // true

  • 当使用数学式或其他比较方法 < > <= >= 时:
    null/undefined 会被转化为数字:null 被转化为 0undefined 被转化为 NaN

下面让我们看看,这些规则会带来什么有趣的现象。同时更重要的是,我们需要从中学会如何远离这些特性带来的“陷阱”。

奇怪的结果:null vs 0

通过比较 null 和 0 可得:

1
2
3
console.log( null > 0 );  //  (1)false
console.log( null == 0 ); // (2)false
console.log( null >= 0 ); // (3)true

为什么会出现这种反常结果,这是因为相等性检查 == 和普通比较符 > < >= <= 的代码逻辑是相互独立的。
进行值的比较时,null 会被转化为数字,因此它被转化为了 0。这就是为什么(3)中 null >= 0 返回值是 true,(1)中 null > 0 返回值是 false。

另一方面,undefinednull 在相等性检查 == 中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(2)中 null == 0 会返回 false。

特立独行的 undefined

undefined 不应该被与其他值进行比较:

1
2
3
console.log( undefined > 0 ); // false (1)
console.log( undefined < 0 ); // false (2)
console.log( undefined == 0 ); // false (3)

为何它看起来如此厌恶 0?返回值都是 false!

原因如下:

  • (1)(2) 都返回 false 是因为 undefined 在比较中被转换为了 NaN,而 NaN 是一个特殊的数值型值,它与任何值进行比较都会返回 false
  • (3) 返回 false 是因为这是一个相等性检查,而 undefined 只与 null 相等,不会与其他值相等。

避免问题

我们为何要研究上述示例?我们需要时刻记得这些古怪的规则吗?不,其实不需要。虽然随着代码写得越来越多,我们对这些规则也都会烂熟于胸,但是我们需要更为可靠的方法来避免潜在的问题:

  • 除了严格相等 === 外,其他但凡是有 undefined/null 参与的比较,我们都需要格外小心。
  • 除非你非常清楚自己在做什么,否则永远不要使用 >= > < <= 去比较一个可能为 null/undefined 的变量。对于取值可能是 null/undefined 的变量,请按需要分别检查它的取值情况。

总结

  • 比较运算符始终返回布尔值。
  • 字符串的比较,会按照“词典”顺序逐字符地比较大小。
  • 当对不同类型的值进行比较时,它们会先被转化为数字(不包括严格相等检查)再进行比较。
  • 在非严格相等 == 下,nullundefined 相等且各自不等于任何其他的值。
  • 在使用 >< 进行比较时,需要注意变量可能为 null/undefined 的情况。比较好的方法是单独检查变量是否等于 null/undefined

条件分支:if 和 ‘?’

有时我们需要根据不同条件执行不同的操作。

我们可以使用 if 语句和条件运算符 ?(也称为“问号”运算符)来实现。

“if” 语句

if(...) 语句计算括号里的条件表达式,如果计算结果是 true,就会执行对应的代码块。

例如:

1
2
3
4
let year = 2024;

if (year == 2024)
console.log( '没错,今年正是2024年' );

在上面这个例子中,条件是一个简单的相等性检查(year == 2024),但它还可以更复杂。

如果有多个语句要执行,我们必须将要执行的代码块封装在大括号内:

1
2
3
4
5
6
let year = 2024;
if (year == 2024)
{
console.log( "正确!" );
console.log( "你真聪明!" );
}

建议每次使用 if 语句都用大括号 {} 来包装代码块,即使只有一条语句。这样可以提高代码可读性。

布尔转换

if (…) 语句会计算圆括号内的表达式,并将计算结果转换为布尔型。

让我们回顾一下 类型转换 一章中的转换规则:

  • 数字 0、空字符串 ""nullundefinedNaN 都会被转换成 false。因为它们被称为“假值(falsy)”。
  • 其他值被转换为 true,所以它们被称为“真值(truthy)”。

所以,下面这个条件下的代码永远不会执行:

1
2
3
if (0) { // 0 是假值(falsy)
...
}

……但下面的条件 —— 始终有效:

1
2
3
if (1) { // 1 是真值(truthy)
...
}

我们也可以将预先计算的布尔值传入 if 语句,像这样:

1
2
3
4
5
6
7
let year = 2024;
let cond = (year == 2024); // 相等运算符的结果是 true 或 false

if (cond)
{
...
}

“else” 语句

if 语句有时会包含一个可选的 “else” 块。如果判断条件不成立,就会执行它内部的代码。

例如:

1
2
3
4
5
6
7
8
9
10
let year = 2025;

if (year == 2024)
{
console.log( '年份不对哦' );
}
else
{
console.log( '只要不等于2024,都会看到这句话' ); // 2024 以外的任何值
}

多个条件:“else if”

有时我们需要测试一个条件的几个变体。我们可以通过使用 else if 子句实现。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let year = 2022);

if (year < 2024)
{
console.log( 'Too early...' );
}
else if (year > 2024)
{
console.log( 'Too late' );
}
else
{
console.log( 'Exactly!' );
}

在上面的代码中,JavaScript 先检查 year < 2024。如果条件不符合,就会转到下一个条件 year > 2024。如果这个条件也不符合,则会显示最后一个 console.log

可以有更多的 else if 块。结尾的 else 是可选的。

条件运算符 ‘?’

有时我们需要根据一个条件去赋值一个变量。

如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
let accessAllowed;
let age = 24;

if (age > 18)
{
accessAllowed = true;
}
else
{
accessAllowed = false;
}

console.log(accessAllowed);

所谓的“条件”或“问号”运算符让我们可以更简短地达到目的。

这个运算符通过问号 ? 表示。有时它被称为三元运算符,被称为“三元”是因为该运算符中有三个操作数。实际上它是 JavaScript 中唯一一个有这么多操作数的运算符。

语法:

1
let result = condition ? value1 : value2;

计算(condition)结果,如果结果为真,则返回 value1,否则返回 value2

例如:

1
2
let age =24;
let accessAllowed = (age > 18) ? true : false;

技术上讲,我们可以省略 age > 18 外面的括号。问号运算符的优先级较低,所以它会在比较运算符 > 后执行。

下面这个示例会执行和前面那个示例相同的操作:

1
2
3
// 比较运算符 "age > 18" 首先执行
//(不需要将其包含在括号中)
let accessAllowed = age > 18 ? true : false;

但括号可以使代码可读性更强,所以我们建议使用它们。

请注意:

在上面的例子中,你可以不使用问号运算符,因为比较本身就返回 true/false

1
2
// 下面代码同样可以实现
let accessAllowed = age > 18;

多个 ‘?’

使用一系列问号 ? 运算符可以返回一个取决于多个条件的值。

例如:

1
2
3
4
5
6
7
8
let age = 18;

let message = (age < 3) ? '小宝贝!' :
(age < 18) ? '小孩子' :
(age < 100) ? '你肯定成年了' :
'洪福齐天!';

console.log( message );
  1. 第一个问号检查 age < 3
  2. 如果为真 — 返回 'Hi, baby!'。否则,会继续执行冒号 ":" 后的表达式,检查 age < 18
  3. 如果为真 — 返回 'Hello!'。否则,会继续执行下一个冒号 ":" 后的表达式,检查 age < 100
  4. 如果为真 — 返回 'Greetings!'。否则,会继续执行最后一个冒号 ":" 后面的表达式,返回 'What an unusual age!'

这是使用 if..else 实现上面的逻辑的写法:

1
2
3
4
5
6
7
8
9
if (age < 3) {
message = '小宝贝!';
} else if (age < 18) {
message = '小孩子';
} else if (age < 100) {
message = '你肯定成年了';
} else {
message = '洪福齐天!';
}

‘?’ 的非常规使用

有时可以使用问号 ? 来代替 if 语句:

1
2
3
4
console.log('今年是哪一年?');
let year = 2022 ;
(year == '2024') ?
console.log('答对了!') : console.log('错误,今年是2024年哦');

根据条件 year =='2024',要么执行 ? 后面的第一个表达式并显示对应内容,要么执行第二个表达式并显示对应内容。

在这里我们不是把结果赋值给变量。而是根据条件执行不同的代码。

不建议这样使用问号运算符。

这种写法比 if 语句更短,对一些程序员很有吸引力。但它的可读性差。

下面是使用 if 语句实现相同功能的代码,进行下比较:

1
2
3
4
5
6
7
8
console.log('今年是哪一年?');
let year = 2022 ;

if (year == '2024') {
console.log('答对了!') ;
} else {
console.log('错误,今年是2024年哦');
}

因为我们的眼睛垂直扫描代码。所以,跨越几行的代码块比长而水平的代码更易于理解。

问号 ? 的作用是根据条件返回其中一个值。请正确使用它。当需要执行不同的代码分支时,请使用 if


逻辑运算符

JavaScript 中有四个逻辑运算符:||(或),&&(与),!(非),??(空值合并运算符)。本文我们先介绍前三个,在下一篇文章中再详细介绍 ?? 运算符。

虽然它们被称为“逻辑”运算符,但这些运算符却可以被应用于任意类型的值,而不仅仅是布尔值。它们的结果也同样可以是任意类型。

||(或)

两个竖线符号表示“或”运算符:

1
result = a || b;

在传统的编程中,逻辑 仅能够操作布尔值。如果参与运算的任意一个参数为 true,返回的结果就为 true,否则返回 false

在 JavaScript 中,逻辑运算符更加灵活强大。但是,首先让我们看一下操作数是布尔值的时候发生了什么。

下面是四种可能的逻辑组合:

1
2
3
4
console.log( true || true );   // true
console.log( false || true ); // true
console.log( true || false ); // true
console.log( false || false ); // false

正如我们所见,除了两个操作数都是 false 的情况,结果都是 true

如果操作数不是布尔值,那么它将会被转化为布尔值来参与运算。

例如,数字 1 被作为 true 处理,数字 0 则被作为 false

1
2
3
if (1 || 0) { // 工作原理相当于 if( true || false )
console.log( 'truthy!' );
}

大多数情况下,逻辑或 || 会被用在 if 语句中,用来测试是否有 任何 给定的条件为 true

例如:

1
2
3
4
5
let hour = 9;

if (hour < 10 || hour > 18) {
console.log( '超市的门是关着的。' );
}

我们可以传入更多的条件:

1
2
3
4
5
6
let hour = 12;
let isWeekend = true;

if (hour < 10 || hour > 18 || isWeekend) {
console.log( '超市的门是关着的。' ); // 是周末
}

或运算寻找第一个真值

上文提到的逻辑处理多少有些传统了。下面让我们看看 JavaScript 的“附加”特性。

拓展的算法如下所示。

给定多个参与或运算的值:

1
result = value1 || value2 || value3;

或运算符 || 做了如下的事情:

  • 从左到右依次计算操作数。
  • 处理每一个操作数时,都将其转化为布尔值。如果结果是 true,就停止计算,返回这个操作数的初始值。
  • 如果所有的操作数都被计算过(也就是,转换结果都是 false),则返回最后一个操作数。

返回的值是操作数的初始形式,不会做布尔转换。

换句话说,一个或运算 || 的链,将返回第一个真值,如果不存在真值,就返回该链的最后一个值。

例如:

1
2
3
4
5
6
7
console.log( 1 || 0 ); // 1(1 是真值)

console.log( null || 1 ); // 1(1 是第一个真值)
console.log( null || 0 || 1 ); // 1(第一个真值)
console.log( null || 0 || '你好' ); // 你好

console.log( undefined || null || 0 ); // 0(都是假值,返回最后一个值)

与“纯粹的、传统的、仅仅处理布尔值的或运算”相比,这个规则就引起了一些很有趣的用法。

  1. 获取变量列表或者表达式中的第一个真值。

    例如,我们有变量 firstNamelastNamenickName,都是可选的(即可以是 undefined,也可以是假值)。

    我们用或运算 || 来选择有数据的那一个,并显示出来(如果没有设置,则用 "Anonymous"):

    1
    2
    3
    4
    5
    let firstName = "";
    let lastName = "";
    let nickName = "超级虎哥";

    console.log( firstName || lastName || nickName || "孙悟空"); // 超级虎哥

    如果所有变量的值都为假,结果就是 "孙悟空"

  2. 短路求值(Short-circuit evaluation)。

    或运算符 || 的另一个用途是所谓的“短路求值”。

    这指的是,|| 对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。

    如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,那么这一特性的重要性就变得显而易见了。

    在下面这个例子中,只会打印第二条信息:

    1
    2
    true || console.log("这条信息不会打印");
    false || console.log("打印出来了");

    在第一行中,或运算符 || 在遇到 true 时立即停止运算,所以 console.log 没有运行。

    有时,人们利用这个特性,只在左侧的条件为假时才执行命令。

&&(与)

两个 & 符号表示 && 与运算符:

1
result = a && b;

在传统的编程中,当两个操作数都是真值时,与运算返回 true,否则返回 false

1
2
3
4
console.log( true && true );   // true
console.log( false && true ); // false
console.log( true && false ); // false
console.log( false && false ); // false

带有 if 语句的示例:

1
2
3
4
5
6
let hour = 12;
let minute = 30;

if (hour == 12 && minute == 30) {
console.log( '当前时间是 12:30' );
}

就像或运算一样,与运算的操作数可以是任意类型的值:

1
2
3
if (1 && 0) { // 作为 true && false 来执行
console.log( "这条信息不会输出,因为条件是false。" );
}

与运算寻找第一个假值

给出多个参加与运算的值:

1
result = value1 && value2 && value3;

与运算 && 做了如下的事:

  • 从左到右依次计算操作数。
  • 在处理每一个操作数时,都将其转化为布尔值。如果结果是 false,就停止计算,并返回这个操作数的初始值。
  • 如果所有的操作数都被计算过(例如都是真值),则返回最后一个操作数。

换句话说,与运算返回第一个假值,如果没有假值就返回最后一个值。

上面的规则和或运算很像。区别就是与运算返回第一个假值,而或运算返回第一个真值。

例如:

1
2
3
4
5
6
7
8
9
// 如果第一个操作数是真值,
// 与运算返回第二个操作数:
console.log( 1 && 0 ); // 0
console.log( 1 && 5 ); // 5

// 如果第一个操作数是假值,
// 与运算将直接返回它。第二个操作数会被忽略
console.log( null && 5 ); // null
console.log( 0 && "no matter what" ); // 0

我们也可以在一行代码上串联多个值。查看第一个假值是如何被返回的:

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
2
3
let x = 1;

(x > 0) && console.log( '大于零' );

&& 右边的代码只有运算抵达到那里才能被执行。也就是,当且仅当 (x > 0) 为真。

所以我们基本可以类似地得到:

1
2
3
let x = 1;

if (x > 0) console.log( '大于零!' );

虽然使用 && 写出的变体看起来更短,但 if 更明显,并且往往更具可读性。因此,我们建议根据每个语法结构的用途来使用:如果我们想要 if,就使用 if;如果我们想要逻辑与,就使用 &&

!(非)

感叹符号 ! 表示布尔非运算符。

语法相当简单:

1
result = !value;

逻辑非运算符接受一个参数,并按如下运作:

  1. 将操作数转化为布尔类型:true/false
  2. 返回相反的值。

例如:

1
2
console.log( !true ); // false
console.log( !0 ); // true

两个非运算 !! 有时候用来将某个值转化为布尔类型:

1
2
console.log( !!"non-empty string" ); // true
console.log( !!null ); // false

也就是,第一个非运算将该值转化为布尔类型并取反,第二个非运算再次取反。最后我们就得到了一个任意值到布尔值的转化。

有一个略显冗长的方式也可以实现同样的效果 —— 一个内建的 Boolean 函数:

1
2
console.log( Boolean("non-empty string") ); // true
console.log( Boolean(null) ); // false

非运算符 ! 的优先级在所有逻辑运算符里面最高,所以它总是在 &&|| 之前执行。
https://zh.javascript.info/logical-operators


空值合并运算符 ‘ ?? ‘

这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills.

空值合并运算符(nullish coalescing operator)的写法为两个问号 ??

由于它对待 nullundefined 的方式类似,所以在本文中我们将使用一个特殊的术语对其进行表示。为简洁起见,当一个值既不是 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
2
3
let user;

console.log(user ?? "匿名"); // 匿名(user 未定义)

在下面这个例子中,我们将一个名字赋值给了 user

1
2
3
let user = "John";

console.log(user ?? "匿名"); // John(user 已定义)

我们还可以使用 ?? 序列从一系列的值中选择出第一个非 null/undefined 的值。

假设我们在变量 firstNamelastNamenickName 中存储着一个用户的数据。如果用户决定不填写相应的值,则所有这些变量的值都可能是未定义的。

我们想使用这些变量之一显示用户名,如果这些变量的值都是 null/undefined,则显示 “匿名”。

让我们使用 ?? 运算符来实现这一需求:

1
2
3
4
5
6
let firstName = null;
let lastName = null;
let nickName = "Supercoder";

// 显示第一个已定义的值:
console.log(firstName ?? lastName ?? nickName ?? "匿名"); // Supercoder

与 || 比较

或运算符 || 可以以与 ?? 运算符相同的方式使用。像我们在 上一章 所讲的那样。

例如,在上面的代码中,我们可以用 || 替换掉 ??,也可以获得相同的结果:

1
2
3
4
5
6
let firstName = null;
let lastName = null;
let nickName = "Supercoder";

// 显示第一个真值:
console.log(firstName || lastName || nickName || "Anonymous"); // Supercoder

纵观 JavaScript 发展史,或 || 运算符先于 ?? 出现。它自 JavaScript 诞生就存在了,因此开发者长期将其用于这种目的。

另一方面,空值合并运算符 ?? 是最近才被添加到 JavaScript 中的,它的出现是因为人们对 || 不太满意。

它们之间重要的区别是:

  • || 返回第一个 值。
  • ?? 返回第一个 已定义的 值。

换句话说,|| 无法区分 false0、空字符串 ""null/undefined。它们都一样 —— 假值(falsy values)。如果其中任何一个是 || 的第一个参数,那么我们将得到第二个参数作为结果。

不过在实际中,我们可能只想在变量的值为 null/undefined 时使用默认值。也就是说,当该值确实未知或未被设置时。

例如,考虑下面这种情况:

1
2
3
4
let height = 0;

console.log(height || 100); // 100
console.log(height ?? 100); // 0
  • height || 100 首先会检查 height 是否为一个假值,它是 0 ,确实是假值。

    • 所以,|| 运算的结果为第二个参数,100
  • height ?? 100 首先会检查 height 是否为 null/undefined ,发现它不是。

    • 所以,结果为 height 的原始值,0

实际上,高度 0 通常是一个有效值,它不应该被替换为默认值。所以 ?? 运算得到的是正确的结果。

优先级

?? 运算符的优先级与 || 相同,它们的优先级都为 3,详见:MDN

这意味着,就像 || 一样,空值合并运算符在 =? 运算前计算,但在大多数其他运算(例如 +*)之后计算。

所以我们可能需要在这样的表达式中添加括号:

1
2
3
4
5
6
7
let height = null;
let width = null;

// 重要:使用括号
let area = (height ?? 100) * (width ?? 50);

console.log(area); // 5000

否则,如果我们省略了括号,则由于 * 的优先级比 ?? 高,它会先执行,进而导致错误的结果。

1
2
3
4
5
// 没有括号
let area = height ?? 100 * width ?? 50;

// ……实际的计算顺序如下(不符合我们的期望):
let area = height ?? (100 * width) ?? 50;

?? 与 && 或 || 一起使用

出于安全原因,JavaScript 禁止将 ?? 运算符与 &&|| 运算符一起使用,除非使用括号明确指定了优先级。

下面的代码会触发一个语法错误:

1
let x = 1 && 2 ?? 3; // Syntax error

这个限制无疑是值得商榷的,它被添加到语言规范中是为了避免人们从 || 切换到 ?? 时的编程错误。

可以明确地使用括号来解决这个问题:

1
2
3
let x = (1 && 2) ?? 3; // 正常工作了

console.log(x); // 2

总结

  • 空值合并运算符 ?? 提供了一种从列表中选择第一个“已定义的”值的简便方式。

    它被用于为变量分配默认值:

    1
    2
    // 当 height 的值为 null 或 undefined 时,将 height 的值设置为 100
    height = height ?? 100;
  • ?? 运算符的优先级非常低,仅略高于 ?=,因此在表达式中使用它时请考虑添加括号。

  • 如果没有明确添加括号,不能将其与 ||&& 一起使用。


循环:while 和 for

我们经常需要重复执行一些操作。循环 是一种重复运行同一代码的方法。

for…of 和 for…in 循环

本文仅涵盖了基础的循环:whiledo..whilefor(..; ..; ..)

如果你阅读本文是为了寻找其他类型的循环,那么:

  • 用于遍历对象属性的 for..in 循环请见:for…in
  • 用于遍历数组和可迭代对象的循环分别请见:for…ofiterables

“while” 循环

while 循环的语法如下:

1
2
3
4
while (condition) {
// 代码
// 所谓的“循环体”
}

condition 为真时,执行循环体的 code

例如,以下将循环输出当 i < 3 时的 i 值:

1
2
3
4
5
let i = 0;
while (i < 3) { // 依次显示 0、1 和 2
console.log( i );
i++;
}

循环体的单次执行(一次循环)叫作 一次迭代。上面示例中的循环进行了三次迭代。

如果上述示例中没有 i++,那么循环(理论上)会永远重复执行下去。实际上,浏览器提供了阻止这种循环的方法,我们可以通过终止进程,来停掉服务器端的 JavaScript。

任何表达式或变量都可以是循环条件,而不仅仅是比较。在 while 中的循环条件会被计算,计算结果会被转化为布尔值。

例如,while (i != 0) 可简写为 while (i)

1
2
3
4
5
let i = 3;
while (i) { // 当 i 变成 0 时,条件为假,循环终止
console.log( i );
i--;
}

单行循环体不需要大括号

如果循环体只有一条语句,则可以省略大括号 {…}

1
2
let i = 3;
while (i) console.log(i--);

“do…while” 循环

使用 do..while 语法可以将条件检查移至循环体 下面

1
2
3
do {
// 循环体
} while (condition);

循环首先执行一次循环体,然后再检查条件,当条件为真时,重复执行循环体。

例如:

1
2
3
4
5
let i = 0;
do {
console.log( i );
i++;
} while (i < 3);

这种形式的语法很少使用,除非你希望不管条件是否为真,循环体 至少执行一次。通常我们更倾向于使用另一个形式:while(…) {…}

“for” 循环

for 循环更加复杂,但它是最常使用的循环形式。

for 循环看起来就像这样:

1
2
3
for (begin; condition; step) {
// ……循环体……
}

我们通过示例来了解一下这三个部分的含义。
下述循环从 i 等于 03(但不包括 3)运行 console.log(i)

1
2
3
for (let i = 0; i < 3; i++) { // 结果为 0、1、2
console.log(i);
}

我们逐个部分分析 for 循环:

语句段
begin let i = 0 进入循环时执行一次。
condition i < 3 在每次循环迭代之前检查,如果为 false,停止循环。
body(循环体) console.log(i) 条件为真时,重复运行。
step i++ 在每次循环体迭代后执行。

一般循环算法的工作原理如下:

1
2
3
4
5
开始运行
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ ...

所以,begin 执行一次,然后进行迭代:每次检查 condition 后,执行 bodystep

如果你这是第一次接触循环,那么回到这个例子,在一张纸上重现它逐步运行的过程,可能会对你有所帮助。

以下是在这个示例中发生的事:

1
2
3
4
5
6
7
8
9
10
11
// for (let i = 0; i < 3; i++) console.log(i)

// 开始
let i = 0
// 如果条件为真,运行下一步
if (i < 3) { console.log(i); i++ }
// 如果条件为真,运行下一步
if (i < 3) { console.log(i); i++ }
// 如果条件为真,运行下一步
if (i < 3) { console.log(i); i++ }
// ……结束,因为现在 i == 3

内联变量声明

这里“计数”变量 i 是在循环中声明的。这叫做“内联”变量声明。这样的变量只在循环中可见。

1
2
3
4
for (let i = 0; i < 3; i++) {
console.log(i); // 0, 1, 2
}
console.log(i); // 错误,没有这个变量。

除了定义一个变量,我们也可以使用现有的变量:

1
2
3
4
5
6
7
let i = 0;

for (i = 0; i < 3; i++) { // 使用现有的变量
console.log(i); // 0, 1, 2
}

console.log(i); //3,可见,因为是在循环之外声明的

省略语句段

for 循环的任何语句段都可以被省略。

例如,如果我们在循环开始时不需要做任何事,我们就可以省略 begin 语句段。

就像这样:

1
2
3
4
5
let i = 0; // 我们已经声明了 i 并对它进行了赋值

for (; i < 3; i++) { // 不再需要 "begin" 语句段
console.log( i ); // 0, 1, 2
}

我们也可以移除 step 语句段:

1
2
3
4
5
let i = 0;

for (; i < 3;) {
console.log( i++ );
}

该循环与 while (i < 3) 等价。

实际上我们可以删除所有内容,从而创建一个无限循环:

1
2
3
for (;;) {
// 无限循环
}

请注意 for 的两个 ; 必须存在,否则会出现语法错误。
可以结合无限循环来等待 setTimeout 的异步执行,等到异步执行完成后,返回一个条件,让无限循环结束(即跳出循环)。

跳出循环

通常条件为假时,循环会终止。

但我们随时都可以使用 break 指令强制退出。

例如,下面这个循环要求用户输入一系列数字,在输入的内容不是数字时“终止”循环。

1
2
3
4
5
6
7
8
9
10
11
12
let sum = 0;

while (true) {

let value = +prompt("请输入数字", '');

if (!value) break; // (*)

sum += value;

}
console.log( 'Sum: ' + sum );

如果用户输入空行或取消输入,在 (*) 行的 break 指令会被激活。它立刻终止循环,将控制权传递给循环后的第一行,即,console.log

根据需要,”无限循环 + break“ 的组合非常适用于不必在循环开始/结束时检查条件,但需要在中间甚至是主体的多个位置进行条件检查的情况。

继续下一次迭代

continue 指令是 break 的“轻量版”。它不会停掉整个循环。而是停止当前这一次迭代,并强制启动新一轮循环(如果条件允许的话)。

如果我们完成了当前的迭代,并且希望继续执行下一次迭代,我们就可以使用它。

下面这个循环使用 continue 来只输出奇数:

1
2
3
4
5
6
7
for (let i = 0; i < 10; i++) {

//如果为真,跳过循环体的剩余部分。
if (i % 2 == 0) continue;

console.log(i); // 1,然后 3,5,7,9
}

对于偶数的 i 值,continue 指令会停止本次循环的继续执行,将控制权传递给下一次 for 循环的迭代(使用下一个数字)。因此 console.log 仅被奇数值调用。

continue 指令利于减少嵌套

显示奇数的循环可以像下面这样:

1
2
3
4
5
6
7
for (let i = 0; i < 10; i++) {

if (i % 2) {
console.log( i );
}

}

从技术角度看,它与上一个示例完全相同。当然,我们可以将代码包装在 if 块而不使用 continue

但在副作用方面,它多创建了一层嵌套(大括号内的 console.log 调用)。如果 if 中代码有多行,则可能会降低代码整体的可读性。

禁止 break/continue 在 ‘?’ 的右边

请注意非表达式的语法结构不能与三元运算符 ? 一起使用。特别是 break/continue 这样的指令是不允许这样使用的。

例如,我们使用如下代码:

1
2
3
4
5
if (i > 5) {
console.log(i);
} else {
continue;
}

……用问号重写:

1
(i > 5) ? console.log(i) : continue; // continue 不允许在这个位置

……代码会停止运行,并显示有语法错误。

这是不(建议)使用问号 ? 运算符替代 if 语句的另一个原因。

break/continue 标签

有时候我们需要一次从多层嵌套的循环中跳出来。

例如,下述代码中我们的循环使用了 ij,从 (0,0)(3,3) 提示坐标 (i, j)

1
2
3
4
5
6
7
8
9
10
11
for (let i = 0; i < 3; i++) {

for (let j = 0; j < 3; j++) {

let input = prompt(`Value at coords (${i},${j})`, '');

// 如果我想从这里退出并直接执行 console.log('Done!')
}
}

console.log('Done!');

我们需要提供一种方法,以在用户取消输入时来停止这个过程。

input 之后的普通 break 只会打破内部循环。这还不够 —— 标签可以实现这一功能!

标签 是在循环之前带有冒号的标识符:

1
2
3
labelName: for (...) {
...
}

break <labelName> 语句跳出循环至标签处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
outer: for (let i = 0; i < 3; i++) {

for (let j = 0; j < 3; j++) {

let input = prompt(`Value at coords (${i},${j})`, '');

// 如果是空字符串或被取消,则中断并跳出这两个循环。
if (!input) break outer; // (*)

// 用得到的值做些事……
}
}

console.log('Done!');

上述代码中,break outer 向上寻找名为 outer 的标签并跳出当前循环。

因此,控制权直接从 (*) 转至 console.log('Done!')

我们还可以将标签移至单独一行:

1
2
outer:
for (let i = 0; i < 3; i++) { ... }

continue 指令也可以与标签一起使用。在这种情况下,执行跳转到标记循环的下一次迭代。

标签并不允许“跳到”所有位置

标签不允许我们跳到代码的任意位置。

例如,这样做是不可能的:

1
2
3
break label;  // 跳转至下面的 label 处(无效)

label: for (...)

break 指令必须在代码块内。从技术上讲,任何被标记的代码块都有效,例如:

1
2
3
4
5
label: {
// ...
break 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
2
3
4
5
6
7
8
9
10
11
12
13
switch(x) {
case 'value1': // if (x === 'value1')
...
[break]

case 'value2': // if (x === 'value2')
...
[break]

default:
...
[break]
}
  • 比较 x 值与第一个 case(也就是 value1)是否严格相等,然后比较第二个 casevalue2)以此类推。
  • 如果相等,switch 语句就执行相应 case 下的代码块,直到遇到最靠近的 break 语句(或者直到 switch 语句末尾)。
  • 如果没有符合的 case,则执行 default 代码块(如果 default 存在)。

举个例子

switch 的例子(高亮的部分是执行的 case 部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let a = 2 + 2;

switch (a) {
case 3:
console.log( '太小啦!' );
break;
case 4:
console.log( '没错!正是我' );
break;
case 5:
console.log( '太大了' );
break;
default:
console.log( "额,什么鬼东西" );
}

这里的 switch 从第一个 case 分支开始将 a 的值与 case 后的值进行比较,第一个 case 后的值为 3 匹配失败。

然后比较 4。匹配,所以从 case 4 开始执行直到遇到最近的 break

如果没有 break,程序将不经过任何检查就会继续执行下一个 case

break 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
let a = 2 + 2;

switch (a) {
case 3:
console.log( '太小啦!' );
case 4:
console.log( '没错!正是我' );
case 5:
console.log( '太大了' );
default:
console.log( "额,什么鬼东西" );
}

在上面的例子中我们会看到连续执行的三个 console.log

1
2
3
console.log( '太小啦' );
console.log( '太大了' );
console.log( "额,什么鬼东西" );

任何表达式都可以成为 switch/case 的参数

switchcase 都允许任意表达式。

比如:

1
2
3
4
5
6
7
8
9
10
11
let a = "1";
let b = 0;

switch (+a) {
case b + 1:
console.log("这里会被运行, 因为 +a 是 1, 刚好等于 b+1");
break;

default:
console.log("这里将不会被运行");
}

这里 +a 返回 1,这个值跟 caseb + 1 相比较,然后执行对应的代码。

“case” 分组

共享同一段代码的几个 case 分支可以被分为一组:

比如,如果我们想让 case 3case 5 执行同样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let a = 3;

switch (a) {
case 4:
console.log('我是4!你看清楚a是几?');
break;

case 3: // (*) 下面这两个 case 被分在一组
case 5:
console.log('错误!');
console.log("Why don't you take a math class?");
break;

default:
console.log('The result is strange. Really.');
}

现在 35 都显示相同的信息。

switch/case 有通过 case 进行“分组”的能力,其实是 switch 语句没有 break 时的副作用。因为没有 breakcase 3 会从 (*) 行执行到 case 5

类型很关键

强调一下,这里的相等是严格相等。被比较的值必须是相同的类型才能进行匹配。

比如,我们来看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let arg = prompt("Enter a value?")
switch (arg) {
case '0':
case '1':
console.log( 'One or zero' );
break;

case '2':
console.log( 'Two' );
break;

case 3:
console.log( 'Never executes!' );
break;
default:
console.log( 'An unknown value' )
}
  1. prompt 对话框输入 01,第一个 console.log 弹出。
  2. 输入 2,第二个 console.log 弹出。
  3. 但是输入 3,因为 prompt 的结果是字符串类型的 "3",不严格相等 === 于数字类型的 3,所以 case 3 不会执行!因此 case 3 部分是一段无效代码。所以会执行 default 分支。

函数

我们经常需要在脚本的许多地方执行很相似的操作。

例如,当访问者登录、注销或者在其他地方时,我们需要显示一条好看的信息。

函数是程序的主要“构建模块”。函数使该段代码可以被调用很多次,而不需要写重复的代码。

我们已经看到了内建函数的示例,如 alert(message)prompt(message, default)confirm(question)。但我们也可以创建自己的函数。

函数声明

使用 函数声明 创建函数。

看起来就像这样:

1
2
3
function showMessage() {
console.log( 'Hello everyone!' );
}

function 关键字首先出现,然后是 函数名,然后是括号之间的 参数 列表(用逗号分隔,在上述示例中为空,我们将在接下来的示例中看到),最后是花括号之间的代码(即“函数体”)。

1
2
3
function name(parameter1, parameter2, ... parameterN) {
...body...
}

我们的新函数可以通过名称调用:showMessage()

例如:

1
2
3
4
5
6
function showMessage() {
console.log( 'Hello everyone!' );
}

showMessage();
showMessage();

调用 showMessage() 执行函数的代码。这里我们会看到显示两次消息。

这个例子清楚地演示了函数的主要目的之一:避免代码重复

如果我们需要更改消息或其显示方式,只需在一个地方修改代码:输出它的函数。

局部变量

在函数中声明的变量只在该函数内部可见。

例如:

1
2
3
4
5
6
7
8
9
function showMessage() {
let message = "Hello, I'm JavaScript!"; // 局部变量

console.log( message );
}

showMessage(); // Hello, I'm JavaScript!

console.log( message ); // <-- 错误!变量是函数的局部变量

外部变量

函数也可以访问外部变量,例如:

1
2
3
4
5
6
7
8
let userName = 'John';

function showMessage() {
let message = 'Hello, ' + userName;
console.log(message);
}

showMessage(); // Hello, John

函数对外部变量拥有全部的访问权限。函数也可以修改外部变量。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let userName = 'John';

function showMessage() {
userName = "Bob"; // (1) 改变外部变量

let message = 'Hello, ' + userName;
console.log(message);
}

console.log( userName ); // John 在函数调用之前

showMessage();

console.log( userName ); // Bob,值被函数修改了

只有在没有局部变量的情况下才会使用外部变量。

如果在函数内部声明了同名变量,那么函数会 遮蔽 外部变量。例如,在下面的代码中,函数使用局部的 userName,而外部变量被忽略:

1
2
3
4
5
6
7
8
9
10
11
12
13
let userName = 'John';

function showMessage() {
let userName = "Bob"; // 声明一个局部变量

let message = 'Hello, ' + userName; // Bob
console.log(message);
}

// 函数会创建并使用它自己的 userName
showMessage();

console.log( userName ); // John,未被更改,函数没有访问外部变量。

全局变量

任何函数之外声明的变量,例如上述代码中的外部变量 userName,都被称为 全局 变量。

全局变量在任意函数中都是可见的(除非被局部变量遮蔽)。

减少全局变量 的使用是一种很好的做法。现代的代码有很少甚至没有全局变量。大多数变量存在于它们的函数中。但是有时候,全局变量能够用于存储项目级别的数据。

参数

我们可以通过参数将任意数据传递给函数。

在如下示例中,函数有两个参数:from 和 text。

1
2
3
4
5
6
function showMessage(from, text) { // 参数:from 和 text
console.log(from + ': ' + text);
}

showMessage('阿祖', '你好!'); // 阿祖: 你好! (*)
showMessage('阿祖', "吃饭了没?"); // 阿祖: 吃饭了没? (**)

当函数在 (*) 和 (**) 行中被调用时,给定值被复制到了局部变量 fromtext。然后函数使用它们进行计算。

这里还有一个例子:我们有一个变量 from,并将它传递给函数。请注意:函数会修改 from,但在函数外部看不到更改,因为函数修改的是复制的变量值副本:

1
2
3
4
5
6
7
8
9
10
11
12
13
function showMessage(from, text) {

from = '*' + from + '*'; // 让 "from" 看起来更优雅

console.log( from + ': ' + text );
}

let from = "阿祖";

showMessage(from, "你好"); // *阿祖*: 你好

// "from" 值相同,函数修改了一个局部的副本。
console.log( from ); // 阿祖

当一个值被作为函数参数 (parameter) 传递时,它也被称为 参数 (argument)

换一种方式,我们把这些术语搞清楚:

函数参数(parameter)是函数声明中括号内列出的变量(它是函数声明时的术语)。
参数(argument)是调用函数时传递给函数的值(它是函数调用时的术语)。
我们声明函数时列出它们的函数参数 (parameters),然后调用它们传递参数 (arguments)

在上面的例子中,我们可以说:“函数 showMessage 被声明,并且带有两个参数 (parameters),随后它被调用,两个参数 (arguments) 分别为 from 和 “Hello””。

默认值

如果一个函数被调用,但有参数 (argument) 未被提供,那么相应的值就会变成 undefined

例如,之前提到的函数 showMessage(from, text) 可以只使用一个参数 (argument) 调用:

showMessage("Ann");
那不是错误,这样调用将输出 “Ann: undefined”。因为参数 text 的值未被传递,所以变成了 undefined

我们可以使用 = 为函数声明中的参数指定所谓的“默认”(如果对应参数的值未被传递则使用)值:

1
2
3
4
5
function showMessage(from, text = "这里是默认的第二项值") {
console.log( from + ": " + text );
}

showMessage("阿祖"); // 阿祖: 这里是默认的第二项值

现在因为 text 参数未被传递,它将会得到默认值 “这里是默认的第二项值”。

这里 “这里是默认的第二项值” 是一个字符串,但它可以是更复杂的表达式,并且只会在缺少参数时才会被计算和分配。所以,这也是可能的:

1
2
3
4
function showMessage(from, text = anotherFunction()) {
// anotherFunction() 仅在没有给定 text 时执行
// 其运行结果将成为 text 的值
}

默认参数的计算

JavaScript 中,每次函数在没带个别参数的情况下被调用,默认参数会被计算出来。

在上面的例子中,如果传递了参数 text,那么 anotherFunction() 就不会被调用。

如果没传递参数 text,那么 anotherFunction() 就会被调用。

JavaScript 老代码中的默认参数
几年前,JavaScript 不支持默认参数的语法。所以人们使用其他方式来设置默认参数。

如今,我们会在旧代码中看到它们。

例如,显式地检查 undefined

1
2
3
4
5
6
7
function showMessage(from, text) {
if (text === undefined) {
text = '未给第二项默认值';
}

console.log( from + ": " + text );
}

……或者使用 || 运算符:

1
2
3
4
5
6
function showMessage(from, text) {
// 如果 text 的值为假值,则分配默认值
// 这样赋值 text == "" 与 text 无值相同
text = text || '未给第二项默认值';
...
}

后备的默认参数
有些时候,将参数默认值的设置放在函数执行(相较更后期)而不是函数声明时,也行得通。

我们可以通过将参数与 undefined 进行比较,来检查该参数是否在函数执行期间被传递进来:

1
2
3
4
5
6
7
8
9
10
11
function showMessage(text) {
// ...

if (text === undefined) { // 如果参数未被传递进来
text = '是个空值';
}

console.log(text);
}

showMessage(); // 是个空值

……或者我们可以使用 || 运算符:

1
2
3
4
5
function showMessage(text) {
// 如果 text 为 undefined 或者为假值,那么将其赋值为 'empty'
text = text || 'empty';
...
}

现代 JavaScript 引擎支持 空值合并运算符 ??,它在大多数假值(例如 0)应该被视为“正常值”时更具优势:

1
2
3
4
5
6
7
8
function showCount(count) {
// 如果 count 为 undefined 或 null,则提示 "unknown"
console.log(count ?? "unknown");
}

showCount(0); // 0
showCount(null); // unknown
showCount(); // unknown

返回值

函数可以将一个值返回到调用代码中作为结果。

最简单的例子是将两个值相加的函数:

1
2
3
4
5
6
function sum(a, b) {
return a + b;
}

let result = sum(1, 2);
console.log( result ); // 3

指令 return 可以在函数的任意位置。当执行到达时,函数停止,并将值返回给调用代码(分配给上述代码中的 result)。

在一个函数中可能会出现很多次 return。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function checkAge(age) {
if (age >= 18) {
return true;
} else {
return confirm('Got a permission from the parents?');
}
}

let age = prompt('How old are you?', 18);

if ( checkAge(age) ) {
console.log( 'Access granted' );
} else {
console.log( 'Access denied' );
}

只使用 return 但没有返回值也是可行的。但这会导致函数立即退出。

例如:

1
2
3
4
5
6
7
8
function showMovie(age) {
if ( !checkAge(age) ) {
return;
}

console.log( "Showing you the movie" ); // (*)
// ...
}

在上述代码中,如果 checkAge(age) 返回 false,那么 showMovie 将不会运行到 console.log

空值的 return 或没有 return 的函数返回值为 undefined
如果函数无返回值,它就会像返回 undefined 一样:

1
2
3
function doNothing() { /* 没有代码 */ }

console.log( doNothing() === undefined ); // true

空值的 returnreturn undefined 等效:

1
2
3
4
5
function doNothing() {
return;
}

console.log( doNothing() === undefined ); // true

不要在 return 与返回值之间添加新行
对于 return 的长表达式,可能你会很想将其放在单独一行,如下所示:

1
2
return
(some + long + expression + or + whatever * f(a) + f(b))

但这不行,因为 JavaScript 默认会在 return 之后加上分号。上面这段代码和下面这段代码运行流程相同:

1
2
return;
(some + long + expression + or + whatever * f(a) + f(b))

因此,实际上它的返回值变成了空值。

如果我们想要将返回的表达式写成跨多行的形式,那么应该在 return 的同一行开始写此表达式。或者至少按照如下的方式放上左括号:

1
2
3
4
5
return (
some + long + expression
+ or +
whatever * f(a) + f(b)
)

然后它就能像我们预想的那样正常运行了。

函数命名

函数就是行为 (action)。所以它们的名字通常是动词。它应该简短且尽可能准确地描述函数的作用。这样读代码的人就能清楚地知道这个函数的功能。

一种普遍的做法是用动词前缀来开始一个函数,这个前缀模糊地描述了这个行为。团队内部必须就前缀的含义达成一致。

例如,以 "show" 开头的函数通常会显示某些内容。

函数以 XX 开始……

"get…" —— 返回一个值,
"calc…" —— 计算某些内容,
"create…" —— 创建某些内容,
"check…" —— 检查某些内容并返回 boolean 值,等。
这类名字的示例:

1
2
3
4
5
showMessage(..)     // 显示信息
getAge(..) // 返回 age(gets it somehow)
calcSum(..) // 计算求和并返回结果
createForm(..) // 创建表单(通常会返回它)
checkPermission(..) // 检查权限并返回 true/false

有了前缀,只需瞥一眼函数名,就可以了解它的功能是什么,返回什么样的值。

一个函数 —— 一个行为
一个函数应该只包含函数名所指定的功能,而不是做更多与函数名无关的功能。

两个独立的行为通常需要两个函数,即使它们通常被一起调用(在这种情况下,我们可以创建第三个函数来调用这两个函数)。

有几个违反这一规则的例子:

getAge —— 如果它通过 console.logage 显示出来,那就有问题了(只应该是获取)。
createForm —— 如果它包含修改文档的操作,例如向文档添加一个表单,那就有问题了(只应该创建表单并返回)。
checkPermission —— 如果它显示 access granted/denied 消息,那就有问题了(只应执行检查并返回结果)。
这些例子假设函数名前缀具有通用的含义。你和你的团队可以自定义这些函数名前缀的含义,但是通常都没有太大的不同。无论怎样,你都应该对函数名前缀的含义、带特定前缀的函数可以做什么以及不可以做什么有深刻的了解。所有相同前缀的函数都应该遵守相同的规则。并且,团队成员应该形成共识。

非常短的函数命名
常用的函数有时会有非常短的名字。

例如,jQuery 框架用 $ 定义一个函数。LoDash 库的核心函数用 _ 命名。

这些都是例外,一般而言,函数名应简明扼要且具有描述性。

函数 == 注释

函数应该简短且只有一个功能。如果这个函数功能复杂,那么把该函数拆分成几个小的函数是值得的。有时候遵循这个规则并不是那么容易,但这绝对是件好事。

一个单独的函数不仅更容易测试和调试 —— 它的存在本身就是一个很好的注释!

例如,比较如下两个函数 showPrimes(n)。它们的功能都是输出到 n 的 素数。

第一个变体使用了一个标签:

1
2
3
4
5
6
7
8
9
10
function showPrimes(n) {
nextPrime: for (let i = 2; i < n; i++) {

for (let j = 2; j < i; j++) {
if (i % j == 0) continue nextPrime;
}

console.log( i ); // 一个素数
}
}

第二个变体使用附加函数 isPrime(n) 来检验素数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function showPrimes(n) {

for (let i = 2; i < n; i++) {
if (!isPrime(i)) continue;

console.log(i); // 一个素数
}
}

function isPrime(n) {
for (let i = 2; i < n; i++) {
if ( n % i == 0) return false;
}
return true;
}

第二个变体更容易理解,不是吗?我们通过函数名 (isPrime) 就可以看出函数的行为,而不需要通过代码。人们通常把这样的代码称为 自描述

因此,即使我们不打算重用它们,也可以创建函数。函数可以让代码结构更清晰,可读性更强。

总结

函数声明方式如下所示:

1
2
3
function name(parameters, delimited, by, comma) {
/* code */
}
  • 作为参数传递给函数的值,会被复制到函数的局部变量。

  • 函数可以访问外部变量。但它只能从内到外起作用。函数外部的代码看不到函数内的局部变量。

  • 函数可以返回值。如果没有返回值,则其返回的结果是 undefined。

  • 为了使代码简洁易懂,建议在函数中主要使用局部变量和参数,而不是外部变量。

  • 与不获取参数但将修改外部变量作为副作用的函数相比,获取参数、使用参数并返回结果的函数更容易理解。

  • 函数命名:

  • 函数名应该清楚地描述函数的功能。当我们在代码中看到一个函数调用时,一个好的函数名能够让我们马上知道这个函数的功能是什么,会返回什么。

  • 一个函数是一个行为,所以函数名通常是动词。

  • 目前有许多优秀的函数名前缀,如 create…、show…、get…、check… 等等。使用它们来提示函数的作用吧。

函数是脚本的主要构建块。现在我们已经介绍了基本知识,现在我们就可以开始创建和使用函数了。但这只是学习和使用函数的开始。我们将继续学习更多函数的相关知识,更深入地研究它们的先进特征。


函数表达式

JavaScript 中,函数不是“神奇的语言结构”,而是一种特殊的值。

我们在前面章节使用的语法称为 函数声明:

1
2
3
function sayHi() {
console.log( "Hello" );
}

另一种创建函数的语法称为 函数表达式

它允许我们在任何表达式的中间创建一个新函数。

例如:

1
2
3
let sayHi = function() {
console.log( "Hello" );
};

在这里我们可以看到变量 sayHi 得到了一个值,新函数 function() { console.log("Hello"); }。

由于函数创建发生在赋值表达式的上下文中(在 = 的右侧),因此这是一个 函数表达式

请注意,在上述代码中 function 关键字后面没有函数名。函数表达式允许省略函数名。

这里我们立即将它赋值给变量,所以上面的两个代码示例的含义是一样的:
“创建一个函数并将其放入变量 sayHi 中”。

在更多更高阶的情况下,稍后我们会遇到,可以创建一个函数并立即调用,或者安排稍后执行,而不是存储在任何地方,因此保持匿名。

函数是一个值
重申一次:无论函数是如何创建的,函数都是一个值。上面的两个示例都在 sayHi 变量中存储了一个函数。

我们还可以用 console.log 显示这个变量的值:

1
2
3
4
5
function sayHi() {
console.log( "Hello" );
}

console.log( sayHi ); // 显示函数代码

注意,最后一行代码并不会运行函数,因为 sayHi 后没有括号。在某些编程语言中,只要提到函数的名称都会导致函数的调用执行,但 JavaScript 可不是这样。

JavaScript 中,函数是一个值,所以我们可以把它当成值对待。上面代码显示了一段字符串值,即函数的源码。

的确,在某种意义上说一个函数是一个特殊值,我们可以像 sayHi() 这样调用它。

但它依然是一个值,所以我们可以像使用其他类型的值一样使用它。

我们可以复制函数到其他变量:

1
2
3
4
5
6
7
8
function sayHi() {   // (1) 创建
console.log( "Hello" );
}

let func = sayHi; // (2) 复制

func(); // Hello // (3) 运行复制的值(正常运行)!
sayHi(); // Hello // 这里也能运行

解释一下上段代码发生的细节:

(1) 行声明创建了函数,并把它放入到变量 sayHi
(2) 行将 sayHi 复制到了变量 func。请注意:sayHi 后面没有括号。如果有括号,func = sayHi() 会把 sayHi() 的调用结果写进 func ,而不是 sayHi 函数 本身。
现在函数可以通过 sayHi()func() 两种方式进行调用。
我们也可以在第一行中使用函数表达式来声明 sayHi

1
2
3
4
5
6
let sayHi = function() { // (1) 创建
console.log( "Hello" );
};

let func = sayHi;
// ...

这两种声明的函数是一样的。

为什么这里末尾会有个分号?
你可能想知道,为什么函数表达式结尾有一个分号 ;,而函数声明没有:

1
2
3
4
5
6
7
function sayHi() {
// ...
}

let sayHi = function() {
// ...
};

答案很简单:这里函数表达式是在赋值语句 let sayHi = ...; 中以 function(…) {…} 的形式创建的。建议在语句末尾加上分号 ; ,它不是函数语法的一部分。

分号用于更简单的赋值,例如 let sayHi = 5;,它也用于函数赋值。

回调函数

让我们多举几个例子,看看如何将函数作为值来传递以及如何使用函数表达式。

我们写一个包含三个参数的函数 ask(question, yes, no)

question 关于问题的文本
yes 当回答为 “Yes” 时,要运行的脚本
no 当回答为 “No” 时,要运行的脚本
函数需要提出 question(问题),并根据用户的回答,调用 yes()no()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ask(question, yes, no) {
if (confirm(question)) yes()
else no();
}

function showOk() {
console.log( "You agreed." );
}

function showCancel() {
console.log( "You canceled the execution." );
}

// 用法:函数 showOk 和 showCancel 被作为参数传入到 ask
ask("Do you agree?", showOk, showCancel);

在实际开发中,这样的函数是非常有用的。实际开发与上述示例最大的区别是,实际开发中的函数会通过更加复杂的方式与用户进行交互,而不是通过简单的 confirm。在浏览器中,这样的函数通常会绘制一个漂亮的提问窗口。但这是另外一件事了。

ask 的两个参数值 showOkshowCancel 可以被称为 回调函数 或简称 回调

主要思想是我们传递一个函数,并期望在稍后必要时将其“回调”。在我们的例子中,showOk 是回答 “yes” 的回调,showCancel 是回答 “no” 的回调。

我们可以使用函数表达式来编写一个等价的、更简洁的函数:

1
2
3
4
5
6
7
8
9
10
function ask(question, yes, no) {
if (confirm(question)) yes()
else no();
}

ask(
"Do you agree?",
function() { console.log("You agreed."); },
function() { console.log("You canceled the execution."); }
);

这里直接在 ask(...) 调用内进行函数声明。这两个函数没有名字,所以叫 匿名函数。这样的函数在 ask 外无法访问(因为没有对它们分配变量),不过这正是我们想要的。

这样的代码在我们的脚本中非常常见,这正符合 JavaScript 语言的思想。

一个函数是表示一个“行为”的值。字符串或数字等常规值代表 数据

函数可以被视为一个 行为(action)

我们可以在变量之间传递它们,并在需要时运行。

函数表达式 vs 函数声明

让我们来总结一下函数声明和函数表达式之间的主要区别。

  • 首先是语法:如何通过代码对它们进行区分。

函数声明:在主代码流中声明为单独的语句的函数:

1
2
3
4
// 函数声明
function sum(a, b) {
return a + b;
}

函数表达式:在一个表达式中或另一个语法结构中创建的函数。下面这个函数是在赋值表达式 = 右侧创建的:

1
2
3
4
// 函数表达式
let sum = function(a, b) {
return a + b;
};
  • 更细微的差别是,JavaScript 引擎会在 什么时候 创建函数。

函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。

一旦代码执行到赋值表达式 let sum = function… 的右侧,此时就会开始创建该函数,并且可以从现在开始使用(分配,调用等)。

函数声明则不同。

在函数声明被定义之前,它就可以被调用。

例如,一个全局函数声明对整个脚本来说都是可见的,无论它被写在这个脚本的哪个位置。

这是内部算法的缘故。当 JavaScript 准备 运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数。我们可以将其视为“初始化阶段”。

在处理完所有函数声明后,代码才被执行。所以运行时能够使用这些函数。

例如下面的代码会正常工作:

1
2
3
4
5
sayHi("John"); // Hello, John

function sayHi(name) {
console.log( `Hello, ${name}` );
}

函数声明 sayHi 是在 JavaScript 准备运行脚本时被创建的,在这个脚本的任何位置都可见。

……如果它是一个函数表达式,它就不会工作:

1
2
3
4
5
sayHi("John"); // error!

let sayHi = function(name) { // (*) no magic any more
console.log( `Hello, ${name}` );
};

函数表达式在代码执行到它时才会被创建。只会发生在 (*) 行。为时已晚。

  • 函数声明的另外一个特殊的功能是它们的块级作用域。

严格模式下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。

例如,想象一下我们需要依赖于在代码运行过程中获得的变量 age 声明一个函数 welcome()。并且我们计划在之后的某个时间使用它。

如果我们使用函数声明,则以下代码无法像预期那样工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let age = prompt("What is your age?", 18);

// 有条件地声明一个函数
if (age < 18) {

function welcome() {
console.log("Hello!");
}

} else {

function welcome() {
console.log("Greetings!");
}

}

// ……稍后使用
welcome(); // Error: welcome is not defined

这是因为函数声明只在它所在的代码块中可见。

下面是另一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let age = 16; // 拿 16 作为例子

if (age < 18) {
welcome(); // \ (运行)
// |
function welcome() { // |
console.log("Hello!"); // | 函数声明在声明它的代码块内任意位置都可用
} // |
// |
welcome(); // / (运行)

} else {

function welcome() {
console.log("Greetings!");
}
}

// 在这里,我们在花括号外部调用函数,我们看不到它们内部的函数声明。


welcome(); // Error: welcome is not defined

我们怎么才能让 welcomeif 外可见呢?

正确的做法是使用函数表达式,并将 welcome 赋值给在 if 外声明的变量,并具有正确的可见性。

下面的代码可以如愿运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let age = prompt("What is your age?", 18);

let welcome;

if (age < 18) {

welcome = function() {
console.log("Hello!");
};

} else {

welcome = function() {
console.log("Greetings!");
};

}

welcome(); // 现在可以了

或者我们可以使用问号运算符 ? 来进一步对代码进行简化:

1
2
3
4
5
6
7
let age = prompt("What is your age?", 18);

let welcome = (age < 18) ?
function() { console.log("Hello!"); } :
function() { console.log("Greetings!"); };

welcome(); // 现在可以了
  • 什么时候选择函数声明与函数表达式?
    根据经验,当我们需要声明一个函数时,优先考虑函数声明语法。它能够为组织代码提供更多的灵活性。因为我们可以在声明这些函数之前调用这些函数。

这对代码可读性也更好,因为在代码中查找 function f(…) {…}let f = function(…) {…} 更容易。函数声明更“醒目”。

……但是,如果由于某种原因而导致函数声明不适合我们(我们刚刚看过上面的例子),那么应该使用函数表达式。

总结

  • 函数是值。它们可以在代码的任何地方被分配,复制或声明。

  • 如果函数在主代码流中被声明为单独的语句,则称为“函数声明”。

  • 如果该函数是作为表达式的一部分创建的,则称其“函数表达式”。

  • 在执行代码块之前,内部算法会先处理函数声明。所以函数声明在其被声明的代码块内的任何位置都是可见的。

  • 函数表达式在执行流程到达时创建。

  • 在大多数情况下,当我们需要声明一个函数时,最好使用函数声明,因为函数在被声明之前也是可见的。这使我们在代码组织方面更具灵活性,通常也会使得代码可读性更高。

  • 所以,仅当函数声明不适合对应的任务时,才应使用函数表达式。在本章中,我们已经看到了几个例子,以后还会看到更多的例子。


箭头函数,基础知识

创建函数还有另外一种非常简单的语法,并且这种方法通常比函数表达式更好。

它被称为 “箭头函数”,因为它看起来像这样:

let func = (arg1, arg2, ..., argN) => expression;
这里创建了一个函数 func,它接受参数 arg1..argN,然后使用参数对右侧的 expression 求值并返回其结果。

换句话说,它是下面这段代码的更短的版本:

1
2
3
let func = function(arg1, arg2, ..., argN) {
return expression;
};

让我们来看一个具体的例子:

1
2
3
4
5
6
7
8
9
10
let sum = (a, b) => a + b;

/* 这个箭头函数是下面这个函数的更短的版本:

let sum = function(a, b) {
return a + b;
};
*/

console.log( sum(1, 2) ); // 3

可以看到 (a, b) => a + b 表示一个函数接受两个名为 ab 的参数。在执行时,它将对表达式 a + b 求值,并返回计算结果。

如果我们只有一个参数,还可以省略掉参数外的圆括号,使代码更短。

例如:

1
2
3
4
let double = n => n * 2;
// 差不多等同于:let double = function(n) { return n * 2 }

console.log( double(3) ); // 6

如果没有参数,括号则是空的(但括号必须保留):

1
2
3
let sayHi = () => console.log("Hello!");

sayHi();

箭头函数可以像函数表达式一样使用。

例如,动态创建一个函数:

1
2
3
4
5
6
7
let age = prompt("What is your age?", 18);

let welcome = (age < 18) ?
() => console.log('Hello!') :
() => console.log("Greetings!");

welcome();

一开始,箭头函数可能看起来并不熟悉,也不容易读懂,但一旦我们看习惯了之后,这种情况很快就会改变。

箭头函数对于简单的单行行为 (action) 来说非常方便,尤其是当我们懒得打太多字的时候。

多行的箭头函数

到目前为止,我们看到的箭头函数非常简单。它们从 => 的左侧获取参数,计算并返回右侧表达式的计算结果。

有时我们需要更复杂一点的函数,比如带有多行的表达式或语句。在这种情况下,我们可以使用花括号将它们括起来。主要区别在于,用花括号括起来之后,需要包含 return 才能返回值(就像常规函数一样)。

就像这样:

1
2
3
4
5
6
let sum = (a, b) => {  // 花括号表示开始一个多行函数
let result = a + b;
return result; // 如果我们使用了花括号,那么我们需要一个显式的 “return”
};

console.log( sum(1, 2) ); // 3

更多内容

在这里,我们赞扬了箭头函数的简洁性。但还不止这些!

箭头函数还有其他有趣的特性。

为了更深入地学习它们,我们首先需要了解一些 JavaScript 其他方面的知识,因此我们将在后面的 深入理解箭头函数 一章中再继续研究箭头函数。

现在,我们已经可以用箭头函数进行单行行为和回调了。

总结

箭头函数对于简单的操作很方便,特别是对于单行的函数。它具体有两种形式:

  • 不带花括号:(...args) => expression —— 右侧是一个表达式:函数计算表达式并返回其结果。如果只有一个参数,则可以省略括号,例如 n => n*2

  • 带花括号:(...args) => { body } —— 花括号允许我们在函数中编写多个语句,但是我们需要显式地 return 来返回一些内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
mindmap
root("JavaScript学习笔记")
("JS 基础内容")
("关于 JavaScript 的一些基本常识")
("如何把代码运行的结果显示出来?")
("注释")
("现代模式/严格模式 ")
("三种特殊值")
("变量")
("数据类型")
("文本串类型")
("引号 (quotes)")
("特殊字符 / 转义字符")
("字符串长度 length")
("访问字符")
("字母大小写转换")
("在字符串查找是否包含另一个字符串")
("数字类型")
("科学计数法")
("数字进制转换")
("舍入")
("检测变量是不是数字 ")
("区别")
("注意事项")
("提取文本串中开头的数字")
("注意事项")
("数字函数")
("变量的数据类型转换")
("**转换成字符串**")
("**转换成数字**")
("**转换成布尔(boolean)类型**")
("总结")
("数学运算")
("术语:“一元运算符”,“二元运算符”,“运算元”")
("数学运算")
("取余 %")
("求幂 **")
("用二元运算符 + 连接字符串")
("数字转化,一元运算符 +")
("运算符优先级")
("赋值运算符")
("赋值 = 返回一个值")
("链式赋值(Chaining assignments)")
("原地修改")
("自增/自减")
("位运算符")
("逗号运算符")
("值的比较")
("比较的结果是 Boolean 类型")
("字符串比较")
("不同类型间的比较")
("严格相等")
("对 null 和 undefined 进行比较")
("奇怪的结果:null vs 0")
("特立独行的 undefined")
("避免问题")
("总结")
("条件分支:if 和 '?'")
("“if” 语句")
("布尔转换")
("“else” 语句")
("多个条件:“else if”")
("条件运算符 ‘?’")
("多个 ‘?’")
("‘?’ 的非常规使用")
("逻辑运算符")
("||(或)")
("或运算寻找第一个真值")
("&&(与)")
("与运算寻找第一个假值")
("!(非)")
("空值合并运算符 ' ?? '")
("与 || 比较")
("优先级")
("?? 与 && 或 || 一起使用")
("总结")
("循环:while 和 for")
("“while” 循环")
("“do…while” 循环")
("“for” 循环")
("省略语句段")
("跳出循环")
("继续下一次迭代")
("break/continue 标签")
("总结")
("switch 语句")
("语法")
("举个例子")
("“case” 分组")
("[类型很关键](https://zh.javascript.info/switch#lei-xing-hen-guan-jian)")
("函数")
("函数声明")
("局部变量")
("外部变量")
("全局变量")
("参数")
("默认值")
("默认参数的计算")
("返回值")
("函数命名")
("函数 == 注释")
("总结")
("函数表达式")
("回调函数")
("函数表达式 vs 函数声明")
("总结")
("箭头函数,基础知识")
("多行的箭头函数")
("更多内容")
("总结")