一、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得到补码
重点:
- JavaScript 负数显示 是 负号+原码(理论上方便查看),比如 parseInt(-10).toString(2) 二进制展示输出是 -1010
- 数据在内存中是以补码形式存储(方便换算),原码和补码是在运行过程进行转换的。二进制的元素其实是补码的运算 通过补码计算得到补码,然后转成反码,再转成原码(这里不是减 1 还是加 1 )。
补码的符号位是真实的数值,只是因为补码的最高位刚好和原码的符号位相同,所以可以当做符号位看,补码是为了表示负数而出现的
[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
表示阿拉伯字母Ain
,U+0041
表示英语的大写字母A
,U+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个字节表示一个符号,根据不同的符号而变化字节长度,具体规则如下:
- 对于单字节的符号,字节的第一位设为
0
,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。 - 对于
n
字节的符号(n > 1
),第一个字节的前n
位都设为1
,第n + 1
位设为0
,后面字节的前两位一律设为10
。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
下表总结了编码规则,字母x
表示可用编码的位。
跟据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
举例:还是以汉字严
为例,演示如何实现 UTF-8 编码。
严
的 Unicode 是4E25
(100111000100101
),根据上表,可以发现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
表示。这正好是两个字节,而且FF
比FE
大1
。
如果一个文本文件的头两个字节是FE FF
,就表示该文件采用大头方式;如果头两个字节是FF FE
,就表示该文件采用小头方式
举例
打开"记事本"程序notepad.exe
,新建一个文本文件,内容就是一个严
字,依次采用ANSI
,Unicode
,Unicode big endian
和UTF-8
编码方式保存。
然后,用文本编辑软件UltraEdit 中的"十六进制功能",观察该文件的内部编码方式。
- ANSI:文件的编码就是两个字节
D1 CF
,这正是严
的 GB2312 编码,这也暗示 GB2312 是采用大头方式存储的。 - Unicode:编码是四个字节
FF FE 25 4E
,其中FF FE
表明是小头方式存储,真正的编码是4E25
。 - Unicode big endian:编码是四个字节
FE FF 4E 25
,其中FE FF
表明是大头方式存储。 - 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-z
,A-Z
,0-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 AD
和E5 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里面的二进制
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。