参考地址: 这么骚的 js 代码,不怕被揍么
在开始之前,我们需要先看这样一段话
按照 ECMAScript 标准,两个需要运算的值会被先转为有符号的 32 位整型,所以超过 32 位的整数会被截断,而小数部分则会被直接舍弃。
1. 使用左移运算符 << 迅速得出 2 的次方
左移运算,是将每一位的数字都向左移动一位。我们以数字 10 为例
当我们对数字 10 进行左移的时候,它会先被转化为 32 位的二进制数。最高一位为符号位(正数为 0,负数为 1)。
然后进行左移,符号位保持不变,其余位依次向左移动一位,右侧以 0 填充。
然后化为十进制就是所得结果,为 20 。我们来验证一下
// 数字 10 左移 1 位
console.log(10 << 1);
十进制数左移一位,就扩大了 10 倍。二进制左移一位则扩大 2 倍。
所以,我们可以通过左移,快速获得 2 的 n 次方。
// 1 * (2 的 10 次方),也就是 2 的 10 次方
console.log(1 << 10);
// 获得 2 的 24 次方
console.log(1 << 24);
2. 使用异或运算符 ^ 切换变量 0 或 1
在数学中,^ 代表幂运算,但在位运算中,它代表异或运算。
我们用 10 和 12 来演示异或运算。
异或运算规则为:相同为 0 ,不同为 1 。
第一行为数字 10 转化的二进制,第二行为数字 12 转化的二进制,红色框中符号位。
进行异或运算时,紫色框外 (含符号位) 的位全部相同 ,所以异或后这些位得到 0 。
紫色框内的位上不同,它们异或得到一个 1 。
因此,第三行为数字 10 与数字 12 异或运算后的二进制。值为 6 。我们来验证一下
console.log(10 ^ 12);
如果我们需要重复改变为 0 和 1 ,就不再需要用 if 条件语句或者三目运算符了。
let a = 0;
a ^= 1;
console.log(a);
a ^= 1;
console.log(a);
a ^= 1;
console.log(a);
3. 使用按位与运算符 & 判断数值奇偶性
类似 if 语句中的逻辑与 && ,&& 前后都为 true 时,返回 true 。
按位与 & 只有前后都为 1 时,才返回 1 ,否则得到 0 。
我们还是以 10 和 12 来演示
在 10 和 12 转化成的二进制中,只有紫色框中这一位同为 1 ,其他位均包含 0 。
所以按位与之后,其他位 (包括符号位) 均得到 0 。最终结果为第三个二进制串,它代表十进制的 8 。验证如下
console.log(10 & 12);
在二进制中,偶数的最后一位一定为 0 ,奇数的最后一位一定为 1 。
我们可以判断二进制最后一位,来得知这个数值的奇偶性。
console.log(7 & 1);
console.log(8 & 1);
console.log(22 & 1);
console.log(51 & 1);
4. 使用双逻辑非 !! 快捷转为布尔值
逻辑非 ! 在条件判断中十分常见,它能够将布尔类型的两个值互相转换。
另外,建议先阅览 JavaScript 中的隐式类型转换
我们通过两次逻辑非运算,可以快速将某个值转化为对应的布尔值。
console.log(!!123);
console.log(!!"hello");
console.log(!!{ name: "李雷" });
console.log(!!["韩梅梅"]);
console.log(!!0);
console.log(!!"");
console.log(!!NaN);
结果如下
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);
负数的右移操作需要格外注意,因为负数在内存中是以补码形式存在的。我们以 -12 来演示
上图为转补码的过程,先将数值转化为 32 位有符号的整数(第一行)。
然后将这个二进制除符号位按位取反,得到该数字的反码(第二行)。
然后对这个反码 +1,就得到了该数值的补码。
对负数进行位操作时,都会先转化为这个负数的补码,再进行位运算,而并不像正数一样使用原码。
当位运算结束后,会再次执行求补码的过程(得到原码),得到的数字才是所得到的结果。
注意无符号右移 >>> 会忽略数字的正负号,将符号位做为普通位进行运算。这将导致符号位丢失。
正数进行无符号右移,由于左边始终以 0 补位,所以没有影响(正数符号位是 0)。
当负数进行无符号右移时,符号位被作为普通位向右移,左边被 0 补位。如下图所示
此时,得到这是一个无符号的数,所有位都是普通位。所以会直接转化为十进制得到值。
所以 -12 无符号右移一位应该得到如下结果
代码验证如下
console.log(-12 >>> 1);
不妨试一试右移 0 位。
console.log(-12 >>> 0);
再次分析
首先,还是将负数转化为补码。然后,将符号位化为普通位,形成一个没有符号的 32 位二进制串。
然后向右移动 0 位。最后,由于这个二进制串已经没有符号位了,得到结果。验证一下
因此,我们应该避免使用无符号右移来操作负数。
6. 使用异或 ^ 来完成值交换
我们再次观察一下异或操作 ^ ,方便接下来的理解。
除了 “相同为 0 ,不同为 1 ”的规律 ,你是否还能发现其他规律呢?就像 a ^ b = b ^ a 。
当左边的数为 0 时,其结果就和右边的数相等,当左边的数为 1 时,其结果就和右边的数相反。
下面通过异或 ^ 实现交换值。
let a = 10;
let b = 12;
a ^= b;
b ^= a;
a ^= b;
console.log("a = " + a);
console.log("b = " + b);
第一次异或运算如下图所示
先将 a 和 b 化为二进制串。通过第一次异或运算,得到的结果是标记了 a 和 b 在哪些位不一样。
相同的位得到 0 ,不同的位得到 1 。然后将这个标记用 a 保存了下来。
第二次异或如下图所示
根据规律,只要后者为 1 结果就与左边的相反。而此时 a 就是记录了 b 哪些位数与最开始的 a 不一致。
取反之后恰好就得到了最开始的 a 的值。实现了将最开始 a 的值赋值给了现在的 b 。
第三次异或如下图所示
此时的 b 变量已经得到最开始的 a 变量的值,a 变量依然是记录的差异值,和第二次异或的原理相同,异或操作将按照差异值进行取反。
此时的 b 变量按照差异值取反等价于最初的 a 变量按照差异值取反,就可以得到最初的 b 变量。
至此,a 与 b 成功完成值的交换,且没有申请多余的空间。
7. 使用异或 ^ 判断符号是否相同
只要两个数值同为正数或者同为负数,他们的符号位进行异或操作之后一定会得到一个 0 。
如果两个数值为一正一负,它们的符号位异或之后一定会得到一个 1 。
因此只要判断异或后的数值是否大于或等于 0 。就可以得知两个数值是否同号。
画图来帮助理解
如果异或后得到一个正数,则表示两个数值同号。同理,只要得到一个负数,则代表两个数值异号。
let a = 100;
let b = 299;
(a ^ b) >= 0 ? console.log("符号相同") : console.log("符号相反");
再试一次
let a = -17;
let b = 50;
(a ^ b) >= 0 ? console.log("符号相同") : console.log("符号相反");
8. 使用异或 ^ 来检查数值是否不相等
只要两个数值相等,他们转化得到的二进制串一定相同。如果两个数值不相等,转化得到的二进制串一定不相同。
完全相同的二进制串进行异或,将得到 0 ,不相同的二进制串,异或得到非 0 。
let a = -17;
let b = 50;
a ^ b ? console.log("a != b") : console.log("a == b");
再试一次
let a = 24;
let b = 24;
a ^ b ? console.log("a != b") : console.log("a == b");
9. 通过 n&(n-1),判断 n 是 否为 2 的整数幂
首先,我们需要明白,到底怎样才算 2 的整数幂。
如果 n 是 2 的整数次幂,那么 n-1 不难知道。
将这两个二进制串进行按位与 & ,只能得到 0 。
我们来反证一下。如果数值不是 2 的整数次幂。那么它的二进制一定有两个或两个以上的位是 1 。
对这个二进制串进行减一的时候,只能将最后一个 1 及其后的位数进行变更,前面为 1 的位保持不变,这将导致 n&(n-1) 必定会得到一个非 0 的值,如下图。
代码如下
function check(num) {
if (num & (num - 1)) {
console.log(`${num} 不是 2 的整数次幂!`);
} else {
console.log(`${num} 是 2 的整数次幂。`);
}
}
check(64);
check(500);
check(1024);
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);
去掉小数部分的方式有很多,上文有不少方法。注意负数需要 -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);
12. 一些可以替代 undefined 的操作
console.log(""._);
console.log((1)._);
console.log((0)[0]);
console.log(void 0);
前三式的原理相同,访问对象不存在的属性得到 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);
我们可以将数组的 length 属性修改为 0 ,使数组丢弃所有的项实现清空数组。
let arrC = [1, 3, 5, 7, 9];
arrC.length = 0;
console.log(arrC);
14. for 循环条件的简写
我们平时使用 for 循环都是完整的个结构,for 循环的简写并不常见。
我们可以在 w3school 中看到,for 循环的三个语句都是可以省略的。去 w3school 查看 for 循环
同时省略三个语句不会出现任何语法错误,并且可以成功执行代码。但是,请不要运行这样的代码
for (;;) {
}
语句一省略的前提是,for 循环不需要用来控制循环终止的变量,或者已经拥有能够控制循环终止的变量。
let i = 5;
for (; i > 0; i--) {
console.log(i);
}
语句二省略前提是,for 循环必须拥有触发循环终止的条件。
for (let i = 5; ; i--) {
if (i <= 0) {
break;
}
console.log(i);
}
语句三省略的前提是,for 循环不需要对控制循环终止的变量进行操作,或者已经存在对控制循环终止的变量的操作了。
for (let i = 5; i > 0; ) {
console.log(i);
i--;
}
我们来看一个这样的代码
let i = 5;
for (; i--; ) {
console.log(i);
}
上面代码将语句三和语句二融为一体,每一次循环时,i 作为判断条件,被隐式转化为布尔值。
判断结束后,就进行自减操作,i-- 既完成了语句二终止条件的功能,也完成了语句三改变控制循环终止变量的功能。
当且仅当 i 为虚值(falsy值)时,判断得到 false ,循环终止。