参考地址: 这么骚的 js 代码,不怕被揍么

在开始之前,我们需要先看这样一段话

按照 ECMAScript 标准,两个需要运算的值会被先转为有符号的 32 位整型,所以超过 32 位的整数会被截断,而小数部分则会被直接舍弃。

1. 使用左移运算符 << 迅速得出 2 的次方


左移运算,是将每一位的数字都向左移动一位。我们以数字 10 为例

javascript位运算- js中的位运算_异或运算

当我们对数字 10 进行左移的时候,它会先被转化为 32 位的二进制数。最高一位为符号位(正数为 0,负数为 1)。

然后进行左移,符号位保持不变,其余位依次向左移动一位,右侧以 0 填充。

然后化为十进制就是所得结果,为 20 。我们来验证一下

// 数字 10 左移 1 位
console.log(10 << 1);

javascript位运算- js中的位运算_javascript位运算-_02

十进制数左移一位,就扩大了 10 倍。二进制左移一位则扩大 2 倍。

所以,我们可以通过左移,快速获得 2 的 n 次方。

// 1 * (2 的 10 次方),也就是 2 的 10 次方
console.log(1 << 10);
// 获得 2 的 24 次方
console.log(1 << 24);

javascript位运算- js中的位运算_javascript_03

2. 使用异或运算符 ^ 切换变量 0 或 1


在数学中,^ 代表幂运算,但在位运算中,它代表异或运算。

我们用 10 和 12 来演示异或运算。

javascript位运算- js中的位运算_前端_04

异或运算规则为:相同为 0 ,不同为 1 。

第一行为数字 10 转化的二进制,第二行为数字 12 转化的二进制,红色框中符号位。

进行异或运算时,紫色框外 (含符号位) 的位全部相同 ,所以异或后这些位得到 0 。

紫色框内的位上不同,它们异或得到一个 1 。

因此,第三行为数字 10 与数字 12 异或运算后的二进制。值为 6 。我们来验证一下

console.log(10 ^ 12);

javascript位运算- js中的位运算_数组_05

如果我们需要重复改变为 0 和 1 ,就不再需要用 if 条件语句或者三目运算符了。

let a = 0;
a ^= 1;
console.log(a);
a ^= 1;
console.log(a);
a ^= 1;
console.log(a);

javascript位运算- js中的位运算_前端_06

3. 使用按位与运算符 & 判断数值奇偶性


类似 if 语句中的逻辑与 && ,&& 前后都为 true 时,返回 true 。

按位与 & 只有前后都为 1 时,才返回 1 ,否则得到 0 。

我们还是以 10 和 12 来演示


javascript位运算- js中的位运算_javascript位运算-_07

在 10 和 12 转化成的二进制中,只有紫色框中这一位同为 1 ,其他位均包含 0 。

所以按位与之后,其他位 (包括符号位) 均得到 0 。最终结果为第三个二进制串,它代表十进制的 8 。验证如下

console.log(10 & 12);

javascript位运算- js中的位运算_前端_08

在二进制中,偶数的最后一位一定为 0 ,奇数的最后一位一定为 1 。

我们可以判断二进制最后一位,来得知这个数值的奇偶性。

console.log(7 & 1);
console.log(8 & 1);
console.log(22 & 1);
console.log(51 & 1);

javascript位运算- js中的位运算_前端_09

4. 使用双逻辑非 !! 快捷转为布尔值


逻辑非 ! 在条件判断中十分常见,它能够将布尔类型的两个值互相转换。

另外,建议先阅览 JavaScript 中的隐式类型转换

我们通过两次逻辑非运算,可以快速将某个值转化为对应的布尔值。

console.log(!!123);
console.log(!!"hello");
console.log(!!{ name: "李雷" });
console.log(!!["韩梅梅"]);
console.log(!!0);
console.log(!!"");
console.log(!!NaN);

结果如下

javascript位运算- js中的位运算_javascript位运算-_10

5. 按位取反 ~、右移 >>、左移 <<、无符号右移 >>>、按位或 | 去掉小数


如本文开头所述,对一个值进行位运算,它的小数部分会被舍弃。

我们可以利用它来快速去除某个值的小数部分,这等价于 Math.floor() 。

console.log(~~11.71);
console.log(11.71 >> 0);
console.log(11.71 << 0);
console.log(11.71 | 0);
console.log(11.71 >>> 0);

javascript位运算- js中的位运算_javascript_11

负数的右移操作需要格外注意,因为负数在内存中是以补码形式存在的。我们以 -12 来演示


javascript位运算- js中的位运算_数组_12

上图为转补码的过程,先将数值转化为 32 位有符号的整数(第一行)。

然后将这个二进制除符号位按位取反,得到该数字的反码(第二行)。

然后对这个反码 +1,就得到了该数值的补码。

对负数进行位操作时,都会先转化为这个负数的补码,再进行位运算,而并不像正数一样使用原码。

