JavaScript 基础知识|逻辑运算符|空值合并运算符
@[toc]
逻辑运算符
JavaScript 中有四个逻辑运算符:||
(或),&&
(与),!
(非),??
(空值合并运算符)。本文我们先介绍前三个,在下一篇文章中再详细介绍 ??
运算符。
虽然它们被称为“逻辑”运算符,但这些运算符却可以被应用于任意类型的值,而不仅仅是布尔值。它们的结果也同样可以是任意类型。
让我们来详细看一下。
||(或)
两个竖线符号表示“或”运算符:
result = a || b;
在传统的编程中,逻辑或仅能够操作布尔值。如果参与运算的任意一个参数为 true
,返回的结果就为 true
,否则返回 false
。
在 JavaScript 中,逻辑运算符更加灵活强大。但是,首先让我们看一下操作数是布尔值的时候发生了什么。
下面是四种可能的逻辑组合:
alert( true || true ); // true
alert( false || true ); // true
alert( true || false ); // true
alert( false || false ); // false
正如我们所见,除了两个操作数都是 false
的情况,结果都是 true
。
如果操作数不是布尔值,那么它将会被转化为布尔值来参与运算。
例如,数字 1
被作为 true
处理,数字 0
则被作为 false
:
if (1 || 0) { // 工作原理相当于 if( true || false )
alert( 'truthy!' );
}
大多数情况下,逻辑或 ||
会被用在 if
语句中,用来测试是否有 任何 给定的条件为 true
。
例如:
let hour = 9;
if (hour < 10 || hour > 18) {
alert( 'The office is closed.' );
}
我们可以传入更多的条件:
let hour = 12;
let isWeekend = true;
if (hour < 10 || hour > 18 || isWeekend) {
alert( 'The office is closed.' ); // 是周末
}
或运算寻找第一个真值
上文提到的逻辑处理多少有些传统了。下面让我们看看 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(都是假值,返回最后一个值)
与“纯粹的、传统的、仅仅处理布尔值的或运算”相比,这个规则就引起了一些很有趣的用法。
-
获取变量列表或者表达式中的第一个真值。
例如,我们有变量
firstName
、lastName
和nickName
,都是可选的(即可以是 undefined,也可以是假值)。我们用或运算
||
来选择有数据的那一个,并显示出来(如果没有设置,则用"Anonymous"
):let firstName = ""; let lastName = ""; let nickName = "SuperCoder"; alert( firstName || lastName || nickName || "Anonymous"); // SuperCoder
如果所有变量的值都为假,结果就是
"Anonymous"
。 -
短路求值(Short-circuit evaluation)。
或运算符
||
的另一个用途是所谓的“短路求值”。这指的是,
||
对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,那么这一特性的重要性就变得显而易见了。
在下面这个例子中,只会打印第二条信息:
true || alert("not printed"); false || alert("printed");
在第一行中,或运算符
||
在遇到true
时立即停止运算,所以alert
没有运行。有时,人们利用这个特性,只在左侧的条件为假时才执行命令。
&&(与)
两个 & 符号表示 &&
与运算符:
result = a && b;
在传统的编程中,当两个操作数都是真值时,与运算返回 true
,否则返回 false
:
alert( true && true ); // true
alert( false && true ); // false
alert( true && false ); // false
alert( false && false ); // false
带有 if
语句的示例:
let hour = 12;
let minute = 30;
if (hour == 12 && minute == 30) {
alert( 'Time is 12:30' );
}
就像或运算一样,与运算的操作数可以是任意类型的值:
if (1 && 0) { // 作为 true && false 来执行
alert( "won't work, because the result is falsy" );
}
与运算寻找第一个假值
给出多个参加与运算的值:
result = value1 && value2 && value3;
与运算 &&
做了如下的事:
- 从左到右依次计算操作数。
- 在处理每一个操作数时,都将其转化为布尔值。如果结果是
false
,就停止计算,并返回这个操作数的初始值。 - 如果所有的操作数都被计算过(例如都是真值),则返回最后一个操作数。
换句话说,与运算返回第一个假值,如果没有假值就返回最后一个值。
上面的规则和或运算很像。区别就是与运算返回第一个假值,而或运算返回第一个真值。
例如:
// 如果第一个操作数是真值,
// 与运算返回第二个操作数:
alert( 1 && 0 ); // 0
alert( 1 && 5 ); // 5
// 如果第一个操作数是假值,
// 与运算将直接返回它。第二个操作数会被忽略
alert( null && 5 ); // null
alert( 0 && "no matter what" ); // 0
我们也可以在一行代码上串联多个值。查看第一个假值是如何被返回的:
alert( 1 && 2 && null && 3 ); // null
如果所有的值都是真值,最后一个值将会被返回:
alert( 1 && 2 && 3 ); // 3,最后一个值
ℹ️与运算
&&
在或运算||
之前进行与运算
&&
的优先级比或运算||
要高。所以代码
a && b || c && d
跟&&
表达式加了括号完全一样:(a && b) || (c && d)
。
⚠️不要用 ||
或 &&
来取代 if
有时候,有人会将与运算符 &&
作为“简化 if
”的一种方式。
例如:
let x = 1;
(x > 0) && alert( 'Greater than zero!' );
&&
右边的代码只有运算抵达到那里才能被执行。也就是,当且仅当 (x > 0)
为真。
所以我们基本可以类似地得到:
let x = 1;
if (x > 0) alert( 'Greater than zero!' );
虽然使用 &&
写出的变体看起来更短,但 if
更明显,并且往往更具可读性。因此,我们建议根据每个语法结构的用途来使用:如果我们想要 if
,就使用 if
;如果我们想要逻辑与,就使用 &&
。
!(非)
感叹符号 !
表示布尔非运算符。
语法相当简单:
result = !value;
逻辑非运算符接受一个参数,并按如下运作:
- 将操作数转化为布尔类型:
true/false
。 - 返回相反的值。
例如:
alert( !true ); // false
alert( !0 ); // true
两个非运算 !!
有时候用来将某个值转化为布尔类型:
alert( !!"non-empty string" ); // true
alert( !!null ); // false
也就是,第一个非运算将该值转化为布尔类型并取反,第二个非运算再次取反。最后我们就得到了一个任意值到布尔值的转化。
有更多详细的方法可以完成同样的事 —— 一个内建的 Boolean
函数:
alert( Boolean("non-empty string") ); // true
alert( Boolean(null) ); // false
非运算符 !
的优先级在所有逻辑运算符里面最高,所以它总是在 &&
和 ||
之前执行。
✅任务
或运算的结果是什么?
重要程度: 5
如下代码将会输出什么?
alert( null || 2 || undefined );
解决方案
结果是
2
,这是第一个真值。alert( null || 2 || undefined );
或运算和 alerts 的结果是什么?
重要程度: 3
下面的代码将会输出什么?
alert( alert(1) || 2 || alert(3) );
解决方案
答案:首先是
1
,然后是2
。alert( alert(1) || 2 || alert(3) );
对
alert
的调用没有返回值。或者说返回的是undefined
。
- 第一个或运算
||
对它的左值alert(1)
进行了计算。这就显示了第一条信息1
。- 函数
alert
返回了undefined
,所以或运算继续检查第二个操作数以寻找真值。- 第二个操作数
2
是真值,所以执行就中断了。2
被返回,并且被外层的 alert 显示。这里不会显示
3
,因为运算没有抵达alert(3)
。
与操作的结果是什么?
重要程度: 5
下面这段代码将会显示什么?
alert( 1 && null && 2 );
解决方案
答案:
null
,因为它是列表中第一个假值。alert( 1 && null && 2 );
与运算连接的 alerts 的结果是什么?
重要程度: 3
这段代码将会显示什么?
alert( alert(1) && alert(2) );
解决方案
答案:
3
。alert( null || 2 && 3 || 4 );
与运算
&&
的优先级比||
高,所以它第一个被执行。结果是
2 && 3 = 3
,所以表达式变成了:null || 3 || 4
现在的结果就是第一个真值:
3
。
或运算、与运算、或运算串联的结果
重要程度: 5
结果将会是什么?
alert( null || 2 && 3 || 4 );
解决方案
检查值是否位于范围区间内
重要程度: 3
写一个 if
条件句来检查 age
是否位于 14
到 90
的闭区间。
“闭区间”意味着,age
的值可以取 14
或 90
。
解决方案
if (age >= 14 && age <= 90)
检查值是否位于范围之外
重要程度: 3
写一个 if
条件句,检查 age
是否不位于 14
到 90
的闭区间。
创建两个表达式:第一个用非运算 !
,第二个不用。
解决方案
第一个表达式:
if (!(age >= 14 && age <= 90))
第二个表达式:
if (age < 14 || age > 90)
一个关于 “if” 的问题
重要程度: 5
下面哪一个 alert
将会被执行?
if(...)
语句内表达式的结果是什么?
if (-1 || 0) alert( 'first' );
if (-1 && 0) alert( 'second' );
if (null || -1 && 1) alert( 'third' );
解决方案
答案:第一个和第三个将会被执行。
详解:
// 执行。 // -1 || 0 的结果为 -1,真值 if (-1 || 0) alert( 'first' ); // 不执行。 // -1 && 0 = 0,假值 if (-1 && 0) alert( 'second' ); // 执行 // && 运算的优先级比 || 高 // 所以 -1 && 1 先执行,给出如下运算链: // null || -1 && 1 -> null || 1 -> 1 if (null || -1 && 1) alert( 'third' );
登陆验证
重要程度: 3
实现使用 prompt
进行登陆校验的代码。
如果访问者输入 "Admin"
,那么使用 prompt
引导获取密码,如果输入的用户名为空或者按下了 Esc 键 —— 显示 “Canceled”,如果是其他字符串 —— 显示 “I don’t know you”。
密码的校验规则如下:
- 如果输入的是 “TheMaster”,显示 “Welcome!”,
- 其他字符串 —— 显示 “Wrong password”,
- 空字符串或取消了输入,显示 “Canceled.”。
流程图:
请使用嵌套的 if
块。注意代码整体的可读性。
提示:将空字符串输入,prompt 会获取到一个空字符串 ''
。Prompt 运行过程中,按下 ESC 键会得到 null
。
解决方案
let userName = prompt("Who's there?", ''); if (userName === 'Admin') { let pass = prompt('Password?', ''); if (pass === 'TheMaster') { alert( 'Welcome!' ); } else if (pass === '' || pass === null) { alert( 'Canceled' ); } else { alert( 'Wrong password' ); } } else if (userName === '' || userName === null) { alert( 'Canceled' ); } else { alert( "I don't know you" ); }
请注意
if
块中水平方向的缩进。技术上是非必需的,但会提升代码的可读性。
空值合并运算符 ‘??’
⚠️最近新增的特性
这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills.
空值合并运算符(nullish coalescing operator)的写法为两个问号 ??
。
由于它对待 null
和 undefined
的方式类似,所以在本文中我们将使用一个特殊的术语对其进行表示。为简洁起见,当一个值既不是 null
也不是 undefined
时,我们将其称为“已定义的(defined)”。
a ?? b
的结果是:
- 如果
a
是已定义的,则结果为a
, - 如果
a
不是已定义的,则结果为b
。
换句话说,如果第一个参数不是 null/undefined
,则 ??
返回第一个参数。否则,返回第二个参数。
空值合并运算符并不是什么全新的东西。它只是一种获得两者中的第一个“已定义的”值的不错的语法。
我们可以使用我们已知的运算符重写 result = a ?? b
,像这样:
result = (a !== null && a !== undefined) ? a : b;
现在你应该清楚了 ??
的作用。让我们来看看它的使用场景吧。
??
的常见使用场景是提供默认值。
例如,在这里,如果 user
的值不为 null/undefined
则显示 user
,否则显示 匿名
:
let user;
alert(user ?? "匿名"); // 匿名(user 未定义)
在下面这个例子中,我们将一个名字赋值给了 user
:
let user = "John";
alert(user ?? "匿名"); // John(user 已定义)
我们还可以使用 ??
序列从一系列的值中选择出第一个非 null/undefined
的值。
假设我们在变量 firstName
、lastName
或 nickName
中存储着一个用户的数据。如果用户决定不填写相应的值,则所有这些变量的值都可能是未定义的。
我们想使用这些变量之一显示用户名,如果这些变量的值都是 null/undefined
,则显示 “匿名”。
让我们使用 ??
运算符来实现这一需求:
let firstName = null;
let lastName = null;
let nickName = "Supercoder";
// 显示第一个已定义的值:
alert(firstName ?? lastName ?? nickName ?? "匿名"); // Supercoder
与 || 比较
或运算符 ||
可以以与 ??
运算符相同的方式使用。像我们在 【上一章】所讲的那样。
例如,在上面的代码中,我们可以用 ||
替换掉 ??
,也可以获得相同的结果:
let firstName = null;
let lastName = null;
let nickName = "Supercoder";
// 显示第一个真值:
alert(firstName || lastName || nickName || "Anonymous"); // Supercoder
纵观 JavaScript 发展史,或 ||
运算符先于 ??
出现。它自 JavaScript 诞生就存在了,因此开发者长期将其用于这种目的。
另一方面,空值合并运算符 ??
是最近才被添加到 JavaScript 中的,它的出现是因为人们对 ||
不太满意。
它们之间重要的区别是:
||
返回第一个 真 值。??
返回第一个 已定义的 值。
换句话说,||
无法区分 false
、0
、空字符串 ""
和 null/undefined
。它们都一样 —— 假值(falsy values)。如果其中任何一个是 ||
的第一个参数,那么我们将得到第二个参数作为结果。
不过在实际中,我们可能只想在变量的值为 null/undefined
时使用默认值。也就是说,当该值确实未知或未被设置时。
例如,考虑下面这种情况:
let height = 0;
alert(height || 100); // 100
alert(height ?? 100); // 0
height || 100
首先会检查height
是否为一个假值,它是0
,,确实是假值。- 所以,
||
运算的结果为第二个参数,100
。
- 所以,
height ?? 100
首先会检查height
是否为null/undefined
,发现它不是。- 所以,结果为
height
的原始值,0
。
- 所以,结果为
实际上,高度 0
通常是一个有效值,它不应该被替换为默认值。所以 ??
运算得到的是正确的结果。
优先级
??
运算符的优先级与 ||
相同,它们的的优先级都为 4
,详见:MDN。
这意味着,就像 ||
一样,空值合并运算符在 =
和 ?
运算前计算,但在大多数其他运算(例如 +
和 *
)之后计算。
所以我们可能需要在这样的表达式中添加括号:
let height = null;
let width = null;
// 重要:使用括号
let area = (height ?? 100) * (width ?? 50);
alert(area); // 5000
否则,如果我们省略了括号,则由于 *
的优先级比 ??
高,它会先执行,进而导致错误的结果。
// 没有括号
let area = height ?? 100 * width ?? 50;
// ……将这样计算(不符合我们的期望):
let area = height ?? (100 * width) ?? 50;
?? 与 && 或 || 一起使用
出于安全原因,JavaScript 禁止将 ??
运算符与 &&
和 ||
运算符一起使用,除非使用括号明确指定了优先级。
下面的代码会触发一个语法错误:
let x = 1 && 2 ?? 3; // Syntax error
这个限制无疑是值得商榷的,它被添加到语言规范中是为了避免人们从 ||
切换到 ??
时的编程错误。
可以明确地使用括号来解决这个问题:
let x = (1 && 2) ?? 3; // 正常工作了
alert(x); // 2
总结
-
空值合并运算符
??
提供了一种从列表中选择第一个“已定义的”值的简便方式。它被用于为变量分配默认值:
// 当 height 的值为 null 或 undefined 时,将 height 的值设置为 100 height = height ?? 100;
-
??
运算符的优先级非常低,仅略高于?
和=
,因此在表达式中使用它时请考虑添加括号。 -
如果没有明确添加括号,不能将其与
||
或&&
一起使用。
- 点赞
- 收藏
- 关注作者
评论(0)