8

Javascript 里的类型转换规则

 1 year ago
source link: https://www.fly63.com/article/detial/11621
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Javascript 里的类型转换是一个你永远绕不开的话题,不管你是在面试中还是工作写代码,总会碰到这类问题和各种的坑,所以不学好这个那是不行滴。关于类型转换我也看过不少的书和各种博客、帖子,也查过规范和做过各种测试,这里就谈谈我的总结和理解吧。

首先,为了掌握好类型转换,我们要理解一个重要的抽象操作:ToPrimitive。

ToPrimitive

为什么说这是个抽象操作呢?因为这是 Javascript 内部才会使用的操作,我们不会显式调用到。

对于 Undefined、Null、Boolean、Number 和 String 这些基础类型会返回原值不会转换;当需要将对象转换为相应的基本类型值时,ToPrimitive 就会调用对象的内部方法 [[DefaultValue]] 来完成。

ToPrimitive 操作接收两个参数,一个是 input 需要转换的值,第二个是可选参数 hint 代表期望的转换类型。并且在调用 [[DefaultValue]] 的时候 hint 会传递过去。

这里我们首先只需要知道 [[DefaultValue]] 会调用 valueOf() 和 toString() 来完成基本类型值的转换,如果任何一个调用返回了基础值则调用就结束了,没有返回基础值就接着调用另一个。但是注意:valueOf() 和 toString() 的调用逻辑顺序并不是固定的取决于 hint 参数,规则如下:

  • 如果 hint 参数是 String,那么先调用 toString() 再调用 valueOf()。
  • 如果 hint 参数是 Number 或者没有值(ES6+ 有默认值 'default'),则先调用 valueOf() 再调用 toString();但是这里有个例外是 Date 对象,如果是 Date 对象则 hint 当做 String 处理。
  • 如果 valueOf() 和 toString() 都没有返回基础值,则抛出异常:TypeError: Cannot convert object to primitive value。

更新内容:

  1. 对象的 Symbol.toPrimitive 属性,会影响 ToPrimitive。如果对象定义了这个方法,则转换的基础值就是这个方法的返回值,如果这个方法没有返回初始值,则后续也不会再调用 toString() 和 valueOf() 了,会直接抛出异常:TypeError: Cannot convert object to primitive value。 Symbol.toPrimitive 属性的用法请看 这里

JavaScript 中的类型转换总是返回基本类型值,如字符串、数字和布尔值,不会返回对象和函数。那么这也对应了三种抽象操作:ToString、ToNumber 和 ToBoolean,下面就来逐一说明。

ToString

var a = {};
console.log(String(a));
// 显式类型转换,输出为:"[object Object]"

以上代码我们通常称为显式类型转换,这里面就包含 ToString 抽象操作,也就是把非字符串值转换为字符串的操作。

先来看看非对象的基本类型值的 ToString 转换规则:

输入类型输出结果
Undefined"undefined"
Null"null"
Boolean输入 true,输出 "true"
输入 false,输出 "false"
Number输入 NaN,输出 "NaN"
输入 +0 或 -0,输出 "0"
如果输入小于 0 的数字,如:-2,输出将包括负号:"-2"
输入 Infinity,输出 "Infinity"

接着我们重点来看一下输入是对象的转换规则。

这个时候 ToPrimitive 就出场了,并且 hint 参数是 String。还记得 ToPrimitive 内部是调用的[[DefaultValue]]吗,**并且这个时候 hint 是 String **。下面来看下这种情况下 ToPrimitive 的调用逻辑:

  1. toPrimitive 调用 [[DefaultValue]] 并传递 hint,然后返回调用结果

  2. [[DefaultValue]] 根据 hint 是 String 执行以下调用顺序:

    • 如果对象存在 toString() 并返回一个基本类型值,即返回这个值

    • 如果 toString() 不存在或返回的不是一个基本类型值,就调用 valueOf()

    • 如果 valueOf() 存在并返回一个基本类型值,即返回这个值

    • 如果 valueOf() 不存在或返回的不是一个基本类型值,则抛出 TypeError 异常

那么这里就可以总结为:对象在类型转换为字符串时, toString() 的调用顺序在 valueOf() 之前,并且这两个方法如果都没有返回一个基本类型值,则抛出异常;如果返回了基本类型值 primValue,则返回 String(primValue)

基本类型值的 ToString 结果参看前面那个表格。

我们来测试一下。先看下这节开头的例子:

var a = {};
console.log(String(a));

字面量对象的原型是 Object.prototype,Object.prototype.toString() 返回内部属性 [[Class]] 的值,那么结果就是 [object Object]。然后测试一下 ToPrimitive 的调用逻辑。来看下这段代码:

var a = Object.create(null);

上面的意思是创建一个没有原型的对象(没有原型就没有继承的 toString() 和 valueOf() 了)。接下来:

