现代js教程摘录

199 阅读14分钟

本博客内容均摘录自现代js教程,连接如下

https://zh.javascript.info

本文知识摘录一些自己认为需要知道的重点知识,并不是完整的js学习文章,由于篇幅很长,所以分多次发布

JavaScript 基础知识

可以使用任何语言,包括西里尔字母(cyrillic letters)甚至是象形文字,就像这样:

let имя = '...';
let 我 = '...';

一般,我们需要在使用一个变量前定义它。但是在早期,我们可以不使用 let 进行变量声明,而可以简单地通过赋值来创建一个变量。现在如果我们不在脚本中使用 use strict 声明启用严格模式,这仍然可以正常工作,这是为了保持对旧脚本的兼容。

// 注意:这个例子中没有 "use strict"

num = 5; // 如果变量 "num" 不存在,就会被创建

alert(num); // 5

上面这是个糟糕的做法,严格模式下会报错。

"use strict";

num = 5; // 错误:num 未定义

JavaScript 中有八种基本的数据类型(译注:前七种为基本数据类型,也称为原始类型,而 object 为复杂数据类型)。

  • number 用于任何类型的数字:整数或浮点数,在 ±(253-1) 范围内的整数。
  • bigint 用于任意长度的整数。
  • string 用于字符串:一个字符串可以包含 0 个或多个字符,所以没有单独的单字符类型。
  • boolean 用于 truefalse
  • null 用于未知的值 —— 只有一个 null 值的独立类型。
  • undefined 用于未定义的值 —— 只有一个 undefined 值的独立类型。
  • symbol 用于唯一的标识符。
  • object 用于更复杂的数据结构。

我们可以通过 typeof 运算符查看存储在变量中的数据类型。

  • 两种形式:typeof x 或者 typeof(x)
  • 以字符串的形式返回类型名称,例如 "string"
  • typeof null 会返回 "object" —— 这是 JavaScript 编程语言的一个错误,实际上它并不是一个 object

typeof 运算符返回参数的类型。当我们想要分别处理不同类型值的时候,或者想快速进行数据类型检验时,非常有用。

它支持两种语法形式:

  1. 作为运算符:typeof x
  2. 函数形式:typeof(x)

换言之,有括号和没有括号,得到的结果是一样的。

typeof x 的调用会以字符串的形式返回数据类型:

typeof undefined // "undefined"

typeof 0 // "number"

typeof 10n // "bigint"

typeof true // "boolean"

typeof "foo" // "string"

typeof Symbol("id") // "symbol"

typeof Math // "object"  (1)

typeof null // "object"  (2)

typeof alert // "function"  (3)

在 JavaScript 中,“number” 类型无法表示大于 (253-1)(即 9007199254740991),或小于 -(253-1) 的整数。这是其内部表示形式导致的技术限制。

在大多数情况下,这个范围就足够了,但有时我们需要很大的数字,例如用于加密或微秒精度的时间戳。

BigInt 类型是最近被添加到 JavaScript 语言中的,用于表示任意长度的整数。

可以通过将 n 附加到整数字段的末尾来创建 BigInt 值。

// 尾部的 "n" 表示这是一个 BigInt 类型
const bigInt = 1234567890123456789012345678901234567890n;
  • alert

    显示信息。

  • prompt

    显示信息要求用户输入文本。点击确定返回文本,点击取消或按下 Esc 键返回 null

  • confirm

    显示信息等待用户点击确定或取消。点击确定返回 true,点击取消或按下 Esc 键返回 false

这些方法都是模态的:它们暂停脚本的执行,并且不允许用户与该页面的其余部分进行交互,直到窗口被解除。

上述所有方法共有两个限制:

  1. 模态窗口的确切位置由浏览器决定。通常在页面中心。
  2. 窗口的确切外观也取决于浏览器。我们不能修改它。

这就是简单的代价。还有其他一些方法可以显示更漂亮的窗口,并与用户进行更丰富的交互,但如果“花里胡哨”不是非常重要,那使用本节讲的这些方法也挺好。

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

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

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

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

let str = "123";
alert(typeof str); // string

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

