跳到主要内容

数字

位操作符

强制类型转换

  • ~{} = ?
  • ~1.24 = ?

按位非运算符()会先将所有值转换为整数,再以二进制表示,最后执行按位取反运算。对 nullundefined、字符串或对象等非数字进行按位非操作,返回的结果都为 -1。对任意数值 “X” 进行按位非的操作大致等同于 “-(X+1)”,并且浮点数的小数部分会被截除。

位移

考虑下面的代码,都会打印出什么?

console.log(1 << 3);
console.log(12 >> 4);
console.log(24 >>> 4);
console.log(11 | 6);
console.log(15 ^ 20);
console.log(~13);

答案:8 0 1 15 27 -14

这些运算符在平时可能很少用到,而且每个语言中基本都有这些运算符(可能符号不一样,但功能是一样的)。

🔍 解析

首先要说一下几个概念:源码、反码、补码、符号位。

在 JavaScript 中,按位操作符可以将操作数当作 32 位的比特序列进行运算,最左边的位是符号位,0 表示正数,1 表示负数,其他位用来表示数值。

  • 源码:最高位用符号位表示,数值位用二进制表示;

  • 反码

    • 若操作数是正整数,它的反码就等于源码;
    • 若操作数是负整数,它的反码等于:符号位不变,数值位按位取反;
  • 补码

    • 若操作数是正整数,它的补码等于反码,也就等于源码;
    • 若操作数是负整数,它的补码等于它的反码加一(不算上符号位);
  • 特殊的:+0 的补码等于 +0 的源码;-0 的补码等于 -0 的反码加一(算上符号位,也就是 -0 的补码与 +0 的补码一样)。

在运算时,所有的按位操作符的操作数都会被转成 补码 形式的有符号 32 位整数。

按位操作的操作数有效范围是 -(2^31) ~ 2^31 - 1。之所以是 31,是因为有一位是符号位。2^31 -1 是因为正整数表示不了 2^31 这个数,这个数会溢出。例如,假如一个 4 位的有符号数,它的最大表示数值为:0111(补码等于源码),0 是符号位,数值位 111 化成十进制就是 7,它是 2^3 - 18 的补码等于 0000,溢出的最高位会被丢弃,在 4 位的有符号数中,7 如果再加一结果就会变成 0。因此呢,32 位的符号数,最大表示数值是 2^31 - 1

负数最高数值并没有减一,假如一个四位的有符号数是 -8,那它的源码就是 1000(溢出的 1 被截掉),它的补码就是 1111(符号位不变,数值位取反),它的补码就是 1000(反码加一),-8 可以表示出来,感觉 1000 好像是 -0 啊~,-0 的源码确实是这个,但它还要转成反码:1111,它的补码等于反码加一(而且还算上符号位),结果等于 0000(进位 1 被丢弃)。

  1. << 左移操作

    1 化成二进制:1;将 1 往左移动三位,就变成了 1000(右边补三个零)。1 << 31 会得到负数中最后一个操作数。

  2. >> 右移操作

    12 化成二进制:1100;向右移动三位,右边删掉三个数位:1(2**31 - 1) >> 31 会得到 0

  3. >>> 无符号右移

    向右被移出的位被丢弃,左侧用 0 填充。对于负数,符号位会被移动,前面补 0,变成一个整数,例如:

    -9 : 11111111111111111111111111110111
    -9 >>> 2 : 00111111111111111111111111111101 = 1073741821
  4. | 按位“或” 都是 0 时结果是 0,其他都是 1;

  5. ^ 按位“异或” 相同为 0,不同为 1;

    a^a 等于 0;a^0 等于 aa^a^a 等于 a

  6. & 按位“与” 都是 1 时结果是 1,其他结果都是 0;

  7. ~ 按位“非” 0 变为 1,1 变为 0(包括符号位也会变);

需要注意的是:~1 等于 -2~-1 等于 0~0 等于 -1~-2 等于 1~2 等于 -3

运算符优先级可以参考 MDN 上的表格: 运算符优先级表格

💡 位运算符使用技巧

1. 乘除运算

2 << 1 就等于 2*23 << 4 就等于 3* (2**4)

8 >> 1 就等于 8 / 212 >> 2 就等于 12 / 2 / 2

如果是小数右移,会将结果向下取整:

6.4 >> 2;   // 1 --> (6.4 / 2 / 2) == 1.6 --> 1
9.6 >> 2; // 2 --> (9.6 / 4 = 2.4) --> 2