当位运算结束后,会再次执行求补码的过程(得到原码),得到的数字才是所得到的结果。

注意无符号右移 >>> 会忽略数字的正负号,将符号位做为普通位进行运算。这将导致符号位丢失。

正数进行无符号右移,由于左边始终以 0 补位,所以没有影响(正数符号位是 0)。

当负数进行无符号右移时,符号位被作为普通位向右移,左边被 0 补位。如下图所示

此时,得到这是一个无符号的数,所有位都是普通位。所以会直接转化为十进制得到值。


javascript位运算- js中的位运算_前端_13

所以 -12 无符号右移一位应该得到如下结果


javascript位运算- js中的位运算_数组_14

代码验证如下

console.log(-12 >>> 1);

javascript位运算- js中的位运算_javascript_15

不妨试一试右移 0 位。

console.log(-12 >>> 0);

javascript位运算- js中的位运算_前端_16

再次分析


javascript位运算- js中的位运算_javascript位运算-_17

首先,还是将负数转化为补码。然后,将符号位化为普通位,形成一个没有符号的 32 位二进制串。

然后向右移动 0 位。最后,由于这个二进制串已经没有符号位了,得到结果。验证一下


javascript位运算- js中的位运算_javascript_18

因此,我们应该避免使用无符号右移来操作负数。

6. 使用异或 ^ 来完成值交换


我们再次观察一下异或操作 ^ ,方便接下来的理解。

javascript位运算- js中的位运算_前端_19

除了 “相同为 0 ,不同为 1 ”的规律 ,你是否还能发现其他规律呢?就像 a ^ b = b ^ a 。

javascript位运算- js中的位运算_前端_20

当左边的数为 0 时,其结果就和右边的数相等,当左边的数为 1 时,其结果就和右边的数相反。

下面通过异或 ^ 实现交换值。

let a = 10;
let b = 12;

a ^= b;
b ^= a;
a ^= b;

console.log("a = " + a);
console.log("b = " + b);

javascript位运算- js中的位运算_javascript位运算-_21

第一次异或运算如下图所示


javascript位运算- js中的位运算_javascript_22

先将 a 和 b 化为二进制串。通过第一次异或运算,得到的结果是标记了 a 和 b 在哪些位不一样。

相同的位得到 0 ,不同的位得到 1 。然后将这个标记用 a 保存了下来。

第二次异或如下图所示


javascript位运算- js中的位运算_javascript_23

根据规律,只要后者为 1 结果就与左边的相反。而此时 a 就是记录了 b 哪些位数与最开始的 a 不一致。

取反之后恰好就得到了最开始的 a 的值。实现了将最开始 a 的值赋值给了现在的 b 。

第三次异或如下图所示


javascript位运算- js中的位运算_javascript_24

此时的 b 变量已经得到最开始的 a 变量的值,a 变量依然是记录的差异值,和第二次异或的原理相同,异或操作将按照差异值进行取反。

此时的 b 变量按照差异值取反等价于最初的 a 变量按照差异值取反,就可以得到最初的 b 变量。

至此,a 与 b 成功完成值的交换,且没有申请多余的空间。

7. 使用异或 ^ 判断符号是否相同


只要两个数值同为正数或者同为负数,他们的符号位进行异或操作之后一定会得到一个 0 。

如果两个数值为一正一负,它们的符号位异或之后一定会得到一个 1 。

因此只要判断异或后的数值是否大于或等于 0 。就可以得知两个数值是否同号。

画图来帮助理解


javascript位运算- js中的位运算_异或运算_25

如果异或后得到一个正数,则表示两个数值同号。同理,只要得到一个负数,则代表两个数值异号。

let a = 100;
let b = 299;
(a ^ b) >= 0 ? console.log("符号相同") : console.log("符号相反");

javascript位运算- js中的位运算_数组_26

再试一次

let a = -17;
let b = 50;
(a ^ b) >= 0 ? console.log("符号相同") : console.log("符号相反");

javascript位运算- js中的位运算_javascript位运算-_27

8. 使用异或 ^ 来检查数值是否不相等


只要两个数值相等,他们转化得到的二进制串一定相同。如果两个数值不相等,转化得到的二进制串一定不相同。

完全相同的二进制串进行异或,将得到 0 ,不相同的二进制串,异或得到非 0 。

let a = -17;
let b = 50;
a ^ b ? console.log("a != b") : console.log("a == b");

javascript位运算- js中的位运算_javascript位运算-_28

再试一次

let a = 24;
let b = 24;
a ^ b ? console.log("a != b") : console.log("a == b");

javascript位运算- js中的位运算_前端_29

9. 通过 n&(n-1),判断 n 是 否为 2 的整数幂


首先,我们需要明白,到底怎样才算 2 的整数幂。


javascript位运算- js中的位运算_异或运算_30

如果 n 是 2 的整数次幂,那么 n-1 不难知道。


javascript位运算- js中的位运算_javascript_31