alert(typeof num); // number

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

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

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

alert(age); // NaN,转换失败

number 类型转换规则:

变成……
undefinedNaN
null0
true 和 false1 and 0
string去掉首尾空格后的纯数字字符串中含有的数字。如果剩余字符串为空,则转换结果为 0。否则,将会从剩余字符串中“读取”数字。当类型转换出现 error 时返回 NaN

例子:

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

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

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

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

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

alert( Boolean("0") ); // true
alert( Boolean(" ") ); // 空白,也是 true(任何非空字符串都是 true)

除了常规的数字,还包括所谓的“特殊数值(“special numeric values”)”也属于这种类型:Infinity-InfinityNaN

alert( 1 / 0 ); // Infinity
alert( Infinity ); // Infinity

NaN 代表一个计算错误。它是一个不正确的或者一个未定义的数学操作所得到的结果,比如:

alert( "not a number" / 2 + 5 ); // NaN

有三种常用的类型转换:转换为 string 类型、转换为 number 类型和转换为 boolean 类型。

字符串转换 —— 转换发生在输出内容的时候,也可以通过 String(value) 进行显式转换。原始类型值的 string 类型转换通常是很明显的。

数字型转换 —— 转换发生在进行算术操作时,也可以通过 Number(value) 进行显式转换。

数字型转换遵循以下规则:

变成……
undefinedNaN
null0
true / false1 / 0
string“按原样读取”字符串,两端的空白会被忽略。空字符串变成 0。转换出错则输出 NaN

布尔型转换 —— 转换发生在进行逻辑操作时,也可以通过 Boolean(value) 进行显式转换。

布尔型转换遵循以下规则:

变成……
0, null, undefined, NaN, ""false
其他值true

上述的大多数规则都容易理解和记忆。人们通常会犯错误的值得注意的例子有以下几个:

  • undefined 进行数字型转换时,输出结果为 NaN,而非 0
  • "0" 和只有空格的字符串(比如:" ")进行布尔型转换时,输出结果为 true

在数学上,求幂的定义也适用于非整数。例如,平方根是以 1/2 为单位的求幂:

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

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

举个例子:

alert( '1' + 2 ); // "12"
alert( 2 + '1' ); // "21"

你看,第一个运算元和第二个运算元,哪个是字符串并不重要。

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

alert(2 + 2 + '1' ); // "41",不是 "221"

在这里,运算符是按顺序工作。第一个 + 将两个数字相加,所以返回 4,然后下一个 + 将字符串 1 加入其中,所以就是 4 + '1' = 41

二元 + 是唯一一个以这种方式支持字符串的运算符。其他算术运算符只对数字起作用,并且总是将其运算元转换为数字。

下面是减法和除法运算的示例:

alert( 6 - '2' ); // 4,将 '2' 转换为数字
alert( '6' / '2' ); // 3,将两个运算元都转换为数字

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

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

例如:

// 对数字无效
let x = 1;
alert( +x ); // 1

let y = -2;
alert( +y ); // -2

// 转化非数字
alert( +true ); // 1
alert( +"" );   // 0

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

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

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

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

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

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

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

// 在二元运算符加号起作用之前,所有的值都被转化为了数字
alert( +apples + +oranges ); // 5

// 更长的写法
// alert( Number(apples) + Number(oranges) ); // 5

从一个数学家的视角来看,大量的加号可能很奇怪。但是从一个程序员的视角,没什么好奇怪的:一元运算符加号首先起作用,它们将字符串转为数字,然后二元运算符加号对它们进行求和。

为什么一元运算符先于二元运算符作用于运算元?接下去我们将讨论到,这是由于它们拥有 更高的优先级

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

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

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

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

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

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

我们可以看到,“一元加号运算符”的优先级是 17,高于“二元加号运算符”的优先级 13。这也是为什么表达式 "+apples + +oranges" 中的一元加号先生效,然后才是二元加法。

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

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

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

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

语句 x = value 将值 value 写入 x 然后返回 x

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

let a = 1;
let b = 2;

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

alert( a ); // 3
alert( c ); // 0

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

