JavaScript 类型转换

97 阅读9分钟

类型转换相关属性方法

[@@toPrimitive]

[@@toPrimitive]是内置Symbol方法,表示对象默认的类型转换行为。

目前 JavaScript 中只有Symbol.prototypeDate.prototype定义了默认方法。

  • Symbol.prototype[@@toPrimitive]:返回 Symbol 对象的原始值作为数据类型返回。
  • Date.prototype[@@toPrimitive]:可以转换一个 Date 对象到一个原始值,根据入参 hint 的不同,可以返回 string 或 number 类型。
    • 如果 hint"string""default"[@@toPrimitive]()将会调用 toString。如果 toString 属性不存在,则调用 valueOf。如果 valueOf 也不存在,则抛出一个TypeError
    • 如果 hint"number"[@@toPrimitive]() 会首先尝试 valueOf,若失败再尝试 toString

注意:

  • [@@toPrimitive]()方法不需要自己调用,当对象需要被转换为原始值时,JavaScript 会自动地调用该方法。
  • [@@toPrimitive]()方法如果存在,则必须返回原始值,返回对象会导致TypeError

[Symbol.toPrimitive]

Symbol.toPrimitive 是内置的 symbol 属性,对应的属性值称为[@@toPrimitive]。其指定了一种接受首选类型并返回对象原始值的表示的方法。可以通过自定义对象的[@@toPrimitive]来控制类型转换行为。

系统定义的[@@toPrimitive]优先级高于自定义的[@@toPrimitive]

Symbol.toPrimitive的属性值调用时,会被传递一个字符串参数 hint,表示要转换到的原始值的预期类型。hint 参数的取值是 "number""string""default" 中的任意一个。

对应的三个取值对应着类型转换的三种算法,下面会介绍。

valueOf

除对象类型外,其他类型valueOf()都返回各自对象的原始值。

  • Object:转换成对象的this
  • String:返回String对象的原始值
  • Date:返回Date对象的原始值
  • Number:返回Number对象的原始值
  • BigInt:返回BigInt对象包装的原始值
  • Boolean:返回Boolean对象的原始值
  • Symbol:返回Symbol对象的原始值

toString

  • Object:返回一个表示该对象的字符串
  • ArrayArray.prototype.toString()实际上是在内部调用了Array.prototype.join(',')
  • Number:返回表示该数字值的字符串
  • Date:返回一个字符串,以本地的时区表示该 Date 对象
  • String:返回该字符串的值,与String.prototype.valueOf()完全相同
  • Function:返回一个表示该函数源码的字符串

类型转换规则

上面介绍[Symbol.toPrimitive]提到hint参数的取值为 "number""string""default" 中的任意一个。这三个取值也表示了JavaScript 规范定义了三种类型转换的基本算法

  • "string":字符串类型转换
  • "number":数字类型转换
  • "default":原始值转换

字符串类型转换规则

@@toPrimitivetoString()valueOf()

数字类型转换规则

[@@toPrimitive]valueOf()toString()

原始值转换规则

[@@toPrimitive]valueOf()toString()

前面介绍过Date对象,hint参数为"default"时,转换规则为:@@toPrimitivetoString()valueOf()

返回值处理

在所有情况下,[@@toPrimitive]() 如果存在,必须可调用并返回原始值,而如果它们不可调用或返回对象,valueOftoString 将被忽略。在过程结束时,如果成功,结果保证是原始值。然后,由此产生的原始值会进一步强制类型转换,具体取决于上下文。

基础类型转换规则

转换为Object

  • nullundefined:抛出TypeError
  • NumberStringBooleanSymbol:封装成其对应的基本类型对象。

转换为String

  • null => "null"
  • undefined => "undefined"
  • true/false => "true"/"false"
  • number的字符串化则遵循通用规则,不过那些极小和极大的数字使用指数形式:
// 1.07 连续乘以七个 1000
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 七个1000一共21位数字
a.toString(); // "1.07e21"

转换为Number

  • true/false => 1/0
  • undefined => NaN
  • null => 0
  • string

对字符串的处理基本遵循数字常量的相关规则 / 语法。处理失败时返回 NaN(处理数字常量失败时会产生语法错误)。不同之处对以 0 开头的十六进制数并不按十六进制处理(而是按十进制)。

转换为Boolean

JavaScript 中的值可以分为以下两类:

  1. 可以被强制类型转换为 false 的值
  2. 其他(被强制类型转换为 true 的值)
1. 假值
  • undefined
  • null
  • false
  • +0
  • -0
  • NaN
  • ""
2. 假值对象

它们都是封装了假值的对象。

var a = new Boolean(false);
var b = new Number(0);
var c = new String("");
3. 真值

真值就是假值之外的值。

Symbol类型转换

除了显示调用String()构造函数转化为字符串外,其他类型转化都会报错,抛出TypeError

类型转换方法

String

  1. 构造函数调用,调用String()
  2. 调用toString()方法,该方法的结果通常与String()函数返回的结果相同,nullundefined无此方法
  3. 模板字符串
  4. ++ 运算符即能用于数字加法,也能用于字符串拼接。如果 + 的其中一个操作数是字符串,则执行字符串拼接;如果其中一个操作数是对象(包括数组),则执行 原始值转换规则****

a + ""String(a)的区别:

根据 原始值转换规则 ,a + "" 会按照该顺序调用[@@toPrimitive]valueOf()toString()。而 String(a) 则是直接调用 ToString()

它们最后返回的都是字符串,但如果 a 是对象而非数字结果可能会不一样!

var a = {
 valueOf: function() { return 42; },
 toString: function() { return 4; }
};

