关于前端:二进制运算

29次阅读

共计 7404 个字符,预计需要花费 19 分钟才能阅读完成。

一、JS 进制

// 二进制(Binary system)
// 以 0b 或 0B 结尾
var x  = 0b10000000000000000000000000000000; // 2147483648
var y = 0B00000000011111111111111111111111; // 8388607

// 二进制转换
// 负数:就是负数的原码
// 正数:负号 + 负数的原码
// 不是数值的二进制补码 源码
(10).toString(2) // 1010
(-10).toString(2) // -1010

// 八进制(Octal number system)
// 以 0 结尾,ECMAScript 6 反对 0o
var n = 0755; // 493
var e = 0o755; // 493 ECMAScript 6 标准

// 十进制(Decimal system)
// 以 0 结尾,然而前面跟 8 以下会当作八进制解决
var l = 0888; // 888 十进制
var o = 0777; // 511 八进制

// 十六进制(Hexadecimal)
// 以 0x 或 0X 结尾
0x123456789ABCDEF   // 81985529216486900
0XA                 // 10

二、原码、反码、补码

先让咱们看下 1 和 -1 原码、反码、补码
而后咱们通过这 2 个数字来解释原码、反码、补码

  • 原码:数字的二进制示意

    • 有符号数:最高位作为符号位,0 示意 +,1 示意 -
    • 无符号数:即无符号位
  • 反码:

    • 负数和 +0 其反码就是原码自身
    • 正数和 -0 原码根底上,符号位放弃不变,其余位数逐位取反,1 换成 0,0 换成 1
  • 补码:

    • 负数和 +0 其补码就是原码自身
    • 正数和 -0 先计算其反码,而后反码加上 1 失去补码

重点:

  1. JavaScript 正数显示 是 负号 + 原码(实践上不便查看),比方 parseInt(-10).toString(2) 二进制展现输入是 -1010
  2. 数据在内存中是以补码模式存储(不便换算),原码和补码是在运行过程进行转换的。二进制的元素其实是补码的运算 通过补码计算失去补码,而后转成反码,再转成原码(这里不是减 1 还是加 1)。
  3. 补码的符号位是实在的数值,只是因为补码的最高位刚好和原码的符号位雷同,所以能够当做符号位看, 补码是为了示意正数而呈现的

    [S2+S1]补 =[S2]补 +[S1]补
    [S2-S1]补 =[S2]补 +[-S1]补

举例计算

-2 的原码:10000010
-2 的反码:11111101
-2 的补码:11111110

计算 -2 + (-2),利用补码计算, 最高位的进位舍弃就好

  11111110
+ 11111110
= 11111100 // 补码
- 1
= 11111011 // 反码
  10000100 = -4 // 原码

溢出

var uint8 = new Uint8Array(1);
uint8[0] = 256;
console.log(uint8[0]) // 0

Uint8Array 是无符号 8 位视图,范畴 0~255,最大 1111 1111 256 是 1 0000 0000,因而只能放后 8 位,所以是 0

uint8[0] = -1;
console.log(uint8[0]) // 255

- 1 在计算机中应用补码存储,的补码是 11111111,依照无符号位那就是 255

正向溢出和负向溢出

下面栗子,第一个是正向溢出,第二个是负向溢出

正向溢出:最小值 + 余数 – 1
负向溢出:最大值 – 余数 + 1

var int8 = new Int8Array(1); // -128~127
int8[0] = 128;
console.log(int8[0]) // -128 = -128+128%127-1

int8[0]=-129;
console.log(int8[0]) // 127 = 127-(-129%-128)+1

解决溢出谬误

int8c = new Uint8ClampedArray(1) // 解决溢出按边界值
int8c[0]=256
console.log(int8c[0]) // 255

int8c[0]=-1
console.log(int8c[0]) // 0

注解:为什么是 -128~127

三、对于文本字符编码

1. ASCII 码

上个世纪 60 年代,美国制订了一套字符编码,对英语字符与二进制位之间的关系,做了对立规定。这被称为 ASCII 码,始终沿用至今。

ASCII 码一共规定了 128 个字符的编码,比方空格 SPACE 是 32(二进制 00100000),大写的字母A 是 65(二进制01000001)。这 128 个符号(包含 32 个不能打印进去的管制符号),只占用了一个字节的前面 7 位,最后面的一位对立规定为0