有趣的代码,不是吗?我们应该了解它的工作原理,因为有时我们会在 JavaScript 库中看到它。

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

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

let a, b, c;

a = b = c = 2 + 2;

alert( a ); // 4
alert( b ); // 4
alert( c ); // 4

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

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

let n = 2;

n *= 3 + 5;

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

自增/自减只能应用于变量。试一下,将其应用于数值(比如 5++)则会报错。

自增/自减和其它运算符的对比

++/-- 运算符同样可以在表达式内部使用。它们的优先级比绝大部分的算数运算符要高。

举个例子:

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

与下方例子对比:

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

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

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

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

let counter = 1;
alert( 2 * counter );
counter++;

位运算符把运算元当做 32 位整数,并在它们的二进制表现形式上操作。

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

下面是位运算符:

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

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

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

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

举个例子:

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

alert( a ); // 7(3 + 4 的结果)

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

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

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

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

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

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

举个例子:

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

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

下面这些表达式的结果是什么?

"" + 1 + 0
"" - 1 + 0
true + false
6 / "3"
"2" * "3"
4 + 5 + "px"
"$" + 4 + 5
"4" - 2
"4px" - 2
7 / 0
"  -9  " + 5
"  -9  " - 5
null + 1
undefined + 1
" \t \n" - 2

好好思考一下,把它们写下来然后和答案比较一下。

解决方案

"" + 1 + 0 = "10" // (1)
"" - 1 + 0 = -1 // (2)
true + false = 1
6 / "3" = 2
"2" * "3" = 6
4 + 5 + "px" = "9px"
"$" + 4 + 5 = "$45"
"4" - 2 = 2
"4px" - 2 = NaN
7 / 0 = Infinity
"  -9  " + 5 = "  -9  5" // (3)
"  -9  " - 5 = -14 // (4)
null + 1 = 1 // (5)
undefined + 1 = NaN // (6)
" \t \n" - 2 = -2 // (7)
  1. 有字符串的加法 "" + 1,首先会将数字 1 转换为一个字符串:"" + 1 = "1",然后我们得到 "1" + 0,再次应用同样的规则得到最终的结果。
  2. 减法 -(像大多数数学运算一样)只能用于数字,它会使空字符串 "" 转换为 0
  3. 带字符串的加法会将数字 5 加到字符串之后。
  4. 减法始终将字符串转换为数字,因此它会使 " -9 " 转换为数字 -9(忽略了字符串首尾的空格)。
  5. null 经过数字转换之后会变为 0
  6. undefined 经过数字转换之后会变为 NaN
  7. 字符串转换为数字时,会忽略字符串的首尾处的空格字符。在这里,整个字符串由空格字符组成,包括 \t\n 以及它们之间的“常规”空格。因此,类似于空字符串,所以会变为 0

这里有一段代码,要求用户输入两个数字并显示它们的总和。

它的运行结果不正确。下面例子中的输出是 12(对于默认的 prompt 的值)。

为什么会这样?修正它。结果应该是 3

let a = prompt("First number?", 1);
let b = prompt("Second number?", 2);

alert(a + b); // 12

解决方案

原因是 prompt 以字符串的形式返回用户的输入。

所以变量的值分别为 "1""2"

let a = "1"; // prompt("First number?", 1);
let b = "2"; // prompt("Second number?", 2);

alert(a + b); // 12

我们应该做的是,在 + 之前将字符串转换为数字。例如,使用 Number() 或在 prompt 前加 +

例如,就在 prompt 之前加 +

let a = +prompt("First number?", 1);
let b = +prompt("Second number?", 2);

alert(a + b); // 3

或在 alert 中:

let a = prompt("First number?", 1);
let b = prompt("Second number?", 2);

alert(+a + +b); // 3

在最新的代码中,同时使用一元和二元的 +。看起来很有趣,不是吗?

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

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

例如:

alert( 'Z' > 'A' ); // true
alert( 'Glow' > 'Glee' ); // true
alert( '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)再判定大小。

例如:

alert( '2' > 1 ); // true,字符串 '2' 会被转化为数字 2
alert( '01' == 1 ); // true,字符串 '01' 会被转化为数字 1

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

例如:

alert( true == 1 ); // true
alert( false == 0 ); // true

一个有趣的现象

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

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

例如:

let a = 0;
alert( Boolean(a) ); // false

let b = "0";
alert( Boolean(b) ); // true

alert(a == b); // true!

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

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

alert( 0 == false ); // true

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

alert( '' == false ); // true

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

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

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

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

让我们试试:

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

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

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

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

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

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

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

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

  • 当使用数学式或其他比较方法 < > <= >= 时:

    null/undefined 会被转化为数字:null 被转化为 0undefined 被转化为 NaN

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

通过比较 null 和 0 可得:

alert( null > 0 );  // (1) false
alert( null == 0 ); // (2) false
alert( null >= 0 ); // (3) true

是的,上面的结果完全打破了你对数学的认识。在最后一行代码显示“null 大于等于 0”的情况下,前两行代码中一定会有一个是正确的,然而事实表明它们的结果都是 false。

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

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

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

alert( undefined > 0 ); // false (1)
alert( undefined < 0 ); // false (2)
alert( 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

以下表达式的执行结果是?

5 > 4
"apple" > "pineapple"
"2" > "12"
undefined == null
undefined === null
null == "\n0\n"
null === +"\n0\n"

解决方案

5 > 4true
"apple" > "pineapple"false
"2" > "12"true
undefined == nulltrue
undefined === nullfalse
null == "\n0\n"false
null === +"\n0\n"false

结果的原因:

  1. 数字间比较大小,显然得 true。
  2. 按词典顺序比较,得 false。"a""p" 小。
  3. 与第 2 题同理,首位字符 "2" 大于 "1"
  4. null 只与 undefined 互等。
  5. 严格相等模式下,类型不同得 false。
  6. 与第 4 题同理,null 只与 undefined 相等。
  7. 不同类型严格不相等。

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

例如:

let age = prompt('age?', 18);

let message = (age < 3) ? 'Hi, baby!' :
  (age < 18) ? 'Hello!' :
  (age < 100) ? 'Greetings!' :
  'What an unusual age!';

alert( message );

可能很难一下子看出发生了什么。但经过仔细观察,我们可以看到它只是一个普通的检查序列。

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

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

if (age < 3) {
  message = 'Hi, baby!';
} else if (age < 18) {
  message = 'Hello!';
} else if (age < 100) {
  message = 'Greetings!';
} else {
  message = 'What an unusual age!';
}

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

拓展的算法如下所示。

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

result = value1 || value2 || value3;

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

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

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

也就是,一个或运算 "||" 的链,将返回第一个真值,如果不存在真值,就返回该链的最后一个值。

例如:

alert( 1 || 0 ); // 1(1 是真值)

alert( null || 1 ); // 1(1 是第一个真值)
alert( null || 0 || 1 ); // 1(第一个真值)

alert( undefined || null || 0 ); // 0(所有的转化结果都是 false,返回最后一个值)

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

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

result = value1 && value2 && value3;

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

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

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

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

例如:

// 如果第一个运算符是真值,
// 与操作返回第二个操作数:
alert( 1 && 0 ); // 0
alert( 1 && 5 ); // 5

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

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

alert( !!"non-empty string" ); // true
alert( !!null ); // false

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

有更多详细的方法可以完成同样的事 —— 一个内置的 Boolean 函数:

alert( Boolean("non-empty string") ); // true
alert( Boolean(null) ); // false

非运算符 ! 的优先级在所有逻辑运算符里面最高,所以它总是在 &&|| 前执行。

下面的代码将会输出什么?

alert( alert(1) || 2 || alert(3) );

解决方案

答案:首先是 1,然后是 2

alert( alert(1) || 2 || alert(3) );

alert 的调用没有返回值。或者说返回的是 undefined

  1. 第一个或运算 || 对它的左值 alert(1) 进行了计算。这就显示了第一条信息 1
  2. 函数 alert 返回了 undefined,所以或运算继续检查第二个操作数以寻找真值。
  3. 第二个操作数 2 是真值,所以执行就中断了。2 被返回,并且被外层的 alert 显示。

这里不会显示 3,因为运算没有抵达 alert(3)

结果将会是什么?

alert( null || 2 && 3 || 4 );

解决方案

答案:3

alert( null || 2 && 3 || 4 );

与运算 && 的优先级比 || 高,所以它第一个被执行。

结果是 2 && 3 = 3,所以表达式变成了:

null || 3 || 4

现在的结果就是第一个真值:3

空值合并运算符 ?? 提供了一种简短的语法,用来获取列表中第一个“已定义”的变量(译注:即值不是 nullundefined 的变量)。

a ?? b 的结果是:

  • a,如果 a 不是 nullundefined
  • b,其他情况。

所以,x = a ?? b 是下面这个表达式的简写:

x = (a !== null && a !== undefined) ? a : b;

下面是一个更长一点的例子。

假设,我们有一个用户,变量 firstNamelastNamenickName 分别对应用户的名字、姓氏和昵称。如果用户决定不输入任何值,那么这些变量都可能是未定义的。

我们想要显示用户的名称:显示这三个变量中的一个,如果都没有设置值,则显示 “Anonymous”。

让我们使用 ?? 运算符选择第一个已定义的变量:

let firstName = null;
let lastName = null;
let nickName = "Supercoder";

// 显示第一个不是 null/undefined 的值
alert(firstName ?? lastName ?? nickName ?? "Anonymous"); // Supercoder

或运算符 || 可以与 ?? 运算符以同样的方式使用。正如 上一章 所讲的,我们可以用 || 替换上面示例中的 ??,也可以获得相同的结果。

重要的区别是:

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

当我们想将 null/undefined0 区别对待时,这个区别至关重要。

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

height = height ?? 100;

如果 height 未定义,则将其赋值为 100

让我们将其与 || 进行比较:

let height = 0;

alert(height || 100); // 100
alert(height ?? 100); // 0

在这个例子中,height || 100 将值为 0height 视为未设置的(unset),与 nullundefined以及任何其他假(falsy)值同等对待。因此得到的结果是 100

height ?? 100 仅当 height 确实是 nullundefined 时才返回 100。因此,alert 按原样显示了 height0

哪种行为更好取决于特定的使用场景。当高度 0 为有效值时,?? 运算符更适合。

出于安全原因,禁止将 ?? 运算符与 &&|| 运算符一起使用。

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

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

这个限制无疑是值得商榷的,但是它被添加到语言规范中是为了避免编程错误,因为人们开始使用 ?? 替代 ||

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

let x = (1 && 2) ?? 3; // 起作用

alert(x); // 2
  • 空值合并运算符 ?? 提供了一种简洁的方式获取列表中“已定义”的值。

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

    // 当 height 的值为 null 或 undefined 时,将 height 的值设置为 100
    height = height ?? 100;
    
  • ?? 运算符的优先级非常低,只略高于 ?=

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

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

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

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

labelName: for (...) {
  ...
}

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

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; // (*)

    // 用得到的值做些事……
  }
}
alert('Done!');

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

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

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

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

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