console.log(String(a));
// Uncaught TypeError: Cannot convert object to primitive value

这里因为没有 toString() 和 valueOf() 所以就抛出 TypeError 异常了。OK,跟前面的总结一致。

下面来测试一下 toString() 和 valueOf() 的调用顺序逻辑,上代码:

a.toString = function () {
  console.log('toString');
  return 'hello';
};

a.valueOf = function () {
  return true;
};

console.log(String(a)); // 先输出 'toString',再输出 'hello'

跟前面的总结一致,确实是 toString() 先返回结果。接着做一下变化:

a.toString = function () {
  return {};
};
// 或是直接去掉这个方法,a.toString = undefined;

a.valueOf = function () {
  return true;
};

console.log(String(a)); // 'true'

当 toString() 返回的不是一个基本类型值或不存在 toString() 时,返回 valueOf() 的结果,并且遵循基本类型值的 ToString 转换结果。OK,验证没有问题 ✌️ 其他的情况也可以根据前面的总结逻辑自己验证下。

在《Javascript 高级程序设计(第 4 版)》和《你不知道的 Javascript(中卷)》上均未提到类型转换到字符串会与 valueOf() 有关系。

ToNumber

首先照例先来看下非对象的基本类型值的 ToNumber 转换规则:

输入类型输出结果
UndefinedNaN
Null0
Boolean输入 true,输出 1
输入 false,输出 0
String输入 '',输出 0
输入 'Infinity',输出 Infinity
输入有效数字的字符串(包括二、八和十六进制),输出数字的十进制数值
如果输入包含非数字格式的字符串,输出 NaN

字符串转数字上面只说了一些常用的情况,更多细节请看 这里

然后来看看对象 ToNumber 的情况。这里与对象转字符串的情况类似,也会调用 ToPrimitive 来转换(hint 是 Number)。但细节与 ToString 稍有不同,这里直接给出结论:

对象在类型转换为数字时, valueOf() 的调用顺序在 toString() 之前,并且这两个方法如果都没有返回一个基本类型值,则抛出异常;如果返回了基本类型值 primValue,则返回 Number(primValue)

这里验证了 ToPrimitive 里面说到的,[[DefaultValue]] 会根据 hint 参数决定 toString() 和 valueOf() 的调用顺序。

接着来用代码说话:

var a = Object.create(null);
console.log(Number(a));
// Uncaught TypeError: Cannot convert object to primitive value

这里因为没有 toString() 和 valueOf() 所以就抛出 TypeError 异常了。OK,跟前面的总结一致。

我们先加入 valueOf() 方法:

a.valueOf = function () {
  return 123;
}
console.log(Number(a)); // 123

valueOf() 返回了数字 123,所以输出没问题。再修改一下:

a.valueOf = function () {
  return true;
}
console.log(Number(a)); // 1

valueOf() 返回了 true,这也是一个基本类型,然后根据基本类型转换规则 true 转换为 1,也是对的。

a.valueOf = function () {
  return NaN;
}
console.log(Number(a)); // NaN

NaN 是一个特殊的数值,所以也是基本类型。OK,也是对的。

这里的结果说明了《Javascript 高级程序设计(第 4 版)》关于对象转换为数字的解释是有错误的,书上是这么说的:如果转换的结果是 NaN,则调用对象的 toString() 方法。

再来验证一下 toString() 的调用顺序:

a.valueOf = function () {
  console.log('valueOf');
  return {};
}
a.toString = function () {
  return '123';
}
console.log(Number(a)); // 先输出 'valueOf',再输出 123

因为 valueOf() 返回了对象非基本类型值,转而执行 toString(),返回的 '123' 根据字符串转换数字的规则就是 123,对于 valueOf() 和 toString() 的执行顺序验证也是 OK 的。

ToBoolean

最后我们来看看转换为布尔值。这个比较简单,一个列表可以全部归纳了:

输入类型输出结果
Undefinedfalse
Nullfalse
Number输入 +0,-0,NaN,输出 false
输入其他数字,输出 true
String输入 length 为 0 的字符串(如:''),输出 false
输入其他字符串,输出 true
Object输入任何对象类型,输出 true

ToBoolean 转换规则比较简单,只有一个需要注意的地方,那就是封箱操作:

var a = new Boolean(false);
console.log(Boolean(a));
// 输出是 true 不是 false 喔

new Boolean(false) 返回的是对象不是布尔值,所以最好避免进行类似的操作。

以上即是我总结的 JS 类型转换基本规则,当你明显感知类型转换即将发生时可以拿上面的规则去套(也就是我们通常说的显式类型转换,以上转换规则面试时特别有用喔)。

既然规则有了,下一篇准备聊一下隐式类型转换,有了这篇的基础掌握隐式转换会容易很多。

欢迎 star 和关注我的 JS 博客:小声比比 JavaScript

链接: https://www.fly63.com/article/detial/11621


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK