JS中的变量类型与类型转换
NOTE:文末有几个题目,看完这篇文章来检测一下吧
一、JS中的变量类型
Everything in JavaScript is either a primitive or object
- 原始类型:Bumber、String、boolean、Null、Undefined、Symbol、BigInt
- 引用类型:Object
其中,引用数据类型可统称为一种,在JS中万物皆对象,其他的如Function、Array、Date等都是基于Object的
二、拓展
1.包装对象
使用原始类型的变量时,为什么我们可以像使用引用数据类型一样直接调用属性和方法呢?这就涉及到"包装对象"。
- 定义
所谓"包装对象",指的是数值、字符串、布尔值分别相对应的Number
、String
、Boolean
三种原始值类型。这三个原生对象可以把原始类型的值变成(包装成)对象。
比如
js
"abc".length //3
- 自动转换
在Number、String、Boolean类型调用属性或方法的时候JavaScript 引擎自动将其转为包装对象,在这个对象上调用其属性或方法。调用结束后,这个临时对象就会被销毁。
2、Number类型中的几个特殊值
NaN
直译为 not a number。不过事实上NaN为Number类型,更贴切地可以描述为不是一个有效的数字。
js
Number("a") //NaN
1/"a" //NaN
0/0 //NaN
NaN的特殊行为:
- 如果
NaN
涉及数学运算(但不涉及位运算),结果通常也是NaN
。(参见下面的反例。) - 当
NaN
是任何关系比较(>
,<
,>=
,<=
)的操作数之一时,结果总是false
。 NaN
不等于(通过 ==、!=、===和 !==)任何其他值------包括与另一个NaN
值。
js
NaN === NaN; // false
Number.NaN === NaN; // false
isNaN(NaN); // true
isNaN(Number.NaN); // true
Number.isNaN(NaN); // true
Object.is(NaN,NaN) //true
Infinity
Infinity
表示"无穷",用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非0数值除以0,得到Infinity
。此外,其也有正负之分
js
1**1024 //Infinity
1/0 //Infinity
-1/0 //-Infinity
3、null与undefined
js
typeof(null) //"object"
typeof(undefined) //"undefined"
JavaScript的最初版本是这样区分的:null是一个表示"无"的对象,转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。
用法区别:
**null表示"没有对象",即该处不应该有值。**典型用法是:
(1) 作为函数的参数,表示该函数的参数不是对象。
(2) 作为对象原型链的终点。
**undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。**典型用法是:
javascript
(1)变量被声明了,但没有赋值时,就等于undefined。
(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。
(3)对象没有赋值的属性,该属性的值为undefined。
(4)函数没有返回值时,默认返回undefined。
二、显示类型转换
首先需要明确一点,JS中只存在三种类型转换
- to string
- to boolean
- to number
然后具体到不同的数据类型向这三种类型转换,如原始数据类型{string
, number
, boolean
, undefined
, null
} +和引用数据类型{object
, ... }
1 to string的原始类型转换
原始数据类型 <value>
通过String(<value>)
进行to string的类型转换规则表:
原始类型 | 类型转换规则 |
---|---|
string | 无需转换 |
number | "" |
boolean | "true" or "false" |
null | "null" |
undefined | "undefined" |
js
String(1) // 1
String(NaN) //"NaN"
String(Infinity) // Infinity
Note: number和boolean都为原始数据类型, 没有toString()方法, 但是仍然可以以点语法的形式调用toString(), javascript会自动将
<number | boolean>.toString()
解析为String(<number | boolean>)
, 进行显式类型转换
2 to number的原始类型转换
原始数据类型 <value>
通过Number(<value>)
进行to number的类型转换规则表:
原始类型 | 类型转换规则 |
---|---|
string | - ("", "<空白符>", "<多个空白符>") -> 0 - "<number>" -> <number> - "<16进制数>" -> <16 进制数> -> 10 十进制数 -其他字符串 → NaN |
number | 无需转换 |
boolean | true -> 1, false -> 0 |
null | 0 |
undefined | NaN |
3 to boolean的原始类型转换
原始数据类型 <value>
通过Boolean(<value>)
进行to number的类型转换规则:
- 转换为
false
""
0 || -0 || NaN
null
undefined
- 转换为
true
- 非空字符串
- 非 0 和非NaN 的数值
对象类型到boolean转换见4.1
NOTES:但这里有个特殊情况,即具有 [[IsHTMLDDA]] 属性的对象(例如document.all),其Boolean值为false,typeof为undefined
ECMA规范文档说明
If is an Object and has an [[IsHTMLDDA]] internal slot, return false.argumentargument
js
Boolean(document.all) // false
这个了解即可,
4 对象 to {string, number, boolean}的 To Primitive类型转换
在ES5版本 , 当我们显式或者隐式地将对象转换为原始值(primitive value)的时候, 通常是默认调用 对象自身或者原型链上的toString()
或者valueOf()
方法, 将其转换原始值. 或者可以自定义覆盖对象的这两个方法来控制对象的to primitive行为, 不过不建议这样做.
ES6 为开发者提供了官方的[Symbol.toPrimitve]
接口来自定义对象的to primitive操作, 当对象发生显式或者隐式类型转换操作的时候, 会自动调用其预先定义的[Symbol.toPrimitive]
方法, 同时会忽略对象自身的toString()
和valueOf()
方法.
在下面的内容, 将重点讨论ES6版本中, 对象发生类型转换时的方法调用逻辑. 主要划分为一下几块内容进行分析:
- 对象 to boolean
- 对象 to string | number
- 对象有自定义的
[Symbol.toPrimitive]
方法- 对象 to string
- 对象 to number
- 对象没有自定义的
[Symbol.toPrimitive]
方法- 对象 to string
- 对象 to number
- 对象有自定义的
4.1 对象 to boolean
关于对象转为布尔值的机制很简单, 一般情况下, 对象to boolean都是直接转换为true
, 而且不会调用对象的[Symbol.toPrimitive], toString, valueOf
这三个方法.
不过需要注意的是, 在<你不知道的JavaScript_中卷>4.2.3 节中提到, document.all
对象在现代浏览器中转为布尔值的时候为true
,而在老版本的IE浏览器中为false
. 这是一种极特殊情况,在vscode中使用这个对象的时候,会提示已经被弃用了,所以不用在意.
这里需要注意的点是空对象
其中,只有null表示空对象,故下列变量中的obj1
和obj2
转换为boolean后均为 true
js
let obj1 = {};
let obj2 = new Object();
let con1 = obj1.constructor;
let con2 = obj2.constructor;
console.log(con1);//ƒ Object() { [native code] }
console.log(con2);//f Object() { [native code] }
4.2 对象to string | number
对于对象发生类型转换时, 主要设计三个方法[Symbol.toPrimitive], toString, valueOf
我们需要明确调用时机顺序。
在ES5的时候, 对象的类型转换是通过内置或者自定义的toString, valueOf
方法进行to primitive类型转换的.
比如下面的代码中, 是对象 to string的类型转换.
js
const obj = {
toString() {
console.log('to string called') // to string called
return 1
},
valueOf() {
console.log('value of called')
},
}
let res = String(obj)
console.log(res) // "1"
console.log(typeof res) // string
对象<obj>
to string的类型转换的步骤为:
- 调用对象<obj>的toString()方法, 没有则去原型链上查找
- 如果toString返回值为原始值, 对返回值进行原始值 to string的类型转换, 转换后的结果即为对象 to string的类型转换结果
- 如果toString返回值为对象, 那么将调用<obj>的valueOf()方法
- 如果返回值为原始值, 对返回值进行原始值to string的类型转换, 转换后的结果即为对象 to string的类型转换结果
- 如果返回值为对象, 则报错 ❌
对象<obj>
to string是先调用toString()
, 并在返回值为对象的时候, 再调用valueOf()
.
而对象<obj>
to number的时候, 调用的顺序相反, 不过其他逻辑基本相同.
对象<obj>
to number的类型转换的步骤为:
-
调用对象
<obj>
的valueOf()方法, 没有则去原型链上查找-
如果valueOf返回值为原始值, 对返回值进行原始值 to number的类型转换(2.2 节), 转换后的结果即为对象 to string的类型转换结果
-
如果valueOf返回值为对象, 那么将调用
<obj>
的toString ()
方法
- 如果返回值为原始值, 对返回值进行原始值to number的类型转换, 转换后的结果即为对象 to number的类型转换结果
- 如果返回值为对象, 则报错 ❌
-
在es5的时候, 对象到原始值的转换结果依赖其toString()
和valueOf()
方法, 这两个方法可以自定义,也可以是原型方法. 一些JavaScript的内置对象会有自己的内置toString()
和valueOf()
方法, 位于其原型上, 汇总如下:
类型 | toString | valueOf |
---|---|---|
object | "[object <type>]" | 指向自身 |
function | 函数的字符串形式 | 指向自身 |
array | "arr0,arr1,..." 或者 ""(数组为空) | 指向自身 |
date | 包含本地时间信息的字符串 | 从1970年 1 月 1 日开始至今的毫秒数 |
regexp | 正则表达式的字符串表示形式 | 指向自身 |
error | 错误名+错误信息: "<err>.name:<err>.message" | 指向自身 |
总结:只有Date重写了自身的valueOf()方法,其余都是返回自身
除了Number、Boolean、String、Array、Date、RegExp、Function这几种实例化对象之外,其他对象返回的都是该对象的类,都是继承的Object.prototype.toString方法。
js
var obj = new Object({});
obj.toString(); // "[object Object]"
Math.toString(); // "[object Math]"
在ES6, 开发者可以通过官方提供的[Symbol.toPrimitive]
接口去定义对象 to primitive 的行为. 需要注意的是 , 如果自定义了对象的[Symbol.toPrimitive]
的方法, 那么, 当对象发生 to primitive类型转换的时候, 那么只会调用[Symbol.toPrimitive]
方法, 而无视ES5中提供的toString(), valueOf()
方法.
比如下面将对象显式转换为string的代码中, 只有[Symbol.toPrimitive]()
会被调用: 如果返回结果为对象, 则直接报错(不会去调用toString
或者是valueOf
); 如果返回结果为原始值, 则将将该原始值进行to string操作, 作为最终的对象 to primitive结果.
js
const obj = {
[Symbol.toPrimitive] () {
console.log("to primitive called") // to primitive called
// return {} error!!!
return 1
}
toString() {
console.log("to string called")
}
valueOf() {
console.log("value of called")
}
}
String(obj) // 对象 to primitive(string)
常用总结
八种假值
undefined
null
NaN
false
''
(empty string)0
-0
0n
(BigInt(0))
注意:" "(包含空格的字符串转换为布尔值为true)
js
!"" //true
!" " //false
三、隐式类型转换
注:symbol无法参与计算
对于某些运算符, 当A <operator> B
的时候, 如果A
和B
类型不一致, 那么将会触发隐式类型转换, 这些运算符汇总如下:
- 宽松相等运算符
==, !=
- 关系运算符
(>, <, <=, >=)
- 逻辑运算符
(&&, ||, !)
if, while, for, ? : + (condition)
中的条件表达式(condition)
- 加性运算符
+
- 算数运算符
(-, *, /, %)
- 一元
+, -
操作
1. 一元加号、减号
+
即强制转换为Number类型。直接参考显示类型转换中的to number
-
与+
类似,不过会将结果取负号
javascript
+"1" // 1
+true //1
+"" //0
+"1c" //NaN
+undefined //NaN
+null //0
+{a:1} //NaN
-"1" // -1
-"" // -0
+"1c" //NaN
-undefined //NaN
-null //-0
-{a:1} //NaN
2. 加法
分为两种:数值相加 和 字符串拼接
在求值时,它首先将两个操作数强制转换为基本类型。然后,检查两个操作数的类型:
- 如果有一方是字符串,另一方则会被转换为字符串,并且它们连接起来。
- 如果双方都是 BigInt,则执行 BigInt 加法。如果一方是 BigInt 而另一方不是,会抛出
TypeError
。 - 否则,双方都会被转换为数字,执行数字加法。
js
1 + "1" // "11"
1+{a:1} // "1[object Object]"
//1.{a:1}转换为 "1[object Object]"
//1 + "[object Object]" ,出现字符串,进行字符串拼接
3. 减法
注意和加法区分。减法不涉及字符串拼接,故需要都转换为Number类型
- 如果有一个操作数为string,boolean,null,undefined中一种,则在后台调用Number()将其转化为数值,再进行减法运算。
- 如果有一个操作数是对象 ,则调用对象的valueOf()方法取得表示该对象的数值。如果得到的值是NaN,减法结果是NaN。如果对象没有valueOf()方法,调用它的toString()方法,并将得到的字符串转化为数值.(参考上文一元加号的运算法则)
js
[]-1 //-1 过程[]->''->0 即相当于 0-1
[0]-1 //-1
[0,1]-1 //NaN 过程[0,1]->"0,1"->NaN 即相当于 NaN-1
{}-1 //NaN
4.相等判断
4.1宽松相等(==)
注意:特例NaN不等于自身
详细比较过程MDN文档与ECMAScript规范有一些初入,这里参考ECMASCript
ECMAScript® 2023 Language Specification (ecma-international.org)
If Type(x) is Type(y), then a. Return IsStrictlyEqual(x,y )
If x is null and is undefined, return true
If x is undefined and is null, return true
NOTE: This step is replaced in section B.3.6.2.(即下列步骤)
a. If is an Object, has an [[IsHTMLDDA]] internal slot, and is either null or undefined, return true
b. If is either null or undefined, is an Object, and has an [[IsHTMLDDA]] internal slot, return true
If x is a Number and y is a String, return ! IsLooselyEqual(x, ! ToNumber(y))
If x is a String and y is a Number, return ! IsLooselyEqual(! ToNumber(x),y )
If x is a BigInt and y is a String, then a. Let n be StringToBigInt(y). b. If n is undefined, return false. c. Return ! IsLooselyEqual(x,n )
If x is a String and y is a BigInt, return ! IsLooselyEqual(y,x )
If x is a Boolean, return ! IsLooselyEqual(! ToNumber(x), y)
If x is a Boolean, return ! IsLooselyEqual(x, ! ToNumber(y))
If x is either a String, a Number, a BigInt, or a Symbol and y is an Object, return ! IsLooselyEqual(x, ? ToPrimitive(y))
If x is an Object and y is either a String, a Number, a BigInt, or a Symbol, return ! IsLooselyEqual(? ToPrimitive(x),y )
If x is a BigInt and y is a Number, or if x is a Number and y is a BigInt a. If x is not finite or y is not finite, return false b. If ℝ(x) = ℝ(y), return true; otherwise return false
Return false.
补充一些名词的解释
[[IsHTMLDDA]] internal slot:拥有IsHTMLDDA作为属性。如document.all
ℝ():表示转换为数学上的值(+∞ 到 -∞),-0与+0相同,均为0。但-infinity与infinity不同
浅浅地翻译一下,大意为(有些地方稍作修改和调整)
js
1、若 Type(x) 与 Type(y) 相同, 则进行严格相等(===)的比较
2、若 x 为 null 且 y 为 undefined, 返回 true。
3、若 x 为 undefined 且 y 为 null, 返回 true。
4、这里只需记住document.all 与 null或undefined ,返回true即可。
5、若 Type(x) 为 Number 且 Type(y) 为 String,返回比较 x == ToNumber(y) 的结果。
6、若 Type(x) 为 String 且 Type(y) 为 Number,返回比较 ToNumber(x) == y 的结果。
7、若 Type(x) 为 String 且 Type(y) 为 BigInt,
使用与 BigInt() 构造函数相同的算法将字符串转换为 BigInt。如果转换失败,返回 false,然后再进行比较。
8、和第7步类似(顺序交换即可)
9、若 Type(x) 为 Boolean, 返回比较 ToNumber(x) == y 的结果。
10、若 Type(y) 为 Boolean, 返回比较 x == ToNumber(y) 的结果。
11、若 Type(x) 为 String 或 Number,且 Type(y) 为 Object,返回比较 x == ToPrimitive(y) 的结果。
12、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number, 返回比较 ToPrimitive(x) == y 的结果。
13、BigInt与Number类型比较
a.如果Number类型的值为无穷值(Infinity或-Infinity),直接返回false
b.如果两个值转换为数学上的值相同,返回true。(数学上-0与+0均为0)
14、返回 false
4.2严格相等(===)
类型必须相同。
特例:NaN不等于自身。
js
null == undefined //true
0 == false //true
null===undefined //false
0 === false //false
4.3 Object.is()
全等的缺陷
js
NaN === NaN //false
-0 === +0 //true
解决:使用Object.is()
js
Object.is(NaN,NaN) //true
Object.is(-0,+0) //false
Object.is()与===的区别
js
//实现Object.is()
//可观察Object.is()与===的区别
function equal(a,b) {
// 出现0、+0、-0
if(a===0||b===0){
return 1/a === 1/b
}
// 均为NaN
if(a!==a&&b!==b){
return true
}
return a===b
}
四、几个面试题
最后来几个面试题来测试一下吧
题目一
js
[] == !{}
1、! 运算符优先级高于==,故先进行!运算。
2、!{}运算结果为false,结果变成 [] == false比较。
3、根据4.1中的规则(引用类型与基本类型比较),等式左边ToPrimitive([]) == ''。
按照上面规则进行原始值转换,[]会先调用valueOf函数,返回this。
不是原始值,继续调用toString方法,x = [].toString() = ''。
故结果为 '' == 0比较。
5、等式左边为string,右边为number,等式左边x = ToNumber('') = 0。
所以结果变为: 0 == 0,返回true,比较结束。
题目二
js
let result = 100 + true + 21.2 + null + undefined + "Tencent" + [] + null + 9 + false;
//'NaNTencentnull9false'
js
//拆解
100 + true //100 + 1 = 101
101 + 21.2 // 122.2
122.2 + null // 122.2 + 0
122.2 + undefined //NaN
NaN + "Tencent" // "NaNTencent"
"NaNTencent" + [] // "NaNTencent" + ""
"NaNTencent" + null // "NaNTencent" + "null"
"NaNTencentnull" + 9 // "NaNTencentnull9"
"NaNTencentnull9" + false // "NaNTencentnull9false"
题目三
js
let arr= []
arr[0] = 1
arr["0"] = 2
arr["1"] = 3
arr[-1] = 4
arr[{}] = 5
console.log(arr);
浏览器打印结果
0: 2
1: 3
-1:4
[object Object]: 5
length: 2
解析:
- 知识点1:在js中,数组底层也是通过键值对存储的。当我们以索引形式为数组添加元素时,会发生以下过程。自动将传入的索引值转换为
CanonicalNumericIndexString
(规范数字索引字符串)- 将索引转换为数值类型
- 再将索引转换为字符串类型
- 最终添加至数组
故 arr[0]与arr["0"]事实上等价
- 知识点2:数组也是一个对象,可以添加属性。数组的索引范围为 [0,2^32-1) ,而向对象中添加属性时,若不在此范围,就相当于向其身上添加属性。
参考文章及资料
undefined与null的区别 - 阮一峰的网络日志 (ruanyifeng.com)
最全的javascript类型转换规则精简总结 - 掘金 (juejin.cn)
ECMAScript® 2023 Language Specification (ecma-international.org)