将这两个二进制串进行按位与 & ,只能得到 0 。

我们来反证一下。如果数值不是 2 的整数次幂。那么它的二进制一定有两个或两个以上的位是 1 。

对这个二进制串进行减一的时候,只能将最后一个 1 及其后的位数进行变更,前面为 1 的位保持不变,这将导致 n&(n-1) 必定会得到一个非 0 的值,如下图。


javascript位运算- js中的位运算_前端_32

代码如下

function check(num) {
	if (num & (num - 1)) {
		console.log(`${num} 不是 2 的整数次幂!`);
	} else {
		console.log(`${num} 是 2 的整数次幂。`);
	}
}

check(64);
check(500);
check(1024);

javascript位运算- js中的位运算_前端_33

10. 使用 A+0.5|0 来替代 Math.round()


如果数值的小数部分小于 0.5 ,那么加上 0.5 之后,整数部分不改变。如果数值的小数部分大于等于 0.5 ,那么加上 0.5 后会进位,整数部分会加一。

如果此时我们只保留整数部分,就实现了四舍五入的运算。

console.log((1.3 + 0.5) | 0);
console.log((5.8 + 0.5) | 0);

javascript位运算- js中的位运算_javascript位运算-_34

去掉小数部分的方式有很多,上文有不少方法。注意负数需要 -0.5 而不是 +0.5 。

11. 使用 .link() 创建链接元素


创建链接标签最常见的就是使用 document.createElement 的方式。还有简单的模板字符串。

使用 .link() 创建的方法并不常见。

let a = document.createElement("a");
a.href = "www.baidu.com";
a.innerText = "百度一下";

let b = `<a herf="www.baidu.com">百度一下</a>`;

let c = "百度一下".link(`www.baidu.com`);

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

javascript位运算- js中的位运算_数组_35

12. 一些可以替代 undefined 的操作


console.log(""._);
console.log((1)._);
console.log((0)[0]);
console.log(void 0);

javascript位运算- js中的位运算_异或运算_36

前三式的原理相同,访问对象不存在的属性得到 undefined 。

对基本数据类型进行对象的相关操作时(比如访问属性),javascript 会将其临时包装为一个对象,一旦操作结束,这个临时的对象会被立即销毁。

临时的对象没有 0 、_ 这些属性,访问就会得到 undefined 。

void 是 JavaScript 的一元操作符,它可以出现在任意类型的操作数之前,将忽略操作数的返回值,直接返回一个 undefined 。

13. 使用 Array.length=0 来清空数组


数组都有 length 来属性表示数组的长度。

当我们通过修改的方式增大数组的 length 的值时,会在数组的尾部添加空项 empty 形成稀疏数组。

而当我们通过修改的方式减小数组的 length 的值的时候,数组会从尾部丢弃元素,至满足长度为止。

let arrA = [1, 2, 3, 4, 5, 6]; // length = 6
let arrB = [6, 5, 4, 3, 2, 1]; // length = 6
arrA.length = 8;
arrB.length = 3;
console.log(arrA);
console.log(arrB);

javascript位运算- js中的位运算_javascript_37

我们可以将数组的 length 属性修改为 0 ,使数组丢弃所有的项实现清空数组。

let arrC = [1, 3, 5, 7, 9];
arrC.length = 0;
console.log(arrC);

javascript位运算- js中的位运算_数组_38

14. for 循环条件的简写


我们平时使用 for 循环都是完整的个结构,for 循环的简写并不常见。

我们可以在 w3school 中看到,for 循环的三个语句都是可以省略的。去 w3school 查看 for 循环

同时省略三个语句不会出现任何语法错误,并且可以成功执行代码。但是,请不要运行这样的代码

for (;;) {
}

语句一省略的前提是,for 循环不需要用来控制循环终止的变量,或者已经拥有能够控制循环终止的变量。

let i = 5;
for (; i > 0; i--) {
	console.log(i);
}

javascript位运算- js中的位运算_javascript位运算-_39

语句二省略前提是,for 循环必须拥有触发循环终止的条件。

for (let i = 5; ; i--) {
	if (i <= 0) {
		break;
	}
	console.log(i);
}

javascript位运算- js中的位运算_前端_40

语句三省略的前提是,for 循环不需要对控制循环终止的变量进行操作,或者已经存在对控制循环终止的变量的操作了。

for (let i = 5; i > 0; ) {
	console.log(i);
	i--;
}

javascript位运算- js中的位运算_前端_41

我们来看一个这样的代码

let i = 5;
for (; i--; ) {
	console.log(i);
}

javascript位运算- js中的位运算_前端_42

上面代码将语句三和语句二融为一体,每一次循环时,i 作为判断条件,被隐式转化为布尔值。

判断结束后,就进行自减操作,i-- 既完成了语句二终止条件的功能,也完成了语句三改变控制循环终止变量的功能。

当且仅当 i 为虚值(falsy值)时,判断得到 false ,循环终止。