2. 非 ASCII 编码

英语用 128 个符号编码就够了,然而用来示意其余语言,128 个符号是不够的。于是不同的通过又依据本人的语言拓展了编码

然而,这里又呈现了新的问题。不同的国家有不同的字母,同一个数字代表的字符可能不一样,比方,130 在法语编码中代表了é,在希伯来语编码中却代表了字母 Gimel (ג)

3. Unicode

因为世界上存在着多种编码方式,同一个二进制数字能够被解释成不同的符号。因而,要想关上一个文本文件,就必须晓得它的编码方式,否则用谬误的编码方式解读,就会呈现乱码。

如果有一种编码,将所有符号都纳入其中。那每一个符号都有举世无双的编码,那么乱码问题就会隐没。这就是 Unicode

Unicode 是一个很大的汇合,能够包容 100 多万个符号。每个符号的编码都不一样。比方,U+0639示意阿拉伯字母 AinU+0041 示意英语的大写字母 AU+4E25 示意汉字 。具体的符号对应表,能够查问 unicode.org,或者专门的汉字对应表。

4. Unicode 的问题

Unicode 它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

  • 如何能力区别 Unicode 和 ASCII?计算机怎么晓得三个字节示意一个符号,而不是别离示意三个符号
  • 英文字母只用一个字节示意就够了,如果 Unicode 对立规定,每个符号用三个或四个字节示意,那么每个英文字母前都必然有二到三个字节是 0,这对于存储来说是极大的节约

5. UTF-8

互联网的遍及,强烈要求呈现一种对立的编码方式。UTF-8 就是在互联网上应用最广的一种 Unicode 的实现形式。

其余实现形式还包含 UTF-16(字符用两个字节或四个字节示意)和 UTF-32(字符用四个字节示意),不过在互联网上根本不必。

反复一遍,这里的关系是,UTF-8 是 Unicode 的实现形式之一。

UTF-8 最大的一个特点,就是它是一种变长的编码方式。它能够应用 1~4 个字节示意一个符号,依据不同的符号而变动字节长度,具体规定如下:

  1. 对于单字节的符号,字节的第一位设为0,前面 7 位为这个符号的 Unicode 码。因而对于英语字母,UTF-8 编码和 ASCII 码是雷同的。
  2. 对于 n 字节的符号(n > 1),第一个字节的前 n 位都设为 1,第n + 1 位设为0,前面字节的前两位一律设为10。剩下的没有提及的二进制位,全副为这个符号的 Unicode 码。

下表总结了编码规定,字母 x 示意可用编码的位。

跟据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是 0,则这个字节独自就是一个字符;如果第一位是 1,则间断有多少个 1,就示意以后字符占用多少个字节。

举例:还是以汉字 为例,演示如何实现 UTF-8 编码。

的 Unicode 是 4E25100111000100101),依据上表,能够发现4E25 处在第三行的范畴内(0000 0800 - 0000 FFFF),因而 的 UTF-8 编码须要三个字节,即格局是 1110xxxx 10xxxxxx 10xxxxxx。而后,从的最初一个二进制位开始,顺次从后向前填入格局中的 x,多出的位补0。这样就失去了,的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4B8A5

6. 字节序 Little endian 和 Big endian

以汉字严为例,Unicode 码是 4E25,须要用两个字节存储,一个字节是 4E,另一个字节是 25。存储的时候,4E 在前,25 在后,这就是 Big endian 形式;25 在前,4E 在后,这是 Little endian 形式。

第一个字节在前,就是 ” 大头形式 ”(Big endian),第二个字节在前就是 ” 小头形式 ”(Little endian)。

那么很天然的,计算机怎么晓得某一个文件到底采纳哪一种形式编码?

Unicode 标准定义,每一个文件的最后面别离退出一个示意编码程序的字符,这个字符的名字叫做 ” 零宽度非换行空格 ”(zero width no-break space),用 FEFF 示意。这正好是两个字节,而且 FFFE1

如果一个文本文件的头两个字节是FE FF,就示意该文件采纳大头形式;如果头两个字节是FF FE,就示意该文件采纳小头形式

举例
关上 ” 记事本 ” 程序 notepad.exe,新建一个文本文件,内容就是一个字,顺次采纳 ANSIUnicodeUnicode big endianUTF-8编码方式保留。

