js数据类型有哪些?这个很基础的问题,被问到的人可能会想干嘛拿这么简单的弱智问题问我,这怎么会问倒我,不就是那几种么,但是深入下去我们经常会犯一些错误,下面会详细介绍js数据类型及常见的特殊情况,还有如何判断数据类型,数据类型之间如何转换等
原始类型有哪几种?null 是对象嘛?
在JS中,存在着7的原始(Primitive)类型,分别是:
首先原始类型存储的都是值,是没有函数可以调用的,比如 undefined.toString()
//在控制台打印
undefined.toString()
//输出结果
VM703:1 Uncaught TypeError: Cannot read property 'toString' of undefined
at :1:11
此时你肯定会有疑问,这不对呀,明明 ‘1’.toString() 是可以使用的。其实在这种情况下,==‘1’ ==已经不是原始类型了,而是被强制转换成了 ==String ==类型也就是对象类型,所以可以调用 toString 函数。
除了会在必要的情况下强转类型以外,原始类型还有一些坑。
其中 JS 的 number 类型是浮点类型的,在使用中会遇到某些 Bug,比如 0.1 + 0.2 !== 0.3,但是这一块的内容会在进阶部分讲到。string 类型是不可变的,无论你在 string 类型上调用何种方法,都不会对值有改变。
另外对于 null 来说,很多人会认为他是个对象类型,其实这是错误的。虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而== null== 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
BigInt 是一种内置对象,它提供了一种方法来表示大于 253 - 1 的整数。这原本是 Javascript中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。
可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数BigInt()。
在以往的版本中,我们有以下的弊端:
// 大于2的53次方的整数,无法保持精度
2 ** 53 === (2 ** 53 + 1)
// 超过2的1024次方的数值,无法表示
2 ** 1024 // Infinity
但是在ES10引入BigInt之后,这个问题便得到了解决。
以下操作符可以和 BigInt 一起使用: +、*、-、**、% 。除 >>> (无符号右移)之外的位操作也可以支持。因为 BigInt 都是有符号的,== >>>== (无符号右移)不能用于 BigInt。BigInt 不支持单目 (+) 运算符。
/ 操作符对于整数的运算也没问题。可是因为这些变量是 BigInt 而不是 BigDecimal ,该操作符结果会向零取整,也就是说不会返回小数部分。
BigInt 和 Number不是严格相等的,但是宽松相等的。
对象类型和原始类型的不同之处?函数参数是对象会发生什么问题?
在 JS 中,除了原始类型那么其他的都是对象类型了。对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。
const a = []
对于常量 a 来说,假设内存地址(指针)为 #001,那么在地址 #001 的位置存放了值 [],常量 a 存放了地址(指针) #001,再看以下代码
const a = []
const b = a
b.push(1)
当我们将变量赋值给另外一个变量时,复制的是原本变量的地址(指针),也就是说当前变量 b 存放的地址(指针)也是 #001,当我们进行数据修改的时候,就会修改存放在地址(指针) #001 上的值,也就导致了两个变量的值都发生了改变。
接下来我们来看函数参数是对象的情况
function test(person) {
person.age = 26
person = {
name: 'xyl',
age: 27
}
return person
}
const p1 = {
name: 'dxx',
age: 25
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
对于以上代码,你是否能正确的写出结果呢?接下来让我为你解析一番:
//输出结果如下
{name: "dxx", age: 26}
{name: "xyl", age: 27}
首先,函数传参是传递对象指针的副本
到函数内部修改参数的属性这步,我相信大家都知道,当前 p1 的值也被修改了
但是当我们重新为 person 分配了一个对象时就出现了分歧,请看下图
所以最后 person 拥有了一个新的地址(指针),也就和 p1 没有任何关系了,导致了最终两个变量的值是不相同的。
typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么?
判断 Target 的类型,单单用 typeof 并无法完全满足,这其实并不是 bug,本质原因是 JS 的万物皆对象的理论。因此要真正完美判断时,我们需要区分对待:
很稳的判断封装:
let class2type = {}
'Array Date RegExp Object Error'.split(' ').forEach(e => class2type[ '[object ' + e + ']' ] = e.toLowerCase())
function type(obj) {
if (obj == null) return String(obj)
return typeof obj === 'object' ? class2type[ Object.prototype.toString.call(obj) ] || 'object' : typeof obj
}
typeof 对于原始类型来说,除了 null 都可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // "object"
typeof 2 ** 1024 //Uncaught SyntaxError: Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence
typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
如果我们想判断一个对象的正确类型,这时候可以考虑使用 instanceof,因为内部机制是通过原型链来判断的,在后面的章节中我们也会自己去实现一个 instanceof。
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true
对于原始类型来说,你想直接通过 instanceof 来判断类型是不行的,当然我们还是有办法让 instanceof 判断原始类型的
class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string'
}
}
console.log('hello world' instanceof PrimitiveString) // true
你可能不知道 Symbol.hasInstance 是什么东西,其实就是一个能让我们自定义 instanceof 行为的东西,以上代码等同于 typeof ‘hello world’ === ‘string’,所以结果自然是 true 了。这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。
instanceof原理
能在实例的 原型对象链 中找到该构造函数的prototype属性所指向的 原型对象,就返回true。即:
// __proto__: 代表原型对象链
instance.[__proto__...] === instance.constructor.prototype
// return true
首先我们要知道,在 JS 中类型转换只有三种情况,分别是:
JS 中在使用运算符号或者对比符时,会自带隐式转换,规则如下:
-、*、/、% :一律转换成数值后计算
+:
- 数字 + 字符串 = 字符串, 运算顺序是从左到右
- 数字 + 对象, 优先调用对象的valueOf -> toString
- 数字 + boolean/null -> 数字
- 数字 + undefined -> NaN
[1].toString() === ‘1’
{}.toString() === ‘[object object]’
NaN !== NaN 、+undefined 为 NaN
在条件判断时,除了 undefined, null, false, NaN, ‘’, 0, -0,其他所有值都转为 true,包括所有对象。
对象在转换类型的时候,会调用内置的[[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下:
- 如果已经是原始类型了,那就不需要转换了
- 如果需要转字符串类型就调用 x.toString(),转换为基础类型的话就返回转换的值。不是字符串类型的话就先调用 valueOf,结果不是基础类型的话再调用 toString
- 调用 x.valueOf(),如果转换为基础类型,就返回转换的值
- 如果都没有返回原始类型,就会报错
当然你也可以重写 Symbol.toPrimitive ,该方法在转原始类型时调用优先级最高。
let a = {
valueOf() {
return 0
},
toString() {
return '1'
},
[Symbol.toPrimitive]() {
return 2
}
}
1 + a // => 3
加法运算符不同于其他几个运算符,它有以下几个特点:
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
如果你对于答案有疑问的话,请看解析:
另外对于加法还需要注意这个表达式 ‘a’ + + 'b’
‘a’ + + ‘b’ // -> “aNaN”
因为 + ‘b’ 等于 NaN,所以结果为 “aNaN”,你可能也会在一些代码中看到过 + ‘1’ 的形式来快速获取 number 类型。
那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
let a = {
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true
在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。