类型转换:type casting,值从一种类型转换为另一种类的操作。
类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时,在 JavaScript 中统称为强制类型转换。 强制类型转换可具体分为隐式强制类型转换和显示强制类型转换。
var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String(a); // 显式强制类型转换
JavaScript 中的强制类型转换总是返回基本类型值,如字符串、数字和布尔值。
1. 抽象操作
抽象操作:仅供 JavaScript 内部使用的操作。
在 JavaScript 中进行不同类型之间的操作时,JavaScript 会首先把这些类型转换为相同的类型在进行操作,这些通常是发生在内部中的操作,比如字符串的转换、数字的转换、布尔值的转换等等,每种类型两两之间都会有定义好的转换规则与抽象操作,本篇主要介绍 ToString、ToNumber、ToBoolean、ToPrimitive 这些常用的抽象操作。
1.1 ToPrimitive(input [, PreferredType])
当对象 {} + {}
、[] + []
进行类似的操作时,都会调用 ToPrimitive 将对象都会被转换为原始值,然后,再进行操作。
ToPrimitive 会接收两个值,第一个参数是 input
即需要转换的值,第二个是转换的首选类型 preferredType
(优先转换为该类型)。
由于所有的对象在布尔上下文中均为 true,所以对于对象就不存在 to-boolean 的转换,只有字符串和数值的转换。
1.1.1 ToPrimitive 转换规则
第一步,判断 input 是否是原始值
- i. 原始值,直接返回 input,结束转换;
- ii. 对象值,继续执行第二步;
第二步,确定对象转换类型 hint
- i. 如果 preferredType 值是 string,那么 hint = string,表示优先转换为字符串;
- ii. 如果 preferredType 值是 number,那么 hint = number,表示优先转换为数字;
- iii. 如果 preferredType 未传递,则默认为 default(可通过定义 @@toPrimitive 修改默认值),default 会在后续确定。
第三步,判断 input 对象是否拥有 @@toPrimitive 方法
- i. 如果
input[Symbol.toPrimitive]
存在,则传入hint
调用input[Symbol.toPrimitive](hint)
该方法。执行结果如果是原始值,返回调用结果;否则抛出 TypeError,结束转换。 - ii. 不存在该方法,执行第四步。
第四步,转换对象为基本类型
- i. 如果 hint 值是 default,则修改默认值为 number;
- ii. 如果 hint 值是 string,依次执行第五步、第六步、第七步;
- iii.如果 hint 值是 number,执行第六步、第五步、第七步;
第五步,hint 值是 string,执行 input 的 toString 方法
- i. 如果 toString() 的结果是基本类型,那么返回该结果,结束转换;
第六步,hint 值是 number,执行 input 的 valueOf 方法
- i. 如果 valueOf() 的结果是基本类型,那么返回该结果,结束转换;
第七步,抛出错误结束转换
如果程序执行到此步骤,说明无法转换到最终的结果,抛出 TypeError,结束转换。
1.1.2 ToPrimitive 代码实现:
// 获取类型
const getType = (obj) => {
return Object.prototype.toString.call(obj).slice(8, -1);
};
// 是否为原始类型
const isPrimitive = (obj) => {
const types = ["String", "Undefined", "Null", "Boolean", "Number"];
return types.indexOf(getType(obj)) !== -1;
};
const ToPrimitive = (input, preferredType) => {
// 如果input是原始类型,那么不需要转换,直接返回
if (isPrimitive(input)) {
return input;
}
let hint = "",
exoticToPrim = null,
methodNames = [];
// 当没有提供可选参数preferredType的时候,hint会默认为"default";
if (!preferredType) {
hint = "default";
} else if (preferredType === "string") {
hint = "string";
} else if (preferredType === "number") {
hint = "number";
}
exoticToPrim = input.toPrimitive;
// 如果有toPrimitive方法
if (exoticToPrim) {
// 如果exoticToPrim执行后返回的是原始类型
if (typeof (result = exoticToPrim.call(O, hint)) !== "object") {
return result;
// 如果exoticToPrim执行后返回的是object类型
} else {
throw new TypeError("TypeError exception");
}
}
// 这里给了默认hint值为number,Symbol和Date通过定义@@toPrimitive方法来修改默认值
if (hint === "default") {
hint = "number";
}
return OrdinaryToPrimitive(input, hint);
};
const OrdinaryToPrimitive = (O, hint) => {
let methodNames = null,
result = null;
if (typeof O !== "object") {
return;
}
// 这里决定了先调用toString还是valueOf
if (hint === "string") {
methodNames = [input.toString, input.valueOf];
} else {
methodNames = [input.valueOf, input.toString];
}
for (let name in methodNames) {
if (O[name]) {
result = O[name]();
if (typeof result !== "object") {
return result;
}
}
}
throw new TypeError("TypeError exception");
};
1.1.3 Symbol.toPrimitive
Symbol.toPrimitive
是内建的 symbol,被用来给转换方法命名,作为对象的属性名,属性值则是一个接收 hint
参数的函数,函数具体是转换对象为原始类型实现,如下:
obj[Symbol.toPrimitive] = function(hint) {
// 返回一个原始值
// hint = "string"、"number" 和 "default" 中的一个
}
比如,通过给 user
对象添加 Symbol.toPrimitive
属性的函数,实现自定义的原始类型转换:
const user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
console.log(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};
console.log(+user); // hint: number -> 1000
console.log(user + 500); // hint: default -> 1500
1.1.4 toString()/valueOf()
如果对象没有定义 Symbol.toPrimitive
,那么 JavaScript 在转换的时候就会尝试调用这两个方法:
-
hint
是 string,则先调用toString()
,再调用valueOf()
;如果toString()
返回的是原始值,则以该返回值作为结果,否则继续判断valueOf()
的返回值; -
hint
是 number,则先调用valueOf()
,再调用toString()
;如果valueOf()
返回的是原始值,则以该返回值作为结果,否则继续判断toString()
的返回值;
const user = {
name: "John",
money: 1000,
// hint="string"
toString() {
return `{name: "${this.name}"}`;
},
// hint="number" 或 "default"
valueOf() {
return this.money;
},
};
console.log(+user); // 1000
console.log(user + "1000"); // 1500
1.2 ToString
抽象操作 ToString 用于将非字符串强制转换为字符串,转换规则如下:
1.2.1 undefined
"undefined"
1.2.2 null
"null"
1.2.3 boolean
true
=> "ture",false
=> "false"
1.2.4 number
数字的转换遵循通用的规则,如下:
- 如果 number 是
NaN
,则转换为 "NaN" - 如果 number 是 +0 或 -0,则都转换为 "0"
- 如果 number 是 +∞,则转换为 "Infinity"
- 如果 number 是 非十进制的数字,则默认转换为十进制的数字字符串
- 如果 number 的十进制数超过 21 位,则会转换为指数形式的字符串
具体参考:https://262.ecma-international.org/12.0/#sec-numeric-types-number-tostring
1.2.5 symbol
symbol 无法转换为字符串,会直接抛出错误,throw TypeError
。
1.2.6 BigInt
- 如果 number 是 +0n 或 -0n,则都转换为 "0"
- 如果 number 是负数,则转换为带符号的字符串
- 其他情况下,均默认转换为十进制的数字字符串
1.2.7 object
先调用 ToPrimitive 方法,将 object 转换为基本类型的值,再继续调用 ToString 转换为字符串类型。
var a = [1, 2, 3];
a.toString(); // "1,2,3",数组的 `toString()` 是重新定义的,将所有单元字符串化后在用 `,` 连接起来
1.3 ToNumber
抽象操作 ToNumber 用于将非数字强制转换为数字类型。
1.3.1 undefined
NaN
1.3.2 null
0
1.3.3 boolean
true
=> 1
,false
=> 0
1.3.4 string
如果字符串中只包含数字(包括 Infinity),那么就转换为对应的数字。
如果字符串中只包含十六进制格式,那么就转换为对应的十进制数字。
如果字符串为空(包括空格),那么转换为 0。
如果字符串包含上述之外的字符,那么转换为 NaN。
1.3.5 symbol
symbol 无法转换为数字,会直接抛出错误,throw TypeError
。
1.3.6 BigInt
BigInt 无法转换为数字,会直接抛出错误,throw TypeError
。
1.3.7 object
先调用内部的 ToPrimitive 方法,将 object 转换为基本类型的值,再继续调用 ToNumber 转换为数字类型。
1.4 ToBoolean
抽象操作 ToBoolean 用于将非布尔值强制转换为布尔值。
1.4.1 undefined
false
1.4.2 null
false
1.4.3 number
如果数字是 0
、NaN
,则转换为 false
,其他转换为 true
。
1.4.4 string
如果是一个空的字符串,即 length
属性值是 0,则转换为 false
,其他转换为 true
。
1.4.5 symbol
true
1.4.6 BigInt
如果值是 0n
,则转换为 false
,其他转换为 true
。
1.4.7 object
true
2. 显示强制类型转换
显式强制类型转换就是手动地将一种值转换为另一种值。
常用地显式类型转换方法有 Number
、String
、Boolean
、parseInt
、parseFloat
、toString
、valueOf
等等。
2.1 字符串和数字的显示转换
字符串和数字之间的转换是通过 String(..)
和 Number(..)
两个内建函数,转换规则遵循 ToString
和 ToNumber
。
2.2 日期显示转换为数字
一元运算符 +
可以将日期强制转换为数字,返回 Unix 时间戳,以 ms 为单位(1970 年 1 月 1 日 00:00:00 UTC 到当前时间)。
var timestamp = +new Date(); // 1628824218581
// 其他获取时间戳的方法(推荐)
var timestamp = new Date().getTime();
var timestamp = Date.now();
2.3 ~ 运算符
~
运算符即字位操作 "非",也会触发强制类型转换。位运算符只适用于 32 位整数,通过抽象操作 ToInt32 进行强制转换为 32 位格式。
ToInt32 首先执行 ToNumber 强制类型转换,比如 "123" 会先被转换为 123,然后再执行 ToInt32。
~x 大致等同于 -(x + 1),通过此规律可以简化对 indexOf
的判断:
var a = "Hello World";
if (a.indexOf("lo") >= 0) {
// true
// 找到匹配!
}
// 简化
if (~a.indexOf("lo")) {
// true
// 找到匹配!
}
2.4 ~~ 运算
~~ 中的第一个 ~ 执行 ToInt32 并反转字位,然后第二个 ~ 再进行一次字位反转,即将所有字位反转回原值,最后得到的仍然是 ToInt32 的结果。
Math.floor(-49.6); // -50
~~-49.6; // -49
2.5 解析数字字符串
解析字符串中的数字(parseInt)和将字符串强制类型转换为数字(Number)的返回结果都是数字。但解析和转换两者之间有明显的差别:
var a = "42";
var b = "42px";
Number(a); // 42
parseInt(a); // 42
Number(b); // NaN
parseInt(b); // 42
parseInt
允许字符串中含有非数字字符,解析顺序从左到右,遇到非数字字符就停止解析。
Number
不允许存在非数字字符,不然就会转换失败返回 NaN。
parseInt(1 / 0, 19); // 18(1/0 得到 "Infinity",第一个是 I,以 19 为基数时值为 18,第二个 n 不是有效的,停止解析。)
parseInt(0.000008); // 0 ("0" 来自于 "0.000008")
parseInt(0.0000008); // 8 ("8" 来自于 "8e-7")
parseInt(false, 16); // 250 ("fa" 来自于 "false")
parseInt(parseInt, 16); // 15 ("f" 来自于 "function..")
parseInt("0x10"); // 16
parseInt("103", 2); // 2
2.6 转换为布尔值
通常建议使用 Boolean(a) 和 !!a 来进行显式强制类型转换。
3. 隐式强制类型转换
隐式类型转换一般是在涉及到运算符的时候才会出现的情况,比如将两个变量相加,或者比较两个变量是否相等。对于对象转原始类型的转换,会遵守 ToPrimitive 的规则。
3.1 字符串与数字之间的转换
对于 +
运算符,如果其中一个操作数是字符串或者可转换为字符串(操作数是对象,包括数组,首先对其调用 ToPrimitive 抽象操作该抽象操作再调用 [[DefaultValue]]
,以数字作为上下文),那么执行字符串拼接操作;否则执行数字加法。
var a = "42";
var b = 0;
var c = 42;
a + b; // "420"
c + b; // 42
var e = [1, 2];
var f = [3, 4];
e + f; // "1,23,4" // 数组通过 valueOf() 无法转换为基本类型值,所以会继续调用 toString()
对于 a + ""
(隐式) 和 String(a)
(显式) 转换字符串有一个细微的差别,根据 ToPrimitive 抽象操作规则,a + ""
会
调用 valueOf()
方法,然后再通过 ToString 抽象操作转换为字符串。而 String(a)
则会直接调用 ToString。
对于 - * /
运算符,在进行运算操作时,两边都会被强制转换为数字进行操作。
var a = [3];
var b = [1];
a - b; // 2
3.2 布尔值到数字的转换
布尔值转换为数字会被转换为 0 和 1,隐式转换的处理方法就是通过与数字进行操作。
var sum = 0;
sum + true + false;
3.3 隐式强制类型转换为布尔值
如下情况,非布尔值都会进行布尔值的隐式强制类型转换,并且遵从 ToBoolean 的抽象操作规则:
- if (..)语句中的条件判断表达式;
- for ( .. ; .. ; .. )语句中的条件判断表达式(第二个);
- while (..) 和 do..while(..) 循环中的条件判断表达式;
- ? :中的条件判断表达式;
- 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)
4. 宽松相等(==
)与严格相等(===
)
宽松相等(loose equals)== 和严格相等(strict equals)=== 都用来判断两个值是否“相等”,比较的结果是布尔值。
两者的区别在于:== 允许在相等比较中进行强制类型转换,而 === 不允许,也就是说在比较的时候,两者都会检查操作数的类型,如果操作数类型不同,== 会先进行强制转换为相同的类型再进行值的比较,而 === 则直接返回 false。
进行 ==
比较的时候,具体的类型转换如下:
4.1 字符串与数字的 ==
比较
- 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果
- 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果
也就是说,如果两者其中之一是数字,那么就会把另外一个字符串转换为数字比较。
var a = 42;
var b = "42";
a === b; // false,没有强制类型转换
a == b; // true,b 先转换为数字再进行比较
4.2 布尔值与其他类型的 ==
比较
- 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
- 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。
会先把布尔值转换为数字,再进行与其他值的比较。
var x = true;
var y = "42";
x == y; // false,先把 x 转换为数字,即 1。此时 x,y 类型仍不相等;再把 "42" 转换为数字 42,1 == 42,false。
4.3 null 和 undefined 的 ==
比较
- 如果 x 为 null,y 为 undefined,则结果为 true。
- 如果 x 为 undefined,y 为 null,则结果为 true。
null 与 undefined 进行宽松比较时,结果总是为 true。其他类型的值与 null 和 undefined 进行比较,总会返回 false。
4.4 对象与对象的 ==
比较
如果两个对象指向同一个值(引用)时,即视为相等,不发生强制类型转换;否则视为不相等。
4.5 对象与非对象的 ==
比较
- 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
- 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPrimitive(x) == y 的结果。
var a = 42;
var b = [42];
a == b; // true,[42] 首先调用 ToPromitive 抽象操作,返回 "42",变成 "42" == 42,然后 又变成 42 == 42,最后二者相等。
var a = "abc";
var b = Object(a); // 同 new String(a)
a === b; // false
a == b; // true,b 通过 ToPromitive 转换为基本类型值 "abc"
4.6 安全使用 ==
如下是一些比较特殊容易出错的情况:
false == 0; // true
false == ""; // true
false == []; // true
"" == 0; // true
"" == []; // true
"" == [null]; // true
"" == [undefined]; // true
0 == []; // true
0 == "\n"; // true,空格,换行符等都会转换为0
[] == ![]; // true
2 == [2]; // true
在实际使用中,应该遵循如下原则:
- 如果两边的值中有 true 或者 false,千万不要使用 ==
- 如果两边的值中有 []、"" 或者 0,尽量不要使用 ==
- 最好使用 === 来避免不经意的强制类型转换
5. 抽象关系比较
在进行 >
、<
等比较的时候,首先双方会调用 ToPrimitive 转换为基本类型
5.1 转换出现非字符串,根据 ToNumber 规则将双方强 制类型转换为数字来进行比较
var a = [42];
var b = ["43"];
a < b; // true
5.2 转换后双方都是字符串,按字母顺序来进行比较
var a = ["42"];
var b = ["043"];
a < b; // false
var a = { b: 42 };
var b = { b: 43 };
a < b; // false,因为 a,b 转换后都是 [object Object]
a > b; // false,同上
a == b; // false,// a,b 指向的不是同一个引用
a <= b; // true // 根据规范a <= b被处理为 b < a,将 b < a 的结果反转,即为 true
a >= b; // true // 同上