而后,用文本编辑软件 UltraEdit 中的 ” 十六进制性能 ”,察看该文件的外部编码方式。

  1. ANSI:文件的编码就是两个字节 D1 CF,这正是的 GB2312 编码,这也暗示 GB2312 是采纳大头形式存储的。
  2. Unicode:编码是四个字节 FF FE 25 4E,其中FF FE 表明是小头形式存储,真正的编码是4E25
  3. Unicode big endian:编码是四个字节 FE FF 4E 25,其中FE FF 表明是大头形式存储。
  4. UTF-8:编码是六个字节 EF BB BF E4 B8 A5,前三个字节EF BB BF 示意这是 UTF- 8 编码,后三个 E4B8A5 就是 的具体编码,它的存储程序与编码程序是统一的。

留神:UTF-8 编码不存在字节序大小端问题(因为字节序只影响同时解决多于两个字节的编码方式,比方 UTF-16/UTF-32,而 UTF- 8 是依照单字节进行解决的),所以 UTF-8 的 BOM 仅起标注文件编码方式的作用,可加可不加

7. 对于 URL 转码

网页的 URL 只能蕴含非法的字符。非法字符分成两类。

  • URL 元字符:分号(;),逗号(,),斜杠(/),问号(?),冒号(:),at(@),&,等号(=),加号(+),美元符号($),井号(#
  • 语义字符:a-zA-Z0-9,连词号(-),下划线(_),点(.),感叹号(!),波浪线(~),星号(*),单引号('),圆括号(()

除了以上字符,其余字符呈现在 URL 之中都必须本义,规定是依据操作系统的默认编码,将每个字节转为百分号(%)加上两个大写的十六进制字母。

比方,UTF-8 的操作系统上,https://www.baidu.com/s?ie=UTF-8&wd= 中国 这个 URL 之中,汉字“中国”不是 URL 的非法字符,所以被浏览器主动转成 https://www.baidu.com/s?ie=UTF-8&wd=%E4%B8%AD%E5%9B%BD。其中,“中”转成了%E4%B8%AD,“国”转成了%E5%9B%BD。这是因为“中”和“国”的 UTF-8 编码别离是E4 B8 ADE5 9B BD,将每个字节后面加上百分号,就形成了 URL 编码。

8. encodeURI 和 encodeURIComponent

  • encodeURI()办法用于转码整个 URL。它的参数是一个字符串,代表整个 URL。它会将元字符和语义字符之外的字符,都进行本义。
  • encodeURIComponent()办法用于转码 URL 的组成部分,会转码除了语义字符之外的所有字符,即元字符也会被转码。所以,它不能用于转码整个 URL。它承受一个参数,就是 URL 的片段。

9. js 进制转换

console.log('0'.charCodeAt()) // "48" 十进制
console.log('0'.charCodeAt().toString(16)) // "30" 十六进制
console.log(0x0030.toString(10)) // "48" 十进制
console.log(String.fromCharCode(48)) // "0"

console.log('万'.charCodeAt().toString(16)) // "4e07" 十六进制
console.log(String.fromCharCode(0x4e07)) // "万"

console.log('万'.charCodeAt().toString(2)) // "100111000000111" 二进制
console.log(String.fromCharCode(0b100111000000111)) // "万"

四、位运算

须要留神:正数按补码模式加入按位与运算。

1. 与操作符(&)

按位与操作符(&)会对加入运算的两个数据 按二进制位 进行与运算,即两位同时为 1 时,后果才为 1,否则后果为 0。运算规定如下:

0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1

例如,3 & 5 的运算后果如下:

   0000 0011
   0000 0101
 = 0000 0001

因而 3 & 5 的值为 1。

例如 3 & -5

  • 3 的二进制 是0000 0011
  • 5 的二进制 是0000 0101
  • - 5 的二进制须要用 5 的补码示意,也就是1111 1011

与运算

  0000 0011
  1111 1011
= 0000 0011 = 3

用处:

(1)判断奇偶

只有依据最未位是 0 还是 1 来决定,为 0 就是偶数,为 1 就是奇数。因而能够用 if ((i & 1) === 0) 代替 if (i % 2 === 0) 来判断 a 是不是偶数。

(2)清零

如果想将一个单元清零,即便其全副二进制位为 0,只有与一个各位都为零的数值相与,后果为零。

(3)是否 2 的 n 次幂

// (x & x - 1) === 0
console.log((2 & 2 - 1) === 0) // true

(4)求平均值避免溢出

// 求平均值,防溢出
function avg(x, y){return (x & y) + ((x ^ y) >> 1);
}

2. 按位或 |

| 运算符跟 & 的区别在于如果对应的位中任一个操作数为 1 那么后果就是 1。

// 1 的二进制示意为: 00000000 00000000 00000000 00000001
// 3 的二进制示意为: 00000000 00000000 00000000 00000011
// -----------------------------
// 1 | 3 的二进制示意为: 00000000 00000000 00000000 00000011
console.log(1 | 3)     // 3

取整

1.3 | 0         // 1
-1.9 | 0  // -1

3. 按位异或 ^

^ 如果对应两个操作位有且仅有一个 1 时后果为 1,其余都是 0。

// 1 的二进制示意为: 00000000 00000000 00000000 00000001
// 3 的二进制示意为: 00000000 00000000 00000000 00000011
// -----------------------------
// 2 的二进制示意为: 00000000 00000000 00000000 00000010
console.log(1 ^ 3)     // 2

异或运算具备以下性质:

  • 交换律:(a^b)^c == a^(b^c)
  • 结合律:(a + b)^c == a^b + b^c
  • 对于任何数 x,都有 x^x=0,x^0=x
  • 自反性: a^b^b=a^0=a;

    // 判断赋值
    if(x === a){x = b}else{x =a}
    // 等价于上面
    x = a ^ b ^ x

    4. 按位非~

~ 运算符是对位求反,1 变 0, 0 变 1,也就是求二进制的反码。

// 1 的二进制示意为: 00000000 00000000 00000000 00000001
// 3 的二进制示意为: 00000000 00000000 00000000 00000011
// -----------------------------
// 1 反码二进制示意: 11111111 11111111 11111111 11111110
// 因为第一位(符号位)是 1,所以这个数是一个正数。JavaScript 外部采纳补码模式示意正数,即须要将这个数减去 1,再取一次反,而后加上负号,能力失去这个正数对应的 10 进制值。// -----------------------------
// 1 的反码减 1:11111111 11111111 11111111 11111101
// 反码取反:00000000 00000000 00000000 00000010
// 示意为 10 进制加负号:-2
console.log(~ 1)     // -2
  • 简略记忆:一个数与本身的取反值相加等于 -1

5. 左移<<

<<左移指定次数,其挪动规定:抛弃高位,低位补 0

// 1 的二进制示意为: 00000000 00000000 00000000 00000001
// -----------------------------
// 2 的二进制示意为: 00000000 00000000 00000000 00000010
console.log(1 << 1)     // 2

6. 有符号右移>>

>>向右挪动指定的位数。向右被移出的位被抛弃,拷贝最左侧的位以填充左侧。因为新的最左侧的位总是和以前雷同,符号位没有被扭转。所以被称作“符号流传”。

// 1 的二进制示意为: 00000000 00000000 00000000 00000001
// -----------------------------
// 0 的二进制示意为: 00000000 00000000 00000000 00000000
console.log(1 >> 1)     // 0
// -1 >>> 1
// - 1 的补码示意为: 11111111111111111111111111111111
// 右移后,还是:   11111111111111111111111111111111
console.log(-1 >> 1)     // -1

7. 无符号右移>>>

>>>右挪动指定的位数。向右被移出的位被抛弃,左侧用 0 填充。因为符号位变成了 0,所以后果总是非负的。(译注:即使右移 0 个比特,后果也是非负的。)

对于非正数,有符号右移和无符号右移总是返回雷同的后果。例如,9 >>> 2 失去 2 和 9 >> 2 雷同。

TODO

阮一峰 Base64 笔记
浮点型数字的存储和计算
0.1 + 0.2 不等于 0.3?为什么 JavaScript 有这种“骚”操作?

参考链接

阮一峰字符编码笔记:ASCII,Unicode 和 UTF-8
URL 编码与解码应用详解
补码的符号位为什么能参加运算
原码运算、反码运算、补码运算和溢出
为什么 8 位有符号类型的数值范畴是 -128~127
web 开发之字符、编码与二进制(一)
JavaScript 外面的二进制

正文完
 0