类型

为什么有的编程规范要求用 void 0 代替 undefined ?

因为undefined有可能被赋值,undefined是一个变量,并不是一个关键字,不存在赋值报错的情况,全局的undefined被赋值总是会失败,但是如果我们在对象的属性上创建了一个名为undefined的属性,却能正确的被使用。

  1. let undefined = 1;
  2. let obj = {
  3. undefined : 2,
  4. };
  5. console.log(undefined); // Uncaught SyntaxError: Identifier 'undefined' has already been declared
  6. console.log(obj.undefined); // 2

0.1+0.2不是等于0.3?

IEEE754中对于浮点数的处理是有误差的,这源自于机器只识别二进制,所以将小数转为二进制数之后可能有精度丢失问题。

解决办法:

  1. 将数值转化为整数进行运算,再将运算的结果转化成浮点数

    一般使用场景多为两位小数运行,如果位数大于2时会出现问题。

  1. function add(a,b) {
  2. a = a * 100;
  3. b = b * 100;
  4. return (a + b) / 100;
  5. }
  1. 方法1有局限性,对其方法进行改进

    将平方根动态计算,解决位数大于2的问题

  1. function add(a,b) {
  2. let baseNum,baseNum1,baseNum2;
  3. // 将a,b小数点后的数值取出
  4. try {
  5. baseNum1 = a.toString().split('.')[1].length;
  6. } catch (e) {
  7. baseNum1 = 0;
  8. }
  9. try {
  10. baseNum2 = b.toString().split('.')[1].length;
  11. } catch (e) {
  12. baseNum2 = 0;
  13. }
  14. // 求出平方根
  15. baseNum = Math.pow(10,Math.max(baseNum1,baseNum2));
  16. return (a * baseNum + b * baseNum) / baseNum;
  17. }
  1. 若需要复杂且全面的运算功能那必须上math.js,其内部引用了decimal.jsfraction.js。功能异常强大,用于生产环境上妥妥的

    为什么给对象添加的方法能用在基本类型上?

    因为会对基本类型进行装箱操作,然后再原型链上查找调用对应的方法。

类型转换

因为 JS 是弱类型语言,所以类型转换发生很频繁,大部分我们熟悉的运算都会先进行类型转换。

其中最复杂的是 Javascript 中的 ‘==’ 运算,因为试图实现跨类型的比较,它的规则复杂到几乎没人可以记住。所以在很多实践中推荐禁止使用 ‘==’,而要求进行显示的类型转换后,用 === 比较。

语言类型 - 图1
较为复杂的部分是 Number 和 String 之间的转换,以及对象跟基本类型之间的转换。我们分别来看一看这几种转换的规则。

StringToNumber

字符串到数字的类型转换,存在一个语法结构,类型转换支持十进制、二进制、八进制和十六进制,比如:

  • 30;
  • 0b111;
  • 0o13;
  • 0xFF;

此外,Javascript 支持的字符串语法还包括正负号科学计数法,可以使用大写或小写的 e 来表示:

  • 1e3;
  • -1e-2;

需要注意的是,parseInt 和 parseFloat 并不使用这个转换,所以支持的语法跟这里不尽相同。

在不传入第二个参数的情况下,parseInt 只支持 16 进制前缀 “0x” ,而且会忽略非数字字符,也不支持科学计数法。

多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。

NumberToString

在较小的范围内,数字到字符串的转换是完全符合你直觉的十进制表示。当Number绝对值较大或较小时,字符串表示则是使用科学计数法表示。

装箱转换

每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所以装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的 call 方法来强迫产生装箱。

我们定义一个函数,函数里面只有 return this,我们调用函数的 call 方法到一个 Symbol 类型的值上,这样就会产生一个 SymbolObject。

我们可以用 console.log 看一下 typeof,它的值是 object,我们使用 SymbolObject instanceof 可以看到,它是 Symbol 这个类的实例,我们找它的 constructor 也是等于 Symbol 的,所以我们无论从哪个角度看,它都是 Symbol 装箱过的对象:

  1. var symbolObject = (function (){ return this; }).call(Symbol("a"));
  2. console.log(typeof symbolObject); // object
  3. console.log(symbolObject instanceof Symbol); //true
  4. console.log(symbolObject.constructor == Symbol); //true

装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。

使用内置的 Object 函数,我们可以在 Javascript 代码中显示调用装箱能力。

  1. var symbolObject = Object(Symbol('a'));
  2. console.log(typeof symbolObject); //object
  3. console.log(symbolObject instanceof Symbol); //true
  4. console.log(symbolObject.constructor == Symbol); //true

每一类装箱对象都有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:

  1. var symbolObject = Object(Symbol('a'));
  2. console.log(Object.prototype.toString.call(symbolObject));

在 Javascript 中,没有任何方法可以更改私有的 Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。

但需要注意的是,call 本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。

拆箱转换

在 Javascript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即拆箱转换)。

对象到 String 和 Number 的转换都遵循 “先拆箱再转换” 的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 Stiring 或者 Number。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。

  1. var o = {
  2. valueOf: ()=> { console.log('valueOf'); return {} },
  3. toString: ()=> { console.log() }
  4. }
  5. o * 2
  6. // valueOf
  7. // toString
  8. // TypeError

在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

  1. var o = {
  2. valueOf: ()=> { console.log('valueOf'); return {} },
  3. toString: ()=> { console.log() }
  4. }
  5. o[Symbol.toPrimitive] = ()=> { console.log('toPrimitive'); return 'hello' }
  6. console.log(o + '');
  7. // toPrimitive
  8. // hello