你会用 “~”运算符吗???

252 阅读4分钟

一个常被人忽视的地方是 ~ 运算符(即字位操作“非”)相关的强制类型转换,它很让人费解,以至于了解它的开发人员也常常对其敬而远之。下面,我们在此深入探讨一下 ~ 有哪些用处。

我们知道字位运算符只适用于32位整数,运算符会强制操作数使用32位格式。这是通过抽象操作ToInt32来实现的。

ToInt32首先执行ToNumber强制类型转换,比如"123"会先被转换为123,然后再执ToInt32。虽然严格说来并非强制类型转换(因为返回值类型并没有发生变化),但字位运算符(如|和~)和某些特殊数字一起使用时会产生类似强制类型转换的效果,返回另外一个数字。

例如 | 运算符(字位操作“或”)的空操作(no-op)0 | x,它仅执行ToInt32转换:

0 | -0; // 0
0 | NaN; // 0
0 | Infinity; // 0
0 | -Infinity; // 0

以上这些特殊数字无法以32位格式呈现(因为它们来自64位IEEE 754标准),因此ToInt32返回0。

关于0  | ___是显式还是隐式仍存在争议。从规范的角度来说它无疑是显式的,但如果对字位运算符没有这样深入的理解,它可能就是隐式的。为了前后保持一致,我们这里将其视为显式。再回到~。它首先将值强制类型转换为32位数字,然后执行字位操作“非”(对每一个字位进行反转)。

这与 ! 很相像,不仅将值强制类型转换为布尔值,还对其做字位反转。

字位反转是个很晦涩的主题,我们一般很少需要关心到字位级别。对~还可以有另外一种诠释,源自早期的计算机科学和离散数学:~返回2的补码。这样一来问题就清楚多了!

~x大致等同于-(x+1)。很奇怪,但相对更容易说明问题:

~42;    // -(42+1) ==> -43

也许你还是没有完全弄明白~到底是什么玩意?稍安勿躁。

在-(x+1)中唯一能够得到0(或者严格说是-0)的x值是-1。也就是说如果x为-1时,~和一些数字值在一起会返回假值0,其他情况则返回真值。

然而这与我们讨论的内容有什么关系呢?

-1是一个“哨位值”,哨位值是那些在各个类型中(这里是数字)被赋予了特殊含义的值。在C语言中我们用-1来代表函数执行失败,用大于等于0的值来代表函数执行成功。

JavaScript中字符串的indexOf(..)方法也遵循这一惯例,该方法在字符串中搜索指定的子字符串,如果找到就返回子字符串所在的位置(从0开始),否则返回-1。

indexOf(..)不仅能够得到子字符串的位置,还可以用来检查字符串中是否包含指定的子字符串,相当于一个条件判断。例如:

var a = "Hello World";
if (a.indexOf( "lo" ) >= 0) {   // true 
    // 找到匹配!
} 
if (a.indexOf( "lo" ) != -1) {  // true   
    // 找到匹配!
} 
if (a.indexOf( "ol" ) < 0) {    // true     
    // 没有找到匹配!
} 
if (a.indexOf( "ol" ) == -1) {  // true     
    // 没有找到匹配!
}

>= 0和== -1这样的写法不是很好,称为“抽象渗漏”,意思是在代码中暴露了底层的实现细节,这里是指用-1作为失败时的返回值,这些细节应该被屏蔽掉。

现在我们终于明白~有什么用处了!~和indexOf()一起可以将结果强制类型转换(实际上仅仅是转换)为真/假值:

let a = "Hello World"; 

~a.indexOf("lo");            // -4   <-- 真值!

if (~a.indexOf( "lo" )) {    // true     
    // 找到匹配!
}

~a.indexOf( "ol" );          // 0    <-- 假值! 
!~a.indexOf( "ol" );         // true 

if (!~a.indexOf( "ol" )) {   // true
    // 没有找到匹配!
}

如果indexOf(..)返回-1,~将其转换为假值0,其他情况一律转换为真值。

由-(x+1)推断~-1的结果应该是-0,然而实际上结果是0,因为它是字位操作而非数学运算。

从技术角度来说,if (~a.indexOf(..))仍然是对indexOf(..)的返回结果进行隐式强制类型转换,0转换为false,其他情况转换为true。但我觉得 ~ 更像显式强制类型转换,前提是我对它有充分的理解。

个人认为~比>= 0和== -1更简洁。

另外介绍一点

有些小伙伴使用~~来截除数字值的小数部分,以为这和Math.floor(..)的效果一样,实际上并非如此。

~~中的第一个 ~ 执行ToInt32并反转字位,然后第二个 ~ 再进行一次字位反转,即将所有字位反转回原值,最后得到的仍然是ToInt32的结果。

对~~我们要多加注意。首先它只适用于32位数字,更重要的是它对负数的处理与Math.floor(..)不同。

Math.floor( -49.6 );    // -50
~~-49.6;                // -49

~~x能将值截除为一个32位整数,x | 0也可以,而且看起来还更简洁。

出于对运算符优先级的考虑,我们可能更倾向于使用~~x;

~~1E20 / 10;        // 166199296 
1E20 | 0 / 10;      // 1661992960
(1E20 | 0) / 10;    // 166199296

最后希望小伙伴们在使用~和~~进行此类转换时需要确保其他人也能够看得懂。