对于每次循环,写出你认为会显示的值,然后与答案进行比较。

以下两个循环的 alert 值是否相同?

  1. 前缀形式 ++i:

    let i = 0;
    while (++i < 5) alert( i );
    
  2. 后缀形式 i++

    let i = 0;
    while (i++ < 5) alert( i );
    

解决方案

这个题目展现了 i++/++i 两种形式在比较中导致的不同结果。

  1. 从 1 到 4

    let i = 0;
    while (++i < 5) alert( i );
    

    第一个值是 i = 1,因为 ++i 首先递增 i 然后返回新值。因此先比较 1 < 5 然后通过 alert 显示 1

    然后按照 2, 3, 4… —— 数值一个接着一个被显示出来。在比较中使用的都是递增后的值,因为 ++ 在变量前。

    最终,i = 4 时,在 ++i < 5 的比较中,i 值递增至 5,所以 while(5 < 5) 不符合循环条件,循环停止。所以没有显示 5

  2. 从 1 到 5

    let i = 0;
    while (i++ < 5) alert( i );
    

    第一个值也是 i = 1。后缀形式 i++ 递增 i 然后返回 值,因此比较 i++ < 5 将使用 i = 0(与 ++i < 5 不同)。

    alert 调用是独立的。这是在递增和比较之后执行的另一条语句。因此它得到了当前的 i = 1

    接下来是 2, 3,4…

    我们在 i = 4 时暂停,前缀形式 ++i 会递增 i 并在比较中使用新值 5。但我们这里是后缀形式 i++。因此,它将 i 递增到 5,但返回旧值。因此实际比较的是 while(4 < 5) —— true,程序继续执行 alert

    i = 5 是最后一个值,因为下一步比较 while(5 < 5) 为 false。

