前语

一个知道同事去面大厂,我问他面了什么,他说形象深入的有两道题,其实都是网上的题,可是由于人家懂原理,就能够持续深挖,咱们先看题:

原问题如下:

  • 浮点数相关:阿里面试官说都知道0.1 + 0.2 不等于 0.3,那为啥 0.1 + 0.1 等于 0.2,你能说说其中的原理吗?(他心里想,我只知道 0.1 + 0.2 不等于 0.3。。。咋办。。。)
  • 字符编码相关:腾讯面试官说为什么一般一个汉字例如
"你".length //  等于1

"".length //  等于2

那么 length 指的到底是什么长度?(他当时心里想,什么鬼啊,长度不一样我哪知道!)

我就从这两个题出发,把问题拆解,构成了如下文章,一起,也更到到自己的github上的前端学习后端知识系统系列,有爱好的朋友能够加我微信一起进群交流

一个中文占多少字节?Unicode 跟编码有什么关系?js 是什么编码?(北京阿里)

一个中文占多少字节?

一个中文字符占用多少字节跟编码密切相关,不能直接说一个中文占 2 个或许 3 个字节,比如 UTF-8 编码下一个中文字符一般占用 3 个字节。

Unicode 跟编码有什么关系?

Unicode 是一种字符集,它界说了每个字符对应的仅有编号,可是并没有规则怎么存储这些字符。编码则是将字符集中的字符转换为字节序列的办法。例如:utf16 能够用两个字节或四个字节来表明一个字符,你也能够规划一种编码 Unicode 的办法,例如统一用 4 个字符表明。

js 是什么编码?

JavaScript 中,一般运用 UTF-16 编码。而咱们的网页一般是 UTF-8 编码,所以运用 javascript 内部的字符串办法时,实际上内部会做一个转换,是以 utf-16 为准。
例如:

"".length; //2

由于一般一个汉字的 length 特点是 1,由于在 unicode 字符集中的数字大小,只需要 utf-16 用两个个字节保存即可。有些数字较大,超过了两个字节表明的范围,就需要 4 个字节表明。

再介绍一个概念,叫码元(code unit),是指存储字符的最小单位,这里即 2 个字节。所以 length 便是码元的个数,上面的表情符号用了 4 个字节,所以是 2 个码元,所以 length 是 2。

为什么 0.1 + 0.2 不等于 0.3?为什么 0.1 + 0.1 等于 0.2 ?请结合 IEEE 标准来说,怎么避免这种核算差错 (深圳腾讯)

在 IEEE 754 标准中,浮点数的表明是有限的,而 0.1 和 0.2 在二进制下是无限循环小数。为什么是无限循环呢,这就要了解 10 进制小数怎么转换为 2 进制小数了,办法如下:

十进制小数转为 n 进制

咱们以 2 进制为例,方法是选用“乘 2 取整,顺序排列”法。具体做法是:

  • 用 2 乘十进制小数,能够得到积,将积的整数部分取出
  • 再用 2 乘余下的小数部分,又得到一个积,再将积的整数部分取出
  • 如此进行,直到积中的小数部分为零,或许达到所要求的精度为止

咱们举个比如:

如: 十进制 0.25 转为二进制

  • 0.25 * 2 = 0.5 取出整数部分:0
  • 0.5 * 2 = 1.0 取出整数部分 1

即十进制0.25的二进制为 0.01 ( 第一次所得到为最高位,最后一次得到为最低位)

此时咱们能够试试十进制0.10.2怎么转为二进制,就知道为啥 0.1 + 0.2 不等于 0.3 了

0.1(十进制) = 0.0001100110011001(二进制)
十进制数0.1转二进制核算进程:
0.1*20.2……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.2”接着核算。
0.2*20.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着核算。
0.4*20.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.8”接着核算。
0.8*21.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.6”接着核算。
0.6*21.2……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着核算。
0.2*20.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着核算。
0.4*20.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.8”接着核算。
0.8*21.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.6”接着核算。
0.6*21.2……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着核算。
0.2*20.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着核算。
0.4*20.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.2”接着核算。
0.8*21.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着核算。
……
……
所以,得到的整数依次是:“0”,“0”,“0”,“1”,“1”,“0”,“0”,“1”,“1”,“0”,“0”,“1”……。
由此,我们肯定能看出来,整数部分呈现了无限循环。

这时,面试官又问我,已然 0.1 的 10 进制不能精确转换为 2 进制,为什么 0.1 + 0.1 == 0.2 是 true ?

为什么 0.1 + 0.1 == 0.2 是 true

由于 64 位浮点数,小数部分最多展现 16 位,由于在 IEEE 754 标准中的 64 位浮点数的小数部分,最多有 53 位, 2 的 53 次方便是 16 位数字,所以小数部分最多展现 16 位。

如下:

(0.1).toPrecision(16);
"0.1000000000000000"(0.1).toPrecision(17);
("0.10000000000000001");

所以 0.1 + 0.1 正好 16 位(四舍五入)切断,等于 0.2。

怎么避免

避免办法能够运用将数字转化为字符串,然后模仿加法运算,一些库便是这样完成的,所以更建议运用成熟的第三方库,当然,这种模仿运算功能并不好(相比于支持 decimal 类型的语言)。能够完全避免呈现差错。(千万甭说能够用乘法转换为整数做运算,也一样会有差错)。

补码有什么用?(上海字节)

运用补码表明法,能够将减法转化为加法,这样加法和减法都能够在加法器上进行运算,简化了核算机的规划和完成。

例如 2 – 4 能够换算为 2 + (-4)。

在补码表明法中,正数的补码与其二进制表明形式相同。负数的补码由对应正数的补码按位取反(即 0 变为 1,1 变为 0),然后再加 1 得到。

基本原理,咱们拿时钟举例:

在时钟上,时针加上(正拨)12 的整数位或减去(反拨)12 的整数位,时针的位置不变。14 点钟在舍去模 12 后,成为(下午)2 点钟(14=14-12=2)。从 0 点出发逆时针拨 10 格即减去 10 小时,也可看成从 0 点出发顺时针拨 2 格(加上 2 小时),即 2 点(0-10=-10=-10+12=2)。因此,在模 12 的前提下,-10 可映射为+2。

举例:
在 JavaScript 中,能够运用按位非(~)运算符来完成取反操作,运用按位与(&)运算符来完成加 1 操作。以下是一个完成减法运算的比如:

function subtract(a, b) {
  b = ~b + 1;
  return a + b;
}