无聊?来看看这篇有趣的类型转换,让你js基础上一个台阶!
开篇
- 类型转换的问题一直存在我们的身边,无论是在日常开发中,还是在面试里,类型转化你的操作都是必不可少的,时不时就需要我们主动进行强制类型转换或者隐式类型转换。
- 类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时。
- 在
JavaScript
中,通常将它们统称为强制类型转换,当然,你也可以理解为隐式强制类型转换
和显示强制类型转换
,二者的区别显而易见,我们能够从代码中看出哪些地方是显示强制类型转换,而隐式强制类型转换则不那么明显,通常是某些操作产生的副作用,例如:
var a = 77;
var b = a + ""; // 隐式强制类型转换
var c = String(a); // 显示类型转换
toString
toString()
方法返回一个表示该对象的字符串。该方法旨在重写(自定义)派生类对象的类型转换的逻辑。- 该方法由
字符串转换
优先调用,所有继承自Object.prototype
的对象都继承toString()
方法。
valueOf
OBject
的valueOf()
方法将this
值转换为一个对象。此方法旨在用于自定义类型转换的逻辑时,重写派生类。- 该方法由
数值转换
和原始值转换
优先调用,但是字符串转换
会优先调用toString()
,并且toString()
,并且toString()
非常有可能返回一个字符串类型,所以valueOf()
在这种情况下通常不会被调用。
Symbol.toPrimitive
Symbol.toPrimitive
是内置的symbol
属性,其指定了一种接受首选类型并返回对象原始值表示的方法。它被所有的强类型转换制
算法优先调用。- 在
Symbol.toPrimitive
属性(用作函数值)的帮助下,对象可以转换为一个原始值。该函数被调用时,会被传递一个字符串参数hint
,表示要转换到的原始值的预期类型。hint
参数的取值是"number"
、"string"
和"default"
中的任意一个。 - 以下示例描述了
Symbol.toPrimitive
属性如何修改从对象转换的原始值:
const object = {
[Symbol.toPrimitive](type) {
if (type === "number") return 77;
if (type === "string") return "string优先调用这里";
if (type === "default") return "default";
},
valueOf() {
return 22;
},
toString() {
return 33;
},
};
console.log(String(object)); // string优先调用这里 type 参数值是 "number"
console.log(Number(object)); // 77 type 参数值是 "string"
console.log(object + ""); // default type 参数值是 "default"
JSON字符串化
- 工具函数
JSON.stringify(...)
在将JSON
对象序列化为字符串时也用到了Tostring
。 JSON
字符串化和toString()
的效果基本相同,只不过序列化的结果总是字符串:
console.log(JSON.stringify(77)); // '77'
console.log(JSON.stringify("77")); // '77'
console.log(JSON.stringify(null)); // 'null'
console.log(JSON.stringify(undefined)); // undefined
JSON.stringify(...)
在对象中遇到undefined
、function()
和symbol
时会自动将其忽略,在数组中则会返回null
,例如:
console.log(JSON.stringify(undefined)); // undefined
console.log(JSON.stringify(function () {})); // undefined
console.log(JSON.stringify(class C {})); // undefined
console.log(JSON.stringify([1, undefined, function () {}, 4])); // [1,null,null,4]
console.log(JSON.stringify({ a: 2, b() {} })); // "{"a":2}"
console.log(JSON.stringify({ x: undefined, y: Object, z: Symbol("") })); // '{}'
- 如果对象中定义了
toJSON()
方法,JSON
字符串化时会首先调用该方法,然后用它的返回值进行序列化。 - 如果要对含有非法
JSON
值的对象做字符串化,或者对象中的某些值无法被序列化时,或需要定义toJSON()
方法返回一个安全的JSON
值,例如:
var obj = {};
var a = {
b: 77,
c: obj,
d: function () {},
};
// 在 a 中创建一个循环引用
obj.cycle = a;
// 循环引用在这里会产生错误
JSON.stringify(a);
- 对象包含循环引用的对象执行
JSON.stringify(...)
会出错。
var obj = {
foo: 11,
toJSON() {
return { b: 77 };
},
};
console.log(JSON.stringify(obj)); // "{"b":77}"
toJSON()
返回的是一个适当的值,可以是任何类型,然后再有JSON.stringify(...)
对其进行字符串化。也就是说,toJSON()
,应该返回一个能够被字符串化的安全的JSON
值,而不是返回一个JSON
字符串,例如:
var a = {
value: [1, 2, 3],
toJSON: function () {
return this.value.slice(1);
},
};
console.log(JSON.stringify(a)); // "[2,3]"
var b = {
value: [1, 2, 3],
toJSON: function () {
return "[" + this.value.slice(1).join() + "]";
},
};
console.log(JSON.stringify(b)); // ""[2,3]""
- 这里第二个函数是对
toJSON
返回的字符串做字符串化,而非数组本身。 - 我们可以向
JSON.stringify(...)
传递一个可选参数replacer
,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除,和toJSON()
很像。 - 如果
replacer
是一个数组,那么它必须是一个字符串数组,其中包含序列化要处理的对象的属性名称,除此之外其他的属性则被忽略。 - 如果
replacer
是一个函数,它会对对象本身调用一次,然后对对象中的每个属性各调用一次,每次传递两个参数,键和值。如果要忽略某个键就返回undefined
,否则返回指定的值:
var a = {
foo: 77,
bar: "moment",
baz: [1, 2, 3],
};
console.log(JSON.stringify(a, ["bar", "baz"])); // "{"bar":"moment","baz":[1,2,3]}"
console.log(
JSON.stringify(a, function (key, val) {
if (key !== "foo") return val;
})
); // "{"bar":"moment","baz":[1,2,3]}"
JSON.stringify(...)
并不是强制类型转换,因为它涉及ToSting
强制类型转换,具体表现在以下两点:
字符串
、数字
、布尔值
和null
的JSON.stringify(...)
规则于ToString
基本相同。- 如果传递给
JSON.stringify(...)
对象中定义了toJSON()
方法,那么该方法会在字符串化前调用,以便将对象转换为安全的JSON
值。
ToNUmber
- 有时我们需要将非数字值当做数组来使用,比如数字运算。为此 es5规范 给我们定义了抽象操作
ToNumber
,其基本语法为ToNUmber(argument)
,它在调用时执行以下步骤:
- 如果
argument
是数字类型,直接返回argument
; - 如果
argument
时Symbol
和Bigint
,则抛出一个TypeError(类型错误)
错误; - 如果
argument
是undefined
,返回NAN
; - 如果
argument
是null
和false
,则返回+0
; - 如果
argument
是true
,则返回1
; - 如果
argument
是一个string
类型,则返回一个StringToNumber(argument)
方法,如果argument
不是只包含数字的字符串,例如"1,2"
,那么Number
函数会将其转为 `NAN;
- 如果传入的是对象(包括数组),会首先被转换为相对应的基本类型,如果返回的是非数字的基本类型值,则再遵循以上
规则6
将其强制转为数字。 - 为了将帝乡转换为相对应的基本类型子,抽象操作 ToPrimitive会首先通过内部操作
[[DefaultValue]](hint)
,如果hint
参数是number
,会检查该值是否有valueOf()
方法,如果存在,设定value
是调用valueOf()
的内部[[Call]]
方法的返回结果,如果value
是原始值,则直接返回,就使用该值进行强制类型转换。如果没有valueOf()
方法就使用toString()
方法,也是跟valueOf()
方法相似,如果返回值存在,就对该值进行强制类型转换。 - 如果
valueOf()
和toString()
均不返回基本类型值,会产生TypeError
错误。 - 从 ES5 开始,使用
OBject.create(null)
创建的对象[[Prototype]]
属性为null
,所以也就没有valueOf()
和toString()
方法,因此无法进行强制类型转换。
var a = {
valueOf: function () {
return "66";
},
toString() {
return "77";
},
};
var b = {
toString() {
return "77";
},
};
var c = [2, 3];
c.toString = function () {
return this.join("");
};
console.log(Number(a)); // 66
console.log(Number(b)); // 77
console.log(Number(c)); // 23
console.log(Number("")); // 0
console.log(Number([])); // 0
console.log(Number([1, 2])); // NAN
console.log(Number(["a"])); // NAN
console.log(Number("12ab")); // NAN
- 在上面的例子中,数字的强制类型转换会优先调用
valueof()
方法,随后是toString()
方法。 - 在
c
变量中修改了数组的原型方法toString()
,使其返回一个字符串"23"
,然后对其强制类型转换。 - 对数组进行强制类型转换,会先调用数组的
toString()
方法,[]
会变成""
,[2,3]
会变成"2,3"
,所以遵循规则,空字符串会返回0
,而如果不是纯数字的字符串会返回NAN
。
ToBolean
JavaScript
中有两个关键词true
和false
,分别代表布尔类型中的真和假。- 在
ES规范
中定义了抽象操作ToBOlean
,该抽象操作接收一个参数argument
,并且遵循以下规则:
- 如果
argument
是一个Completion Record
类型,并且是一个abrupt completion
,直接返回argument
,否则返回ToBolean(argument.[[value]])
; - 如果
argument
是一个undefined
类型,返回false
; - 如果
argument
是一个Null
类型,返回false
; - 如果
argument
是一个Boolean
类型,直接返回argument
; - 如果
argument
是一个Number
类型,并且是+0
,-0
或者NAN
,返回false
,否则返回true
; - 如果
argument
是一个String
类型,并且是一个空字符串(字符串长度为0
),返回false
,否则返回true
; - 如果
argument
是一个Symbol
类型,返回true
; - 如果
argument
是一个Object
类型,包括[]
、{}
、function(){}
,都返回true
(注意,这里还有一个意外,请看规则9); - 如果
argument
是一个Object
类型,并且argument
拥有[[IsHTMLDDA]]
内部插槽,返回false
,例如document.all
;
console.log(Boolean(undefined)); // false
console.log(Boolean(null)); // false
console.log(Boolean(true)); // true
console.log(Boolean(+0)); // false
console.log(Boolean(-0)); // false
console.log(Boolean(NaN)); // false
console.log(Boolean(777));
console.log(Boolean("")); // false
console.log(Boolean("1")); // true
console.log(Boolean('""')); // true 长度不为0
console.log(Boolean(Symbol())); // // true
console.log(Boolean(function () {})); // // true
console.log(Boolean({})); // true
console.log(Boolean([])); // true
console.log(Boolean(document.all)); // false
Completion Record
Completion Record(完成记录)
是一种特殊的Record
,用于表示流程运行带特定步骤时的运行结果。例如我们控制台在输入var a = 2;
得到的结果却是undefined
。普通语句执行后会得到[[type]]
值为normal
的Completion Record
,所以普通语句执行完成之后就继续执行下一条,而只有表示式语句才会有[[value]]
值,var
语句执行得到的是一个[[vaue]]
值为空的Completion Record
。ES5规范
中可运行任何步骤、语句都会显示或隐式地返回一个Completion Record
,它具有特定的三个字段,如下图:
[[Type]]
字段,即当前Completion Record
的类型,它的值是normal
、return
、throw
、break
或者continue
五选一,表示这个Completion Record
是通过什么样的语句、步骤而生成的;- 如果
[[Type]]
是normal
、return
、throw
三者中的一个,那么这个Completion Record
的[[Value]]
会记录是生成正常值或者抛出的异常值,否则就是empty
; - 如果
[[Type]]
是break
或continue
,那么 [[Target]] 就包含控制流要转移的目标label
,类似于goto
语句。
- 当
Completion Record
的[[Type]]
是normal
时,我们就成这个Completion Record
是 normal completion 正常完成,否则,就称之为 abrupt completion 中断式完成。
显示强制类型转换
- 显示强制类型转换是那些显而易见的类型转换,很多类型转换都属于此类。
字符串和数字之间的显式转换
- 字符串和数字之间的转换是通过
String(...)
和Number(...)
这两个内建函数来实现的,但是它们前面没有new
关键字,并不创建封装对象。例如:
var a = 77;
console.log(String(a)); // "77"
var b = "3.14";
console.log(Number(b)); // 3,14
- 除了
String(...)
和Number(...)
意外,还有其他方法可以实现字符串和数字之间的显式转换:
var a = 77;
console.log(a.toString()); // "77"
var b = "3.14";
console.log(+b); // 3,14
a.toString()
是显式的,不过其中设计隐式转换。因为77
根本没有tostring()
的方法或者说toString()
对这些基本类型不适用,所以JavaScript
引擎会自动为77
创建一个封装对象,然后对该对象调用toString()
。这里显示转换中含有隐式转换,a.toString()
实际上是执行的以下代码:
var a = 77;
var aa = new String(a);
console.log(aa.toString()); // "77"
- 在上例中
+b
是+
运算符的一元形式(即只有一个操作数)。+
运算符显示地将b
转换为数字,而非数字加法运算,也不是字符串拼接。 - 一元操作符的其他操作,例如:
var a = "3.14";
var b = 3.14 + +a;
console.log(b); // 6.28
console.log(1 + - + + + - + 1); // 2
- 那么我们继续看看下面你的例子:
console.log(+[]); // 0
console.log(+["1"]); // 1
console.log(+["1", "2", "3"]); // NaN
console.log(+{});// NaN
- 上面的例子中
+[]
,首先调用valueOf()
方法,但是valueOf()
不存在,调用toString()
方法,返回""
,得到结果,然后对其调用ToNumber()
方法,""
对应的返回值是0
,所以返回0
,下面两个同样的道理。而{}
调用toString()
方法返回的是[object Object]
,对其调用ToNumber()
方法,返回的结果是NaN
。
显式解析数字字符串
- 解析字符串中的数字和将字符串强制类型转换为数字返回的结果都是数字。但是解析和转换两者之间还是有明显的差别的。例如:
var a = "77";
var b = "77px";
console.log(Number(a)); // 7
console.log(parseInt(a)); // 7
console.log(Number(b)); // Nan
console.log(parseInt(b)); // 7
- 解析运行字符串中含有非数字字符,解析从左侧到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败并返回
NaN
。 - 再看一个例子:
console.log(parseInt(1/0, 19)); // 18
- 咦,这怎么输出的18,难道不是
NaN
吗,这一定有问题,不行,我要重启电脑看看,再次打开电脑,发现依然是18
,在开机的过程中我已经想到了答案了,且听我一一道来:parseInt(1/0, 19)
实际上是parseInt("Infinity", 19)
。第一个字符是"I"
,以19为基数时值为18。第二个字符"n"
不是一个有效的数字字符,解析到此为止,所以输出的是15
。 - 现在一些看起来奇奇怪怪的但实际上解释的通的例子:
console.log(parseInt(0.000008)); // 0 (0 来自于"0.000008")
console.log(parseInt(0.0000008)); // 0 (0 来自于"8e-7")
console.log(parseInt(false, 16)); // 250 ("fa" 来自于 "false")
console.log(parseInt(parseInt, 16)); // 15 ("f" 来自于 "function")
console.log(parseInt("0x10")); // 16
console.log(parseInt("103", 2)); // 2 (3无效)
parseInt()
如果传入一个对象,如果该对象含有toString()
方法,则直接隐式调用用该方法,和前面说到的一样,把该方法返回值作为parseInt()
的参数传递:
var obj = {
a: 1,
toString() {
return this.a;
},
valueOf() {
return 2;
},
toJSON() {
return 3;
},
};
console.log(parseInt(obj)); // 1
显示转换为布尔值
- 与前面的
String(...)
和Number(...)
一样,Boolean(...)
是显示的ToBoolean
强制类型转换,虽然Boolean(...)
是显示的,但并不常用。 - 一元运算符
!
显式地将值强制类型转换为布尔值。但是它同时还将真值反转为假值(或者将假值反转为真值)。所以显式强制类型转换为布尔值最常用的方法是!!
,因为第二个!
会将结果反转回原值:
var a = "0";
var b = [];
var c = {};
var d = "";
var e = 0;
var f = null;
var g;
console.log(!!a); // true
console.log(!!b); // true
console.log(!!c); // true
console.log(!!d); // false
console.log(!!e); // false
console.log(!!f); // false
console.log(!!g); // false
隐式强制类型转换
- 隐式强制类型转换指的是那些隐藏的强制类型转换,副作用也不是很明显。换句话说,你自己不觉得不够明显的强制类型转换都可以算作隐式强制类型转换。
- 显示强制类型转换旨在让代码更加清晰易读,而隐式强制类型转换看起来就像是它的对立面,会让代码变得更晦涩。
字符串和数字之间的隐式强制类型转换
- 通过重载,
+
运算符既能用于数字加法,也能用于字符串拼接。JavaScript
怎样来判断我们要执行的是哪个操作?例如:
var a = "77";
var b = "0";
var c = 77;
var d = 0;
console.log(a + b); // 770
console.log(c + d); // 77
- 这里为什么会得到
"770"
和77
两个不同的结果呢?通常的理解是,因为某一个或者两个操作数都是字符串,所以+
执行的是字符串拼接操作。这样解释只对了一般,实际情况要复杂的多,例如:
var a = [1, 2];
var b = [3, 4];
console.log(a + b); // "1,23,4"
a
和b
都不是字符串,但是它们都被强制转换为字符串然后进行拼接,原因是什么呢?
- 根据上图
ES5规范
,我们来个简单的总结,主要有以下规则:
- 把第一个表达式
AdditiveExpression
的值赋值给左引用lref
; - 使用
GetValue(lref)
方法获取左引用lref
的计算结果,并赋值给左值lval
。 - 使用
ReturnIfAbrupt(lval)
返回运算结,如果lval
是abrupt completion
,则直接返回,如果是一个Completion Record
,那么则通过该记录的内部[[value]]
获取生成的是正常值还是抛出的异常值,否则就是empty
,empty
一般都为undefined
; - 右边的也是相同的步骤;
- 使用
ToPrimitive(lval)
获取左值lval
的原始类型,并将其赋值给左原生值lprim
; - 使用
ToPrimitive(lval)
获取左值rval
的原始类型,并将其赋值给右原生值rprim
; - 如果操作符左边或者操作符右边其中一个是
String
类型,则把另外一个非String
类型转换为String
类型,再进行字符串拼接。如果另外一个也是String
类型,则直接进行字符串拼接; - 把
ToNumber(lprim)
的结果赋值给左数字lnum
; - 把
ToNumber(rprim)
的结果赋值给左数字rnum
; - 返回左数字
lnum
和 右数字rnum
相加的数值;
- 然鹅,加法操作还有以下规范:
- 如果其中一个操作数是
NaN
,则返回结果便是NaN
; - 两个
Infinity
相加还是Infinity
; - 两个符号相反的
Infinity
相加是NaN
; Infinity
和一个有限值相加还是Infinity
;- 两个
-0
的和是-0
,两个+0
或两个符号相反的0
的和为+0
。 - 零和非零有限值之和等于非零操作数。
console.log(null + 1); // 1 Number(null) === 0
console.log(undefined + 1); // NaN Number(undefined)=== NaN
console.log(NaN + 1); // NaN
console.log(-Infinity + Infinity); // Infinity
console.log(Infinity + Infinity); // Infinity
console.log(9999999 + Infinity); // Infinity
console.log(-0 + 0); // 0
console.log(-0 + -0); // -0
- 在上面的数组相加的例子中,因为数组并没有
valueOf()
方法,于是转而调用toString()
。因此上例中的两个数组变成了"1,2"
和"3,4"
。+
操作符将它们进行了拼接,所以便返回了"1,23,4"
。 - 我们再来看看下面的例子:
var obj = {
valueOf() {
return 7;
},
toString() {
return "7";
},
};
var foo = {
valueOf() {
return 7;
},
toString() {
return "7";
},
};
var bar = {
toString() {
return "7";
},
};
console.log(foo + obj); // 14
console.log([1] + foo); // "17"
console.log(foo + bar); // 7
-
通过代码输出可以得出结论,两个对象相加,如果对象内部有
valueOf()
方法,会优先调用该方法,否则调用toString()
方法。剩下的继续遵循上面讲解的规则。 -
我们再看看下面的那几对奇葩:
console.log([] + {});// [object Object]
console.log({} + []); // [object Object]
- 通过查看输出,两者的结果一致,按照规范,它们是这样执行的:
[]
调用toString()
方法,返回""
,而{}
调用toString()
方法,返回的是[object Object]
,两者进行字符串拼接,于是有[object Object]
这样的输出。 - 但是这两个卧龙凤雏在浏览器上输出便产生了不同的结果,具体看下图:
- 这是因为
{}
被当成了一个独立的代码快运行了,所以{} + []
变成了+[]
,所以结果就变成了0
。 - 但是,对其加上一个括号,它又变回原样了。
- 最后一个例子:
console.log(new Date(2022, 11, 20) + 777); // Tue Dec 20 2022 00:00:00 GMT+0800 (中国标准时间)777
-
在这里
Date
是一个特例,如果其中一个操作数是对象,则对象则会遵循对象到原始值的转换规则,也就是首先调用valueOf()
,日期对象直接调用toString
方法转成字符串,其它对象先调用valueOf()
方法,所以该结果进行的是字符串拼接。 -
+
操作符讲完了,接下来就看看-
操作符,但是这个没+
操作符那么复杂,这个很简单,继续看规范。
- 和
+
操作符不同的是,-
操作符默认都是把左右值优先转成number
类型的,具体解释请看下面的代码:
console.log([1, 3] - 2); // Nan Number([1, 3]) - 2
console.log(null - 1); // -1 Number(null) - 1
console.log(undefined - 1); // NaN Number(undefined) - 1
console.log(77 - "7"); // 70 77 - Number("7")
console.log([3] - [1]); // 2 Number([3]) - Number([1])
console.log(new Date() - 1); // 1668914470081 new Date().valueOf() - 1
- 好了,突然看了一些字数,好像已经很多了,那么就分成两篇吧,这个大概要到星期二或者星期三才能完成,再继续写就要挂科了。
参考文献
转载自:https://juejin.cn/post/7167937724786638855