对于每次循环,写下它将显示的值。然后与答案进行比较。

两次循环 alert 值是否相同?

  1. 后缀形式:

    for (let i = 0; i < 5; i++) alert( i );
    
  2. 前缀形式:

    for (let i = 0; i < 5; ++i) alert( i );
    

解决方案

答案:在这两种情况下都是从 04

for (let i = 0; i < 5; ++i) alert( i );

for (let i = 0; i < 5; i++) alert( i );

这可以很容易地从 for 算法中推导出:

  1. 在一切开始之前执行 i = 0
  2. 检查 i < 5 条件
  3. 如果 true —— 执行循环体并 alert(i),然后进行 i++

递增 i++ 与检查条件(2)分开。这只是另一种写法。

在这没使用返回的递增值,因此 i++++i之间没有区别。

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

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

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

break 的例子:

let a = 2 + 2;

switch (a) {
  case 3:
    alert( 'Too small' );
  case 4:
    alert( 'Exactly!' );
  case 5:
    alert( 'Too big' );
  default:
    alert( "I don't know such values" );
}

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

alert( 'Exactly!' );
alert( 'Too big' );
alert( "I don't know such values" );

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

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

let a = 3;

switch (a) {
  case 4:
    alert('Right!');
    break;

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

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

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

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

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

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

let arg = prompt("Enter a value?")
switch (arg) {
  case '0':
  case '1':
    alert( 'One or zero' );
    break;

  case '2':
    alert( 'Two' );
    break;

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

空值的 return 或没有 return 的函数返回值为 undefined

如果函数无返回值,它就会像返回 undefined 一样:

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

alert( doNothing() === undefined ); // true

空值的 returnreturn undefined 等效:

function doNothing() {
  return;
}

alert( doNothing() === undefined ); // true

不要在 return 与返回值之间添加新行

对于 return 的长表达式,可能你会很想将其放在单独一行,如下所示:

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

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

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

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

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

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

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

我们还可以用 alert 打印这个变量值:

function sayHi() {
  alert( "Hello" );
}

alert( sayHi ); // 显示函数代码

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

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

function sayHi() {
  // ...
}

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

答案很简单:

  • 在代码块的结尾不需要加分号 ;,像 if { ... }for { }function f { } 等语法结构后面都不用加。
  • 函数表达式是在语句内部的:let sayHi = ...;,作为一个值。它不是代码块而是一个赋值语句。不管值是什么,都建议在语句末尾添加分号 ;。所以这里的分号与函数表达式本身没有任何关系,它只是用于终止语句。

在函数声明被定义之前,它就可以被调用。

例如,一个全局函数声明对整个脚本来说都是可见的,无论它被写在这个脚本的哪个位置。

这是内部算法的原故。当 JavaScript 准备 运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数。我们可以将其视为“初始化阶段”。

在处理完所有函数声明后,代码才被执行。所以运行时能够使用这些函数。

例如下面的代码会正常工作:

sayHi("John"); // Hello, John

function sayHi(name) {
  alert( `Hello, ${name}` );
}

函数声明 sayHi 是在 JavaScript 准备运行脚本时被创建的,在这个脚本的任何位置都可见。

……如果它是一个函数表达式,它就不会工作:

sayHi("John"); // error!

let sayHi = function(name) {  // (*) no magic any more
  alert( `Hello, ${name}` );
};

函数表达式在代码执行到它时才会被创建。只会发生在 (*) 行。为时已晚。

函数声明的另外一个特殊的功能是它们的块级作用域。