谈谈JS中的~

前言

最近在阅读Koa2源码,在阅读过程当中get到了一些特别的技巧:就是关于~这个家伙的。为此作了一些调研,把学到的东西分享给你们~~javascript

~ 是啥

非科班出身的同窗可能会对这个~符号比较懵,这里简单介绍一下(科班同窗就能够跳过啦)。~是位运算符的一种,它的做用是按位取反。java

咱们都知道,计算机其实只认两个符号:0、1,但其实在计算机中,数值一概是用补码来表示和存储,所以位运算也是基于补码运算的。git

这里又涉及到了补码的概念,因此简单介绍一下原码、补码:github

  1. 正数的原码、补码是它自己的二进制
  2. 负数的原码是它的二进制,从左往右数第一位做为符号位,1表明负数。如:-1的原码是 1 000 0001
  3. 负数的补码是它的原码的非符号位的按位取反加1。举个🌰:-1的原码是1000 0001 (左边第一个1就是符号位,1表明负数),除非符号位按位取反加1后是 1 111 1111。

~ 是如何实现的

JS中的~与其余语言中的~略有不一样,这里先简单介绍一下比较常规的~。数组

在对正数进行~操做时:函数

  1. 将正数的原码转换为补码(都是它的二进制啦)
  2. 对补码按位取反 (注意二进制第一个符号位,本来是0,表明正数)
  3. 正数的补码按位取反后,计算机:你这小子第一位如今是1啊,是个负数的补码
  4. 计算机将补码转换为原码(转换规则也是非符号位的按位取反加1)
  5. 将原码转换对应进制输出(10进制)

举个实例:性能

  1. 3的原码补码都是 0000 0011
  2. 按位取反后 1111 1100
  3. 补码转换为原码 1 000 0100
  4. 输出10进制:-4

对负数进行~操做时:测试

  1. 将负数的原码转换为补码(强调下:负数的补码是它的原码的非符号位的按位取反加1)
  2. 补码按位取反
  3. 计算机:你小子原来是个正数啊,原码、补码都同样了,真好办
  4. 补码直接输出对应进制

举个实例:ui

  1. -3的原码是 1000 0011
  2. -3的补码是 1111 1101
  3. 按位取反后 0000 0010
  4. 输出10进制:2

总结:~x = -x - 1,如 ~3 = -3-1 = -4 ;~(-3) = 3 - 1 = 2es5

而在JS里,~会首先对运算对象进行转换为整数的操做,转换规则参考ECMA-262规范的ToInt32。这里简单翻译一下,假设咱们输入的参数为input;

  1. let number = ToNumber(input) (ToNumber函数定义)
    1. 关于ToNumber简单提一下,若是参数类型是Undefined 返回 NaN
    2. 若是是Null,返回 +0
    3. 若是是布尔值,值为true,返回1,值为false,返回0
    4. 若是是数字,返回它自己
    5. 若是是字符串:参照规则9.3.1,规则比较多,不一一叙述了
    6. 若是是对象:
      1. let primValue = ToPrimitive(input argument, hint Number) (ToPrimitive定义;第一个参数为输入的变量,第二个为控制变量,若是输入的变量能够转换为多个基本类型,能够用第二个变量控制)
      2. 返回 ToNumber(primValue)
  2. 若是number值是NaN, +0, −0, +∞, 或者 −∞,则返回 +0
  3. let posInt = sign(number) * floor(abs(number))
    1. sign() 函数根据输入内容是正数仍是负数,返回1或者-1,可是没法用于0;(sign定义)
    2. abs 取number的绝对值 (abs定义)
    3. floor 取最接近该number且不大于它自己的整数 (floor 定义)
  4. let int32bit = posInt modulo 232
    1. modulo 为取模运算 (modulo定义)
  5. 若是 int32bit >= 231,返回 int32bit - 232,不然返回 int32bit

举个例子:~undefined,在转换过程当中,ToNumber(undefined)结果为NaN,ToInt32(NaN)结果为+0,所以~undefined结果与~+0一致,为-1。另外对于对象的转换,核心规则主要根据[[DefaultValue]] (hint)。下面各举一些对象转换的例子:

> ~{}
-1
> ~[]
-1
> ~NaN
-1
> ~[1]
-2
> ~[2,3]
-1
> ~{toString: () => '45'}
-46
> ~{toString: () => '45',valueOf: () => 123}
-124
复制代码

~ 妙用

  1. 对于-1,在JS中有个很出名的函数是indexOf(),若是indexOf返回-1 即表示要找的内容在目标数组、字符串中不存在。
const target = [1,4,56,7]
console.log(!!~target.indexOf(8)) //false
// 经过~转换为0后,不少处理能够简化,
// 能够免去写 !== -1 或者 === -1等烦恼
复制代码
  1. 注意下 ~x = -x -1,那么 ~~x = ~(~x) = - (-x - 1) - 1 = x, 即 ~~x == x自己。刚才也提到,在进行~x时,会先通过整数处理,所以~还有转换为整数的巧妙做用
const target = '321'
console.log(typeof ~~target) // number
复制代码
  1. 刚才也提到JS是进行转换整数的处理,对于正浮点数,采起的是相似Math.floor的处理,便可以用~~去代替Math.floor,而负浮点数是相似Math.ceil的处理。总而言之只取整数部分。
const target1 = '321.235'
console.log(~~target1) // 321

const target2 = '-321.77'
console.log(~~target2) // -321
复制代码
  1. ~ 与 ! ~与!虽然逻辑不一样,可是在某些场合下能起到相同的做用
> ~~undefined == !!undefined
true
> ~~null == !!null
true
> ~~0 == !!0
true
> ~~1 == !!1
true
// 注意,并非全部同样的输入,~、!计算结果都相等
> ~~'a' == !!'a'
false
> ~~[0] == !![0]
false
> ~~-1 == !!-1
false
> ~~2 == !!2
false
复制代码

~ 速度比较

上面提到了~的几种用处,归根结底都是利用了~能快速转换整数的能力,下面分别对~~与其它经常使用的转换操做进行简单的速度比较,测试环境:macOS Mojave 10.14.2 Node v8.12.0

let {numArr,stringArr} = require('./data');
let func1 = number => {
  let start = performance.now();
  let b = ~~number; // 测试符号:包括 ~ 、Math.floor、parseInt、+ 四种
  return {
    value: b,
    time: performance.now() - start
  };
};
let totalTime = 0;
numArr.map(item => {
  totalTime += func1(item).time
})
console.log({
  type: 'fun1',
  totalTime,
  len: numArr.length,
  average: totalTime / numArr.length
})


复制代码

因为篇幅问题,这里不放全全部代码了,主要修改内容为有测试的运算符号以及数据来源。数据为MockJS生成随机浮点数数组,包含正负浮点数、正负浮点字符串、随机Null、undefined、NaN数组;

  1. 测试转换内容为±浮点数:
操做符号 测试数量 总耗时 平均时间
~~ 609 0.24905507266521454 0.00040895742637966264
Math.floor 609 0.34583795070648193 0.00056787840838502780
parseInt 609 0.28167189657688140 0.00046251542951868867
+ 609 0.31073787808418274 0.00051024282115629350
  1. 测试转换内容为±字符串浮点数:
操做符号 测试数量 总耗时 平均时间
~~ 701 0.4971509501338005 0.0007092024966245371
Math.floor 701 0.5587870776653290 0.0007971284988093138
parseInt 701 0.5465480908751488 0.0007796691738589855
+ 701 0.4985470473766327 0.00071119407614355590
  1. 测试内容为undefined、null、NaN:
操做符号 测试数量 总耗时 平均时间
~~ 2187 2.5649426132440567 0.0011728132662295642
!! 2187 2.2864151149988170 0.0010454572999537344

在JS中,因为有参数的转换,~~必然会存在性能的损耗,可是这个损耗在咱们能够接受的范围内,对比上面几组测试结果可得知,同是数字的状况下,~速度最快,而其余两种状况因为转换的缘由,略有下降。

题外话

在测试过程当中,还发现了一个有意思的事,发现用console、performance测量时,第一个耗时较长,有图有真相:

第一、2张图是console.time、performance.now计算的结果,在第二张图时还互换了下两个函数调用顺序。猜想是第一次调用console.time、performance须要进行某些初始化。

相关文章
相关标签/搜索