原来布尔操作符不止可以操作布尔值😮

最近在学习《JavaScript高级程序设计》,在阅读到中后部分时,经常的出现了很多如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 客户端能力检测
*/
if (client.browser.ie && client.browser.ie < 7) {
// do something
}

/**
* 跨浏览器事件对象处理
*/
{
getTarget: function (event) {
return event.target || event.srcElement;
}
}

从上面的代码中,我们不难发现,布尔操作符的操作对象并不一定是布尔值,这并不难理解,因为在《JavaScript高级程序设计》的最开始就介绍了 JavaScript 是变量类型松散的编程语言。

根据我们对与或逻辑的直觉,我们也不难判断上述两段代码中不二操作的的意图:我们知道不管是&& 还是 ||,他们都具有“短路逻辑” 的特性,即 && 的前一个操作数为false,|| 的前一个操作数为true时,我们就不会再去判断第二个操作数。也就是说,当 && 的前一个操作数为true, || 的前一个操作数为false时,表达式的返回值由第二个操作数决定。

由此,上面的客户端检测代码其实是想要在 client.browser.ie 存在的情况下再去判断其值,而后者则是表达了优先返回event.target,当其不存时,再返回 event.srcElement (处理IE兼容)。

当然以上都只是一些直觉性的判断,让我们回到红宝书的对应章节,来理性的梳理一下在使用布尔操作数操作非布尔值时,究竟会遵循什么样的规则。

逻辑非 !

当 ! 的操作数为布尔值时,自然没有争议的会对操作数取反。

当 ! 的操作数为非布尔值时,逻辑非会首先将它的操作数转换为布尔值,然后再对其取反。

在红宝书前置的内容中,我们了解到可以通过Boolean() 函数来对不同类型的变量进行布尔求值,那么既然逻辑非 ! 会先对其进行布尔求值再取反,我们便可以通过 !! 来代替Boolean()函数进行布尔求值。

红宝书中给出了 逻辑非 ! 操作对于不同操作数的操作结果,我觉得这样的梳理方式对我来说有一些别扭,这里对应给出双非操作 !! 针对不同类型的操作结果,即对于不同类型的布尔求值结果:

  • 操作数为对象引用:true
  • 操作数为空字符串:false
  • 操作数为非空字符串:true
  • 操作数为数值0:false
  • 操作数为非0数值:true
  • 操作数为NaN:false
  • 操作数为null:false
  • 操作数为undefined:false

可以看到这些规则总体都是符合直觉的。

用法

利用双非操作来进行布尔求值。

逻辑与 &&

当逻辑与 && 的操作数为非布尔值时,红宝书给出了如下的规则来计算操作结果:

我们可以看到,这样的规则描述略微显得有些混乱和难以理解记忆,并且这段描述中的后三点不知道是不是因为翻译的缘故,表述是错误的。

这里我先给出我对上述规则梳理验证后的结论:

  • 第一个操作数求值为false时,直接返回第一个操作数
  • 第一个操作数求值为true时,返回第二个操作数

这里说的求值,即在逻辑非部分所提到的布尔求值。

上述的两点规则已经可以涵盖红宝书中所给出的六点规则并且这两条规则同样适用于操作数直接为布尔值的情况。而且对于我自己来讲,逻辑更为通畅。

我们在反过来看刚刚我所说的红宝书的后三条规则存在错误:

  1. 如果有一个操作数为null,则返回null
  2. 如果有一个操作数为undefined,则返回undefined
  3. 如果有一个操作数为NaN,则返回NaN

不难发现这与我所总结规则的第一条是矛盾的,矛盾的情况为当逻辑与 && 的第一个操作数为布尔值false的情况,

下图为控制台验证结果:

由这里的控制台输出我们可以发现,结果依旧在上述两条规则的射程范围内,但是却与红宝书的描述相悖。

用法

利用逻辑与的上述规则(利用“短路求值”),我们或许可以总结这样较为简洁的两种用法:

1
2
3
4
5
6
7
// 寻找第一个假值
var res = candidate1 && candidate2 && candidate3;

// 防止属性访问错误
if (client.browser.ie && client.browser.ie < 7) {
// do something
}

逻辑或 ||

与逻辑与类似,红宝书依旧为逻辑或罗列了一系列针对非布尔值操作数的计算规则:

虽然这里的规则并没有像逻辑与中那样出现错误,不过依旧显得太过冗杂了。

类似逻辑与,我们可以将上述规则梳理为两条:

  • 第一个操作数求值为true时,直接返回起一个操作数
  • 第一个操作数求值为false时,返回第二个操作数

这里的两条规则同样适用于所有类型的操作数。

用法

利用上述特性,逻辑或可以进行这样的简洁操作

1
2
3
4
5
6
//寻找第一个真值,进行对象的优先选择:跨浏览器事件对象处理
{
getTarget: function (event) {
return event.target || event.srcElement;
}
}

总结

布尔求值(!! 或 Boolean())规则

  • 操作数为对象引用:true
  • 操作数为空字符串:false
  • 操作数为非空字符串:true
  • 操作数为数值0:false
  • 操作数为非0数值:true
  • 操作数为NaN:false
  • 操作数为null:false
  • 操作数为undefined:false

逻辑与规则 &&

  • 第一个操作数求值为false时,直接返回第一个操作数
  • 第一个操作数求值为true时,返回第二个操作数

应用:寻找第一个假值、防止属性访问错误(第一个操作数求值为真才会执行第二个操作数中的操作)

逻辑或规则 ||

  • 第一个操作数求值为true时,直接返回起一个操作数
  • 第一个操作数求值为false时,返回第二个操作数

应用:寻找第一个真值,进行对象的优先选择


其实,上述的这些复杂度和“便利”都是由JavaScript的松散变量类型的特性引入的,因为该特性,在JavaScript的编程中总是会存在这样或那样的隐式的类型转换,不光是布尔操作符的计算过程中会存在布尔求值这样的隐式类型转换,在数值运算操作符中也会存在隐式的向数值类型的类型转换。

写这篇博客的缘由和初衷或许有以下两点吧:布尔操作符的返回值相较于其他操作符来说更为复杂,因为他会按原有类型直接返回操作数,当然这在引入复杂度的同时也带来了便利;再者就是红宝书对于这段的描述多多少少有些复杂并且存在一些问题。

关于类型转换带来的问题,一直困惑我的相关问题还有JavaScript中 ==和=== 的使用,虽然知道一般来说使用===,不过对其背后发生了什么还是有些模糊,后续我也会打算针对这个内容写一篇相关博客明确以下认知。