由于JavaScript
是弱类型语言,而且JavaScript
声明变量的时候并没有预先确定的类型,变量的类型就是其值的类型,也就是说「变量当前的类型由其值所决定」,夸张点说上一秒是String
,下一秒可能就是个Number
类型了,这个过程可能就进行了某些操作发生了强制类型转换。虽然弱类型的这种「不需要预先确定类型」的特性给我们带来了便利,同时也会给我们带来困扰,为了能充分利用该特性就必须掌握类型转换的原理。本文我们将深入了解JavaScript
的类型机制。
JS
内置数据类型有 8 种类型,分别是:undefined
、Null
、Boolean
、Number
、String
、BigInt
、Symbol
、Object
。
其中又可分为「基础类型」和「引用类型」。
undefined
、Null
、Boolean
、Number
、String
、BigInt
、Symbol
Object
类型。细分的话,有:Object
类型、Array
类型、Date
类型、RegExp
类型、Function
类型等。依据「存储方式」不同,数据类型大致可以分成两类:
可以通过以下栗子加深理解:
const obj1 = {
name: 'obj1',
id: '123'
}
const obj2 = obj1;
console.log(obj1.name); // obj1
obj2.name = 'obj2';
console.log(obj1.name); // obj2
console.log(obj2.name); // obj2
当obj2
的name
被修改后,obj1
的name
也随之改变,这里就体现了引用类型的“共享”的特性,即这两个值都存在同一块内存中共享,一个发生了改变,另外一个也随之跟着变化。
string
、number
、boolean
和 null
undefined
这五种类型统称为「原始类型」(Primitive
),表示不能再细分下去的基本类型。
ToPrimitive
对原始类型不发生转换处理,只「针对引用类型(object)的」,其目的是将引用类型(object
)转换为非对象类型,也就是原始类型。
toPrimitive(obj: any, preferedType?: 'string' |'number')
ToPrimitive
运算符「接受一个值,和一个可选的期望类型作参数」。ToPrimitive
运算符将值转换为非对象类型,如果对象有能力被转换为不止一种原语类型,可以使用可选的 「期望类型」 来暗示那个类型。
它内部方法,将任意值转换成原始值,转换规则如下:
preferedType
为string
:obj
的toString
方法,如果为原始值,则return
,否则进行第2步obj
的valueOf
方法,如果为原始值,则return
,否则进行第3步TypeError
异常preferedType
为number
:obj
的valueOf
方法,如果为原始值,则return
,否则进行第2步obj
的toString
方法,如果为原始值,则return
,否则第3步TypeError
异常preferedType
参数为空Date
,则type被设置为String
Number
接着,我们看下各个对象的转换实现:
「对象」 | 「valueOf()」 | toString() | 「默认 preferedType」 |
---|---|---|---|
Object | 原值 | "[object Object]" | Number |
Function | 原值 | "function func() {...}" or "() => {...}" | Number |
Array | 原值 | "a, b, c,..." | Number |
Date | 数字 | 例如:"Thu Nov 11 2021 19:49:37 GMT+0800 (中国标准时间)" | String |
toString()
可以等效为join(",")
,遇到null
, undefined
都被忽略,遇到symbol
直接报错,遇到无法ToPrimitive
的对象也报错。模板字符串
或者使用String()
包装时,preferedType=string
,即优先调用 .toString()
。例如:
[1, null, undefined, 2].toString() // '1,,,2'
// Uncaught TypeError: Cannot convert a Symbol value to a string
[1, Symbol('x')].toString()
// Uncaught TypeError: Cannot convert object to primitive value
[1, Object.create(null)].toString()
toString()
方法返回一个表示该对象的字符串。
每个对象都有一个 toString()
方法,当对象被表示为「文本值」时或者当以期望「字符串」的方式引用对象时,该方法被自动调用。
「【注】」toString()
和valueOf()
在特定的场合下会自行调用。
Object.prototype.valueOf()
方法返回指定对象的原始值。
JavaScript
调用 valueOf()
方法用来把对象转换成原始类型的值(数值、字符串和布尔值)。但是我们很少需要自己调用此函数,valueOf
方法一般都会被 JavaScript
自动调用。
不同内置对象的valueOf
实现:
下面来看几个栗子:
const Str = new String('123');
console.log(Str.valueOf());//123
const Num = new Number(123);
console.log(Num.valueOf());//123
const Date = new Date();
console.log(Date.valueOf()); //1637131242574
const Bool = new Boolean('123');
console.log(Bool.valueOf());//true
var Obj = new Object({valueOf:()=>{
return 1
}})
console.log(Obj.valueOf());//1
Number
运算符转换规则:
null
转换为 0undefined
转换为 NaN
true
转换为 1,false
转换为 0NaN
**【注】**对象这里要先转换为原始值,调用ToPrimitive
转换,type指定为number
了,继续回到ToPrimitive
进行转换。
接下来看几个栗子:
Number("0") // 0
Number("") // 0
Number(" ") // 0
Number("\n") // 0
Number("\t") // 0
Number(null) // 0
Number(false) // 0
Number(true) // 1
Number(undefined); // NaN
Number("x"); // NaN
Number({}); // NaN
String
运算符转换规则
null
转换为 'null'
undefined
转换为 undefined
true
转换为 'true'
,false
转换为 'false'
**【注】**对象这里要先转换为原始值,调用ToPrimitive
转换,type
就指定为string
了,继续回到ToPrimitive
进行转换。
接下来看几个栗子:
String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
String(1) // '1'
String(-1) // '-1'
String(0) // '0'
String(-0) // '0'
String(Math.pow(1000,10)) // '1e+30'
String(Infinity) // 'Infinity'
String(-Infinity) // '-Infinity'
String({}) // '[object Object]'
String([1,[2,3]]) // '1,2,3'
String(['koala',1]) //koala,1
ToBoolean
运算符转换规则
除了下述 6 个值转换结果为 false
,其他全部为true
:
undefined
null
-0
0
或+0
NaN
''
(空字符串)假值以外的值都是真值。其中包括所有对象(包括空对象)的转换结果都是true
,甚至连false
对应的布尔对象new Boolean(false)
也是true
接下来看几个栗子:
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true
字符串的自动转换,主要发生在字符串的「加法运算」时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。
遇到对象先执行ToPrimitive
转换为基本类型,然后按照基本类型的规则处理
// {}.toString() === "[object Object]"
1 + {} === "1[object Object]"
// [2, 3].toString() === "2,3"
1 + [2, 3] === "12,3"
[1] + [2, 3] === "1,2,3"
function test() {}
// test.toString() === "function test() {}"
10 + test === "10function test() {}"
加法过程中,遇到字符串,则会被处理为「字符串拼接」
上面的对象最后也都转成了字符串,遵循本条规则。接着来几个纯字符串的例子:
1 + "1" === "11"
1 + 1 === 2
1 + 1 + "1" === "21"
"1" + 1 === "11"
"1" + "1" === "11"
1 + "1" + 1 === "111"
对象字面量{}
在最前面则不代表对象
不是对象是什么?我们看看下面这个栗子:
// [].toString() === "";
// {}.toString() === "[object Object]";
[] + {} === "[object Object]";
// { // empty block } + [] => [].toString() => "" => Number("") => 0
{} + [] === 0;
{ a: 2 } + [] === 0;
先说 [] + {}
。一个数组加一个对象。加法会进行隐式类型转换,规则是调用其 valueOf()
或 toString()
以取得一个非对象的值(primitive value
)。如果两个值中的任何一个是字符串,则进行字符串串接,否则进行数字加法。[]
和 {}
的 valueOf()
都返回对象自身,所以都会调用 toString()
,最后的结果是字符串串接。[].toString()
返回空字符串,({}).toString()
返回"[object Object]"
。最后的结果就是"[object Object]"
。
然后说 {} + []
。看上去应该和上面一样。但是 {}
除了表示一个对象之外,也可以表示一个空的 block
。在 [] + {}
中,[]
被解析为数组,因此后续的+
被解析为加法运算符,而 {}
就解析为对象。但在{} + []
中,{}
被解析为空的 block
,随后的 +
被解析为正号运算符。即实际上成了:{ // empty block } + []
即对一个空数组执行正号运算,实际上就是把数组转型为数字。首先调用 [].valueOf()
。返回数组自身,不是primitive value
,因此继续调用[].toString()
,返回空字符串。空字符串转型为数字,返回0
,即最后的结果。
「【注】」{}+[]
如果被parse
成statement
的话,{}
会被parse
成空的block
,但是在需要被parse
成expression
的话,就会被parse
成空的Object
。所以{}+[]
和console.log({}+[])
的输出结果还不一样,因为参数列表只接受expression
。
Number
(「除了加法运算符,其他运算符都会把运算自动转成数值。」)1 + true === 2
1 + false === 1
1 + null === 1
1 + undefined // NaN
减法操作时,一律需要把类型转换为Number
,进行数学运算
3 - 1 === 2
3 - '1' === 2
'3' - 1 === 2
'3' - '1' - '2' === 0
// [].toString() => "" => Number(...) => 0
3 - [] === 3
// {}.toString() => "[object Object]" => Number(...) => NaN
3 - {} // NaN
+x 和 一元运算 + x 是等效的(以及- x),都会强制转换成Number
+ 0 === 0
- 0 === -0
1 + + "1" === 2
1 + + + + ["1"] === 2
// 负负得正
1 + - + - [1] === 2
// 负负得正
1 - + - + 1 === 2
1 - + - + - 1 === 0
1 + + [""] === 1
// ["1", "2"].toString() => "1,2" => Number(...) => NaN
1 + + ["1", "2"] // NaN
// 多出来的 + 是一元操作符,操作数是后面那个 undefined,Number(undefined) => NaN
("ba" + + undefined + "a").toLowerCase() === "banana"
==
的比较中,Number
优先于String
,下面以x == y
为例:x
,y
均为number
,直接比较ToPrimitive()
type为number
进行转换,再进行后面比较boolean
,按照ToNumber
将boolean
转换为1或者0,再进行后面比较x
为string
,y
为number
,x
转成number
进行比较if(obj)
, while(obj)
等判断时或者 「三元运算符」只能够包含布尔值// 条件部分的每个值都相当于false,使用否定运算符后,就变成了true
if ( !undefined && !null && !0 && !NaN && !'' ) {
console.log('true');
} // true
//下面两种情况也会转成布尔类型
expression ? true : false
!! expression
相等于、全等都需要对类型进行判断,当类型不一致时,宽松相等会触发隐式转换。下面介绍规则:
对象与对象类型一致,不做转换
{} != {}
[] != {}
[] != []
对象与基本类型,对象先执行ToPrimitive
转换为基本类型
// 小心代码块
"[object Object]" == {}
[] == ""
[1] == "1"
[1,2] == "1,2"
数字与字符串类型对比时,字符串总是转换成数字
"2" == 2
[] == 0
[1] == 1
// [1,2].toString() => "1,2" => Number(...) => NaN
[1,2] != 1
布尔值先转换成数字,再按数字规则操作
// [] => "" => Number(...) => 0
// false => 0
[] == false
// [1] => "1" => 1
// true => 1
[1] == true
// [1,2] => "1,2" => NaN
// true => 1
[1,2] != true
"0" == false
"" == false
null
、undefined
、symbol
null
、undefined
与任何非自身的值对比结果都是false
,但是null == undefined
是一个特例。
null == null
undefined == undefined
null == undefined
null != 0
null != false
undefined != 0
undefined != false
Symbol('x') != Symbol('x')
对比不像相等,可以严格相等(===
)防止类型转换,对比一定会存在隐式类型转换。
对象总是先执行ToPrimitive
为基本类型
[] < [] // false
[] <= {} // true
{} < {} // false
{} <= {} // true
任何一边出现「非字符串」的值,则一律转换成「数字」做对比
// ["06"] => "06" => 6
["06"] < 2 // false
["06"] < "2" // true
["06"] > 2 // true
5 > null // true
-1 < null // true
0 <= null // true
0 <= false // true
0 < false // false
// undefined => Number(...) => NaN
5 > undefined // false
typeof
操作符可以区分「基本类型」,「函数」和「对象」。
判断结果: 'string'
、'number'
、'boolean'
、'undefined'
、'function'
、'symbol'
、'bigInt'
、'object'
console.log(typeof null) // object
console.log(typeof undefined) // undefined
console.log(typeof 1) // number
console.log(typeof 1.2) // number
console.log(typeof "hello") // string
console.log(typeof true) // boolean
console.log(typeof Symbol()) // symbol
console.log(typeof (() => {})) // function
console.log(typeof {}) // object
console.log(typeof []) // object
console.log(typeof /abc/) // object
console.log(typeof new Date()) // object
缺点:
typeof
有个明显的bug就是typeof null
为object
;typeof
无法区分各种内置的对象,如Array
, Date
等。接下来讲简单介绍一下原理:
JS
是动态类型的变量,每个变量在存储时除了存储变量值外,还需要存储变量的类型。JS
里使用32位(bit)存储变量信息。低位的1~3个bit
存储变量类型信息,叫做类型标签(type tag
)
.... XXXX X000 // object
.... XXXX XXX1 // int
.... XXXX X010 // double
.... XXXX X100 // string
.... XXXX X110 // boolean
int
类型的type tag
使用1个bit,并且取值为1,其他都是3个bit, 并且低位为0。这样可以通过type tag
低位取值判断是否为int
数据;int
,还剩下2个bit,相当于使用2个bit区分这四个类型:object
, double
, string
, boolean
;null
,undefined
和Function
并没有分配type tag
。「如何识别Function
」
函数并没有单独的type tag
,因为函数也是对象。typeof
内部判断如果一个对象实现了[[call]]
内部方法则认为是函数。
「如何识别undefined
」
undefined
变量存储的是个特殊值JSVAL_VOID
(0-2^30),typeof
内部判断如果一个变量存储的是这个特殊值,则认为是undefined
。
#define JSVAL_VOID INT_TO_JSVAL(0 - JSVAL_INT_POW2(30))
「如何识别null
」
null
变量存储的也是个特殊值JSVAL_NULL
,并且恰巧取值是空指针机器码(0),正好低位bit的值跟对象的type tag
是一样的,这也导致著名的bug:
typeof null // object
有很多方法可以判断一个变量是一个非null
的对象,例如:
// 利用Object函数的装箱功能
function isObject(obj) {
return Object(obj) === obj;
}
isObject({}) // true
isObject(null) // false
语法:A instanceof B
, 即判断A
是否为B
类型的实例,也可以理解为B
的prototype
是否在A
的原型链上
Object.create({}) instanceof Object // true
Object.create(null) instanceof Object // false
Function instanceof Object // true
Function instanceof Function // true
Object instanceof Object // true
[] instanceof Array // true
{a: 1} instanceof Object // true
new Date() instanceof Date // true
// 对于基本类型,使用字面量声明的方式可以正确判断类型
new String('dafdsf') instanceof String // true
'xiaan' instanceof String // false, 原型链不存在
作为类型判断的一种方式,instanceof
操作符不会对变量object
进行隐式类型转换:
"" instanceof String; // false,基本类型不会转成对象
new String('') instanceof String; // true
对于没有原型的对象或则基本类型直接返回false
:
1 instanceof Object // false
Object.create(null) instanceof Object // false
B
必须是个对象。并且大部分情况要求是个构造函数(即要具有prototype
属性)
// TypeError: Right-hand side of 'instanceof' is not an object
1 instanceof 1
// TypeError: Right-hand side of 'instanceof' is not callable
1 instanceof ({})
// TypeError: Function has non-object prototype 'undefined' in instanceof check
({}) instanceof (() => {})
「原理:」
// 自定义 instanceof
function myInstanceof(obj, objType) {
// 首先用typeof来判断基础数据类型,如果是,直接返回false
if(typeof obj !== 'object' || obj === null) return false;
// getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(obj);
while(true) { //循环往下寻找,直到找到相同的原型对象
if(proto === null) return false;
if(proto === objType.prototype) return true;//找到相同原型对象,返回true
proto = Object.getPrototypeof(proto);
}
}
// 验证一下自己实现的myInstanceof是否OK
console.log(myInstanceof(new Array('2','3'), Array)); // true
console.log(myInstanceof(123, Number)); // false
console.log(myInstanceof(new Number(123), Number)); //true
对于 Object.prototype.toString()
方法,会返回一个形如 "[object XXX]"
的字符串
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('Miss U') // “[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(function(){}) // ”[object Function]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call(document) //[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"
Object.prototype.toString
不能区分基本类型的,只是用于区分各种对象;null
和undefined
不存在对应的引用类型,内部特殊处理了;「原理:」
每个对象都有个内部属性[[Class]]
,内置对象的[[Class]]
的值都是不同的("Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String"),并且目前[[Class]]
属性值只能通过Object.prototype.toString
访问。而Object.prototype.toString
内部先访问对象的Symbol.toStringTag
属性值拼接返回值的。
Object.prototype.toString
的内部逻辑:
undefined
, 则返回"[object Undefined]"
;null
, 则返回"[object Null]"
;Symbol.toStringTag
属性值subType
subType
是个字符串,则返回[object subType]
[[Class]]
属性值type
,并返回[object type]
最后,我们可以封装一个通用的类型检测方法:
function getPrototype(obj){
let type = typeof obj;
if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回
console.log(obj,':',res)
return type;
}
// 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
const res = Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1')
console.log(obj,'=',res);
return res;
}
getPrototype([]) // "Array" typeof []是object,因此toString返回
getPrototype('abc') // "string" typeof 直接返回
getPrototype(window) // "Window" toString返回
getPrototype(null) // "Null"首字母大写,typeof null是object,需toString来判断
getPrototype(undefined) // "undefined" typeof 直接返回
getPrototype() // "undefined" typeof 直接返回
getPrototype(function(){}) // "function" typeof能判断,因此首字母小写
getPrototype(/123/g) //"RegExp" toString返回