logo头像
Snippet 博客主题

数据类型

参考链接

语法和数据类型(MDN)
JavaScript 数据类型和数据结构(MDN)
JavaScript深入之参数按值传递

内置类型

JS 中分为七种内置类型,七种内置类型又分为两大类型:原始类型对象类型

原始(Primitive)类型

基本类型有六种,分别是:

首先原始类型存储的都是值,是没有函数可以调用的,比如 undefined.toString(),对于原始类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型

1
2
3
111.toString() // error
let a = 111 // 这只是字面量,不是 number 类型
a.toString() // 使用时候才会转换为对象类型

但是 ‘1’.toString() 是可以使用的。其实在这种情况下,’1’ 已经不是原始类型了,而是被强制转换成了 String 类型也就是对象类型,所以可以调用 toString 函数。

除了会在必要的情况下强转类型以外,原始类型还有一些坑。

其中 JS 的数字类型是浮点类型的,没有整型。并且浮点类型基于 IEEE 754标准实现, 在使用中会遇到某些 Bug,比如 0.1 + 0.2 !== 0.3。NaN 也属于 number 类型,并且 NaN 不等于自身。

对象(Object)类型

对象类型和原始类型的不同之处?函数参数是对象会发生什么问题?

在 JS 中,除了原始类型那么其他的都是对象类型了。对象类型和原始类型不同的是,原始类型存储的是,对象类型存储的是地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。

1
const a = []

对于常量 a 来说,假设内存地址(指针)为 #001,那么在地址 #001 的位置存放了值 [],常量 a存放了地址(指针) #001,再看以下代码

1
2
3
const a = [] 
const b = a
b.push(1)

当我们将变量赋值给另外一个变量时,复制的是原本变量的地址(指针),也就是说当前变量 b 存放的地址(指针)也是 #001,当我们进行数据修改的时候,就会修改存放在地址(指针) #001 上的值,也就导致了两个变量的值都发生了改变。

接下来我们来看函数参数是对象的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test(person) { 
person.age = 26
person = {
name: 'yyy',
age: 30
}
return person
}
const p1 = {
name: 'yck',
age: 25
}
const p2 = test(p1)
console.log(p1) // -> {name: "yck", age: 26}
console.log(p2) // -> {name: "yyy", age: 30}

分析以上代码

  • 首先,函数传参是传递对象指针的副本
  • 到函数内部修改参数的属性这步,我相信大家都知道,当前 p1 的值也被修改了
  • 但是当我们重新为了 person 分配了一个对象时就出现了分歧,请看下图

ef95196512985e80d3eaff87b1f36e46.png

所以最后 person 拥有了一个新的地址(指针),也就和 p1 没有任何关系了,导致了最终两个变量的值是不相同的。

Typeof

typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么?

typeof 操作符

typeof 对于原始类型,除了 null 都可以显示正确的类型

1
2
3
4
5
6
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,但是还会显示 undefined

typeof 对于对象,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型

1
2
3
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

对于 null 来说,虽然它是基本类型,但是会显示 object,这是一个存在很久了的 Bug

1
typeof null // 'object'

PS:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。

typeof vs instanceof

如果我们想判断一个对象的正确类型,这时候可以考虑使用 instanceof,因为内部机制是通过原型链来判断的,在后面我们也会自己去实现一个 instanceof。

1
2
3
4
5
6
7
8
9
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 判断原始类型的

1
2
3
4
5
6
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 也不是百分之百可信的。

如果我们想获得一个变量的正确类型,可以通过 Object.prototype.toString.call(xx)。这样我们就可以获得类似 [object Type] 的字符串。

判断是否未定义(undefined

  1. 严格相等和 undefined

你可以使用undefined和严格相等或不相等操作符来决定一个变量是否拥有值。在下面的代码中,变量x是未定义的,if 语句的求值结果将是true

1
2
3
4
5
6
7
var x;

if (x === undefined) {
// 执行这些语句
} else {
// 这些语句不会被执行
}

注意:这里是必须使用严格相等操作符(===)而不是标准相等操作符(==),因为 x == undefined 会检查x是不是null,但是严格相等不会检查。null不等同于undefined。移步比较操作符查看详情。

  1. Typeof 操作符和 undefined
1
2
3
4
var x;
if(typeof x === 'undefined') {
// 执行这些语句
}
  1. Void操作符和 undefined

void 操作符是第三种可以替代的方法

1
2
3
4
5
let a
// 我们也可以这样判断 undefined
a === undefined
// 但是 undefined 不是保留字,能够在低版本浏览器被赋值let undefined = 1// 这样判断就会出错// 所以可以用下面的方式来判断,并且代码量更少// 因为 void 后面随便跟上一个组成表达式// 返回就是 undefined
a === void 0

类型转换

首先我们要知道,在 JS 中类型转换只有三种情况,分别是:

  • 转换为布尔值
  • 转换为数字
  • 转换为字符串

0f1ea5870705aaea801a4b0a0674eb71.png

转Boolean

在条件判断时,除了 undefined, null, false, NaN, ‘’, 0, -0,其他所有值都转为 true,包括所有对象。

对象转原始类型

对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下:

  • 如果已经是原始类型了,那就不需要转换了
  • 调用 x.valueOf(),如果转换为基础类型,就返回转换的值
  • 调用 x.toString(),如果转换为基础类型,就返回转换的值
  • 如果都没有返回原始类型,就会报错

首先会调用 valueOf 然后调用 toString。并且这两个方法你是可以重写的。

1
2
3
4
let a = {
valueOf() {
return 0
}}

当然你也可以重写 Symbol.toPrimitive ,该方法在转基本类型时调用优先级最高。

1
2
3
4
5
6
7
8
9
10
11
let a = {
valueOf() {
return 0;
},
toString() {
return '1';
},
[Symbol.toPrimitive]() {
return 2;
}}
1 + a // => 3'1' + a // => '12'

四则运算符

加法运算符不同于其他几个运算符,它有以下几个特点:

  • 运算中其中一方为字符串,那么就会把另一方也转换为字符串
  • 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1
2
3
1 + '1' // '11' 
true + true // 2
4 + [1,2,3] // "41,2,3"
  • 对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,得到结果 ‘11’
  • 对于第二行代码来说,触发特点二,所以将 true 转为数字 1
  • 对于第三行代码来说,触发特点二,所以将数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3

对于加号需要注意这个表达式 ‘a’ + + ‘b’

1
2
3
'a' + + 'b' // -> "aNaN"
// 因为 + 'b' -> NaN
// 你也许在一些代码中看到过 + '1' -> 1 形式来快速获取 number 类型。

那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字

1
2
3
4
4 * '3' // 12 
4 * [] // 0
4 * [1, 2] // NaN
// 数组转数字,空数组为 0, 存在一个元素且为数字转数字,其它情况 NAN

比较运算符

  1. 如果是对象,就通过 toPrimitive 转换对象
  2. 如果是字符串,就通过 unicode 字符索引来比较
1
2
3
4
5
6
7
8
9
let a = { 
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true

在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。

== vs ===

== 和 === 有什么区别?

对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换。

对于 === 来说就简单多了,就是判断两者类型和值是否相同。