在计算数组某段的中间索引时,可以这样:

function getMidIdx(from, to){
// from 表示开始的索引
// to 表示结束的索引
return from + ((to - from) >> 1);
}

2. 两个数字交换

例如下面的例子,用异或交换两个整数类型的数字变量:

var n1 = -3, n2 = 4;
n1 ^= n2; // n1 与 n2 异或,然后把结果再赋给 n1
n2 ^= n1;
n1 ^= n2;
console.log(n1, n2); // 4 -3

这种方式只能交换整数,小数运算时小数部分会被截掉。

使用异或还可以判断两个数是不是异号的。例如:

var a1 = 3, a2 = -4;

function isDiffSign(n1, n2){
return (n1 ^ n2) < 0; // 符号位异或时,符号不同异或就会变成负数(不同为 1)
}
console.log(isDiffSign(a1, a2)); // true

在 leetcode 上有这么一道题目:

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次或偶数次。找出那个只出现了一次的元素。

我们就可以使用异或来做这道题。因为 0^a 总是等于 a,而 a^a 总是等于 0,a^a^a 也就等于它本身了。代码如下:

/**
* @param {number[]} nums
* @return {number}
*/
var singleNumber = function(nums) {
let n = 0; // 初始是 0
for(let i = 0;i < nums.length;i ++){
n = n ^ nums[i]; // 异或运算
}
return n;
};

判断是不是2的指数

因为如果一个数是 2 的指数的话,它的二进制表示就绝对是只有一个 1 存在。在判断时,我们可以将这个数减去 1,减去 1 后的数的二进制表示就会变成全是 1,没有零。这俩在进行"与"操作,结果就是 0。

function isTwoIndex(number){
return number & (number - 1) === 0;
}

console.log(isTwoIndex(16)); // true

isNaN

isNaN 用来判断传入的参数是不是 NaN,是就返回 true。它的 polyfill 如下:

function isNaN(value){
var n = Number(value);
// NaN !== NaN 将返回 true,它是一个自身不等于自身的值
return n !== n;
}

isNaN 的不足:如果它的参数既不是 NaN,也不是数字,而是一个其他的类型变量,例如:一个字符串,这个字符串不能转化成数字,返回的结果不是 false,而是 true。我们要做的是传入参数的是 NaN 才被判定是 true,不然就返回 false。代码如下:

function isNaN2(value){
return typeof value === 'number' && isNaN(value);
}

typeof NaN 的结果是 number。Number.isNaN() API 功能与 isNaN2 一致

isInteger

封装一个 isInteger 函数,用于检测传入的值是整数。

代码如下:

function isInteger(num){
return typeof num === "number"
&& isFinite(num)
&& value % 1 === 0;
}

上面代码中,isFinite 是一个全局的函数,用来判断一个数字是不是有限的,例如:

// 下面三个都是无限的
isFinite(Infinity); // false
isFinite(NaN); // false
isFinite(-Infinity); // false

isFinite(0); // true
// 对于非数值的参数会转换成数值
isFinite("0"); // true

Number 对象中也有一个 isFinite 函数,与全局的 isFinite 不同的是:它不会强制将一个非数值的参数转换成数值,这就意味着,只有数值类型的值,且是有穷的。

两数正确相加

在 JavaScript 这门语言中的 Number 类型是IEEE 754的双精度数值,IEEE 754标准就是一个对实数进行计算机编码的标准。这个标准在进行小数运算时精度可能会有不足,使用了IEEE 754标准的语言进行小数运算时会出现精度问题,这种问题不止 JS 这门语言独有。

例如下面的运算并没有得到预期的结果:

15.2 * 3.5  // 结果:53.199999999999996,期望结果:53.2
0.1 + 0.2 // 结果:0.30000000000000004,期望结果:0.3

回到问题,编写一个 accmul 函数,让两个并不大的小数正确相乘。代码如下:

function accMul(n1, n2){
var m = 0,
s1 = n1.toString(),
s2 = n2.toString();

m += s1.split(".")[1].length;
m += s2.split(".")[1].length;
var result = Number(s1.replace(".", "")) * Number(s2.replace(".", ""));
result = result / Math.pow(10, m);
return result;
}

如果 n1n2 都是小数,先用 toString 将两个小数转成字符串,调用 split 方法分隔整数部分与小数部分,然后拿到小数部分的长度,相乘后的结果的小数位数等于相乘前两个小数的小数位数相加。result 是两个小数转成整数然后相乘,再通过总的小数长度得到最终的运算结果。