a + ""; // "42"
String( a ); // "4"

Number

  1. 构造函数调用,调用Number()
  2. toString():收表示转换基数的可选实参,如果不指定此实参,转换规则将是基于十进制
  3. 数字运算符(+、-、*、/):比如 a - 0 会将 a 强制类型转换为数字
  4. toFixed():根据小数点后的指定位数将数字转换为字符串
  5. toExponential():使用指数记数法将数字转换为指数形式的字符串,其中小数点前只有一位,小数点后的位数则由参数指定(也就是说有效数字位数比指定的位数要多一位)
  6. toPrecision():根据指定的有效数字位数将数字转换成字符串。如果有效数字的位数少于数字整数部分的位数,则转换成指数形式
  7. parseInt / parseFloat:解析一个字符串并返回指定基数的十进制整数/浮点数

Boolean

  1. 构造函数调用,调用Boolean()
  2. !
  3. if (..) 语句中的条件判断表达式
  4. for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)
  5. while (..)do..while(..) 循环中的条件判断表达式
  6. ? : 中的条件判断表达式
  7. 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)

== 和 ===

== 允许在相等比较中进行类型转换,而 === 不允许。

类型相等

基本类型

如果两个值的类型相同,就仅比较它们是否相等。例如,42等于 42,"abc" 等于 "abc"。

有几个非常规的情况需要注意。

NaN 不等于 NaN

+0 等于 -0

引用类型

两个对象指向同一个值时即视为相等,不发生强制类型转换。

类型不相等

== 在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之一或两者都转换为相同的类型后再进行比较。

=== 直接返回 false

  1. nullundefined:在 ==nullundefined 相等(它们也与其自身相等),除此之外其他值都不存在这种情况。
  2. 字符串和数字:字符串转化为数字。
  3. 其他类型和布尔值:布尔值转换为数字。
var a = "42";
var b = true;

a == b; // false
  1. 对象和非对象:
    1. 如果非对象是字符串或数字,对象调用 原始值转换规则
    2. 如果非对象是布尔值,对象调用 原始值转换规则,布尔值会转换为数字。
  1. 其他优先级更高的规则
var a = null;
var b = Object(a);
a == b; // false

var c = undefined;
var d = Object(c);
c == d; // false

var e = NaN;
var f = Object(e); 
e == f; // false

因为没有对应的封装对象,所以 null undefine 不能够被封装, Object(null)Object(undefined) 均返回一个常规对象。

NaN 能够被封装为数字封装对象,但拆封之后NaN == NaN返回false,因为NaN不等于NaN

特殊情况

// 比较特殊的隐式转换
"0" == false; // true
false == 0; // true
false == ""; // true
false == []; // true
"" == 0; // true
"" == []; // true
0 == []; // true

// 非常特殊的隐式转化
[] == ![] // true

根据 ToBoolean 规则,它会进行布尔值的显式类型转换(同时反转奇偶校验位),所以 [] == ![] 变成了 [] == false;布尔值 false 会隐式转换为0,变成[] == 0[]会隐式转换为"",变成"" == 0;最后""会隐式转换为0,等式成立。

抽象关系比较

NaN和任何数比较都为false

  1. 比较双方首先调用 [@@toPrimitive](),如果结果出现非字符串,就根据 数字类型转换规则 将双方强制类型转换为数字来进行比较。
var a = [42];
var b = ["43"];

a < b; // true;
b < a; // false
  1. 比较双方都是字符串,则按字母顺序来进行比较:
var a = ["42"];
var b = ["043"];

a < b; // false

a 和 b 并没有被转换为数字,因为 [@@toPrimitive]() 返回的是字符串,所以这里比较的是 "42"和 "043" 两个字符串,它们分别以 "4" 和 "0" 开头。因为 "0" 在字母顺序上小于 "4",所以最后结果为 false。

同理:

var a = [4, 2];
var b = [0, 4, 3];

a < b; // false

a 转换为 "4, 2",b 转换为 "0, 4, 3",同样是按字母顺序进行比较。

再比如:

var a = { b: 42 };
var b = { b: 43 };

a < b; // false

结果还是 false,因为 a 是 "[object Object]",b 也是 "[object Object]",所以按照字母顺序 a < b 并不成立。

  1. 比较操作符
var a = { b: 42 };
var b = { b: 43 };

a < b; // false
a == b; // false
a > b; // false
a <= b; // true
a >= b; // true

a <= b 被处理为 b < a,然后将结果反转。因为 b < a 的结果是 false,所以 a <= b 的结果是 true。

这可能与我们设想的大相径庭,即 <= 应该是“小于或者等于”。实际上 JavaScript 中 <= 是“不大于”的意思(即 !(a > b),处理为 !(b < a))。同理 a >= b 处理为 b <= a

比较的转换规则如下。

  • 如果有操作数求值为对象,根据 数字类型转换规则 转换为原始值。
  • 如果在完成对象到原始值的转换后两个操作数都是字符串, 则使用字母表顺序比较这两个字符串,其中“字母表顺序”就是组 成字符串的16位Unicode值的数值顺序。
  • 如果在完成对象到原始值的转换后至少有一个操作数不是字符串,则两个操作数都会被转换为数值并按照数值顺序来比较。0-0被认为相等。Infinity比它本身之外的任何数都大,-Infinity 比它本身之外的任何数都小。如果有一个操作数是(或转换后是) NaN,则这些比较操作符都返回false。虽然算术操作符不允许 BigInt值与常规数值混用,但比较操作符允许数值与BigInt进行比较。

参考资料