持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
前言
JS 是一门弱类型的语言,这使得 JS 相较于其他语言易入门、使用方便,在早期吸引大量的程序员,成为互联网的绝对霸主。
JS 的类型系统是松散的,所以在我们写代码期间不可避免的需要对类型加以限制。
尤其对于那些插件、工具库的开发者,无法预料他人传入函数的会是什么,所以对参数进行精确的类型检测或转换,更是保证工具能够正常运行必不可少的要求。
而且在面试当中,时不时也会考查类型转换的相关内容。
本文将给出各类型最精确的检测方法,以及各类型向其他类型转换时的默认规则与推荐用法。
类型检测
在本节,将介绍对于各类型的检测方法。
基础数据类型
JS 中有 7 种基础数据类型:
Undefined未定义类型Null空对象类型Boolean布尔类型Number数字类型String字符串类型Symbol符号类型BigInt大整数类型
在检测基础类型的时候,我推荐使用 typeof 运算符,目前 js 中不存在修改 typeof 的方法,所以其获得的结果一定是可靠的。
针对于 Null,建议直接使用 ===
console.log(typeof 0) // 'number'
console.log(typeof true) // 'boolean'
console.log(typeof '') // 'string'
console.log(typeof Symbol()) // 'symbol'
console.log(typeof 1n) // 'bigint'
console.log(typeof undefined) // 'undefined'
console.log(typeof null) // 'object'
console.log(null === null) // true
引用数据类型
如果仅仅是区分基础数据类型与引用数据类型,使用 typeof 再把 null 排除就够了
const obj = {}
console.log(typeof obj === 'object' && obj != null) // true
但我们更多的时候,我们是想更细微地区分引用类型,比如:
Object普通对象Array数组Function函数Date时间Set集合Map映射- ……
我推荐使用 Object.prototype.toString.call() 方法。
这样的写法是调用基类 Object 身上的 toString 方法,虽然所有对象都继承了基类,但很多对象都重写了自己的 toString 方法,比如 Array 和 Date,通过 call 调用能直接获取该对象所对应的类型标签。
console.log([].toString()) // '' (空字符串,调用的是数组的 join 方法)
console.log(new Date().toString()) // 'Mon May 23 2022 15:47:05 GMT+0800 (中国标准时间)'
console.log(Object.prototype.toString.call({})) // '[object Object]'
console.log(Object.prototype.toString.call([])) // '[object Array]'
console.log(Object.prototype.toString.call(new Function())) // '[object Function]'
console.log(Object.prototype.toString.call(new Date())) // '[object Date]'
console.log(Object.prototype.toString.call(new Set())) // '[object Set]'
console.log(Object.prototype.toString.call(new Map())) // '[object Map]'
对于自己定义的对象或类,可以通过修改其 Symbol.toStringTag 属性,来改变 Object.prototype.toString.call 的返回值。
class Student1 {
constructor() {}
get [Symbol.toStringTag]() {
return 'Student1'
}
}
function Student2() {
this[Symbol.toStringTag] = 'Student2'
}
const student1 = new Student1()
const student2 = new Student2()
const student3 = { [Symbol.toStringTag]: 'Student3' }
console.log(Object.prototype.toString.call(student1)) // '[object Student1]'
console.log(Object.prototype.toString.call(student2)) // '[object Student2]'
console.log(Object.prototype.toString.call(student3)) // '[object Student3]'
也正因如此,如果调用者故意隐瞒对象的类型,是没有办法检测出来的。对方甚至可以修改基础数据的类型。
const arr = []
arr[Symbol.toStringTag] = '我真的不是数组'
console.log(Object.prototype.toString.call(arr)) // '[object 我真的不是数组]'
console.log(Object.prototype.toString.call(1)) // '[object Number]'
Number.prototype[Symbol.toStringTag] = '猜猜我是什么类型'
console.log(Object.prototype.toString.call(1)) // '[object 猜猜我是什么类型]'
或许你会想到删除此属性来获取准确的类型,但对方可以轻松的阻止你对属性的删除。
const getType = (obj) => {
let tag = obj[Symbol.toStringTag]
delete obj[Symbol.toStringTag]
let res = Object.prototype.toString.call(obj)
obj[Symbol.toStringTag] = tag
return res
}
const arr1 = []
arr1[Symbol.toStringTag] = '我真的不是数组'
console.log(getType(arr1)) // '[object Array]'
const arr2 = []
Object.defineProperty(arr2, Symbol.toStringTag, {
get() {
return '我真的不是数组'
},
})
console.log(getType(arr2)) // '[object 我真的不是数组]'
总结
对于基础数据类型,可以通过 typeof 操作符准确获取其类型;对于引用数据类型,推荐使用 Object.prototype.toString.call 方法,但如果调用者刻意隐藏,我们是没办法准确获取其类型的。
类型转换
在本节,将介绍对于各类型相互转换时的默认规则,以及推荐使用的转换方法。
显示转换与隐式转换
类型转换分为显示类型转换与隐式类型转换
通过调用 String Number 等构造器实现的类型转换属于显示类型转换。
通过运算符 + - ! && 等,或在函数所需参数与传入参数类型不同时,进行的类型转换是隐式类型转换。
这两种转换方式在行为上几乎没有区别。
基础数据类型
Undefined 与 Null
其他类型无法向 undefined 或 null 类型转换。
当 undefined 与 null 向其他类型转换的结果如下,不过一般只会使用它们作为假值的特性,并不建议转换为布尔以外的类型。
let variate = undefined
console.log(Boolean(variate)) // false
console.log(Number(variate)) // NaN
console.log(String(variate)) // 'undefined'
console.log(Symbol(variate)) // Symbol()
console.log(BigInt(variate)) // TypeError
variate = null
console.log(Boolean(variate)) // false
console.log(Number(variate)) // 0
console.log(String(variate)) // 'null'
console.log(Symbol(variate)) // Symbol(null)
console.log(BigInt(variate)) // TypeError
Boolean
当其他类型向布尔类型转换时,只有假值(null undefined '' 0 -0 NaN 0n)会被转换为 false,其余的都为 true。
当布尔类型转换为其他类型的结果如下,但一般不建议向其他类型转换。
let variate = true
console.log(Number(variate)) // 1
console.log(String(variate)) // 'true'
console.log(Symbol(variate)) // Symbol(true)
console.log(BigInt(variate)) // 1n
let variate = false
console.log(Number(variate)) // 0
console.log(String(variate)) // 'false'
console.log(Symbol(variate)) // Symbol(false)
console.log(BigInt(variate)) // 0n
Number String 与 BigInt
将数字类型与字符串类型、大整数类型相互转换的需求是很常见的,需要注意的几点:
-
数字或大整数向字符串转换时,会默认使用
String方法,大整数末尾的 n 不会保留。 -
数字的数位过长时,会存在精度的缺失,而且在转换为字符串时,也可能会采用
e+的缩写形式,推荐使用大整数。 -
字符串向数字转换时,隐式转换会调用
Number方法,只要不符合数字规则,就会返回NaN,推荐使用parseInt与parseFloat方法。 -
字符串或数字向大整数转换时,隐式转换会抛出异常,而显示调用
BigInt方法必须传入一个能显示转换为整数的参数,否则也会抛出异常。 -
字符串与数字或大整数相加时,会统一转换为字符串进行拼接。
console.log(String(10 ** 30)) // '1e+30'
console.log(String(10n ** 30n))
// '1000000000000000000000000000000'
console.log(String(BigInt(10 ** 30)))
// '1000000000000000019884624838656'
console.log(Number('1a')) // NaN
console.log(Number('1.1.1')) // NaN
console.log(parseInt('1a')) // 1
console.log(parseFloat('1.1.1')) // 1.1
console.log(BigInt('1a')) // SyntaxError
console.log(BigInt(1.1)) // RangeError
console.log(1n - '1') // TypeError
Symbol
其他类型不会隐式地向符号类型转换,通过 Symbol 方法,会将内部的参数转换为字符串类型(undefined 除外),作为符号的描述。
符号向布尔类型转换为真;在向字符串隐式转换时会抛出异常,显示转换结果为 Symbol(description) 的形式;向数字或大整数类型转换时会抛出异常。
不建议转换符号类型。
let sy = Symbol('foo')
console.log(Boolean(sy)) // true
console.log(String(sy)) // 'Symbol(foo)'
console.log(sy + '') // TypeError
console.log(Number(sy)) // TypeError
console.log(BigInt(sy)) // TypeError
引用数据类型
各类型对象之间不会相互转换,主要是向基础数据类型转换,主要跟对象身上的 3 个转换方法有关,分别是 Symbol.toPrimitive valueOf toString
对象会按照一定规律尝试这些方法,直到得到了基础数据类型,如果没有得到基础数据类型,会抛出一个异常。
Symbol.toPrimitive 的优先级最高,如果对象配置了此方法,则仅会调用此方法。此方法会接受一个字符串参数,表示需要的类型(string number default)
如果对象没有 Symbol.toPrimitive 方法,会根据转换倾向,依次尝试valueOf 与 toString,如果需要的类型是字符串,优先尝试 toString,否则会优先尝试 valueOf
let obj1 = {
[Symbol.toPrimitive](hint) {
console.log(hint)
return {}
},
toString() {
return ''
},
valueOf() {
return 1
},
}
console.log(String(obj1)) // 'string' TypeError
console.log(Number(obj1)) // 'number' TypeError
console.log(obj1 + '') // 'default' 'TypeError
let obj2 = {
toString() {
return ''
},
valueOf() {
return 1
},
}
console.log(String(obj2)) // ''
console.log(Number(obj2)) // 1
console.log(obj2 + '') // '1'
Object.create(null) + '' // TypeError
转换倾向
转换倾向是当前表达式或参数所需的类型倾向,变量会根据转换倾向,进行类型的转换。
基础数据类型会一步到位的转换至所需的类型;对象会根据转换倾向按照次序调用身上的转化方法,如果得到的基础数据与所需的类型不同,会再按照基础类型转换的规则进行转换。
转换倾向一般由以下规则决定:
-
当传入参数与所需的类型不相符时,转换倾向为所需的参数类型;
-
通过
[XXX]形式访问对象属性时,转换倾向为字符串。 -
当对象参与运算时,除了
+运算符的倾向是default,其余都是number,均会优先调用valueOf方法; -
当只有基础数据类型参与运算时,仅当操作数中存在字符串且运算符为
+,转换倾向为字符串,否则均为数字。 -
位运算的转换倾向是数字,但对象参与位运算时,操作的是地址,并不进行类型转换
-
==两边操作数的转化比较复杂,可以看我之前这篇文章 细说 JS 中的 “==” 运算符
let obj = {
[Symbol.toPrimitive](hint) {
console.log(hint)
return ''
},
}
Symbol(obj) // 'string'
;[].slice(obj) // 'number'
;[][obj] // 'string'
obj+ '' // default
obj++ // 'number'
console.log(1 & '') // 0
console.log(1 & obj) // 1
习题
做点练习题巩固一下吧,看看这几段代码会输出什么吗
第一题
const res = 0.1 - '.1' + !-'1a' + '1e+1'
console.log(res)
第二题
const a = {
num: 1,
valueOf() {
return this.num++
},
}
const b = {
num: 4,
toString() {
return this.num++
},
}
const res = a == 1 && a == 2 && a == 3 && a == b
console.log(res)
第三题
const res = ([][[]] + [])[+!![]] + ([] + {})[!+[] + !![]]
console.log(res)
第四题
const a = {
[Symbol.toPrimitive](hint) {
if (hint === 'string') {
return 'aaa'
} else if (hint === 'number') {
return 10
} else {
return hint
}
},
valueOf() {
return 100
},
toString() {
return 'a'
},
}
const b = {
valueOf() {},
toString() {
return 'bbb'
},
}
const c = {
[Symbol.toStringTag]: 'c',
}
const res = `${a} ${a + b} ${-a + c}`
console.log(res)
第五题
console.log(123['toString'].length + 123)
解答
下面是这些题目的答案与解释
第一题
0.1 - '.1' + !-'1a' + '1e+1' 结果为 '11e+1'
我们从左往右分析
0.1 - '.1',.1 会使用 Number 转换为数字 0.1,前两项结果为 0
!-'1a',先将 1a 转换为数字 NaN,此为假植,取非得到 true,与 0 相加时转换为 1,前三项结果为 1
然后数字与字符串相加,直接拼接,结果为 '11e+1'
第二题
a == 1 && a == 2 && a == 3 && a == b 结果为 false
每次比较会对 a 进行类型转换,调用其 valueOf 方法,使它的 num 属性加 1,所以 a == 1 && a == 2 && a == 3 结果为true,但是两个对象进行比较时,不会进行类型转换,比的是地址,所以 a == b 返回 false,最终结果为 false
第三题
([][[]] + [])[+!![]] + ([] + {})[!+[] + !![]] 结果为'nb'
这个表达式有些复杂,咱们逐步将其拆解
首先根据中间的加号,拆解为前后两部分相加,前一部分为 ([][[]] + [])[+!![]],后一部分为 ([] + {})[!+[] + !![]]
-
对于前一部分
([][[]] + [])[+!![]],是对[][[]] + []表达式的结果取它的+!![]属性值。[][[]]又是对一个空数组取[]属性,[]作为属性调用toString方法返回'',空数组没有名为''的属性,该表达式返回undefined,又与[]相加,等同于undefined + ''结果是'undefined'对于表达式
+!![],!![]是将空数组转换为布尔值,结果为true,又前置+转换为数字,true转换为数字结果是1所以前一部分总的结果就是
'undefined'[1],字符串的第二位'n' -
对于后一部分
([] + {})[!+[] + !![]],是对[] + {}表达式的结果取它的!+[] + !![]属性值。[] + {},两对象相加,调用双方toString方法,等同于'' + '[object Object]',结果为'[object Object]'!+[] + !![],+[]等同于+'',结果为0,取非为true,后一部分也为true,true + true结果为2所以前一部分总的结果就是
'[object Object]'[2],字符串的第三位'b'
最终结果是两部分相加 'n' + 'b',也就是 'nb'
下图详细展示了整个过程
第四题
`${a} ${a + b} ${-a + c}` 结果为 'aaa defaultundefined -10[object c]'
a 配置了 Symbol.toPrimitive 方法,所以其在类型转换时仅会调用此方法,${a} 的转换倾向为字符串,结果为 'aaa',参与加法运算时转换倾向为默认,结果为 'default',前置符号时转换倾向为数字,结果为 10
b 配置了 valueOf 与 toString 方法,在运算时优先调用 valueOf,其返回值 undefined 为基础数据类型,在与前一项 'default' 相加组成 'defaultundefined'
c 配置了 Symbol.toStringTag 属性,在调用 Object 身上的 toString 方法返回的结果是 '[object c]',与前一项 -10 进行拼接,结果为 '-10[object c]'。
第五题
123['toString'].length + 123 结果为124
对于数字 123,获取它原型上的 toString,该方法的 length 为 1,与 123 相加结果为 124。
为什么会有这个结果,可以看我的这篇 面试败笔 | 函数的length
结语
如果文中有错误或不严谨的地方,请务必给予指正,十分感谢。
内容整理不易,如果喜欢或者有所启发,希望能点赞关注,鼓励一下新人作者。