你不一定了解的js数据类型(一)
最近看到一个面试题,看的有点恶心,里面涉及了数据类型转换的一些知识,于是决定写一篇关于js数据类型以及数据类型转换的文章,里面主要涉及了js的数据类型、深浅拷贝、数据类型的强制转换。
# 先看下面试题
{} + [] // 0
[] + {} // "[object Object]"
({}+[]) // "[object Object]"
+[] // 0
+{} // NaN
[].toString() // []
({}).toString() // "[object Object]"
{}.toString() // Uncaught SyntaxError: Unexpected token .
0+[] // 0
0+{} // "0[object Object]"
可能讲的不那么深,但是我一定尽量写得细一点,把项目中遇到的,类似精度丢失的问题都尽量讲到。
因为这块的知识点太多了,所以这个地方还是只先讲js的基础数据类型,下一篇会继续聊引用数据类型,以及两种类型的强转换和隐性转换。
# 一、js的数据类型
之前在异步、同步的文章里,讲过了js是单线程语言,这与他的使命有关系,注定了他必须是单线程。
js除了是单线程语言,还是一门弱类型语言。
# (一)什么是弱类型语言?
面试的时候,有些面试官会让你谈一谈你对弱类型语言的理解,你可以这么回答:
弱类型语言就是一个变量在定义的时候,没有指定这个变量的类型,比如var一个变量a等于一个空字符串,它可以赋值一个数字,也可以赋值一个Boolean,并且可以进行数据类型转换的语言,就是弱类型语言,像PHP、Python、JavaScript都是弱类型语言。
# 弱类型语言的优势与劣势
# 优势
1、把一些复杂度比较高的地方,比如内存、指针等问题都给简单化了。其实编程就是人与机器交流,弱类型语言就是降低了人的难度,但是对机器就不友好了,很多逻辑都交给了机器。
2、复杂度降低了,项目开发的效率更高了,门槛也就越来越低了,这就能理解为什么这几年比较火的,PHP、Python、js,就是因为他们是弱类型语言,相对更容易上手。
# 劣势
劣势太多了,我随便列举几条吧: (但是这丝毫不影响PHP是全世界最好的语言)
1、你得到的有可能并不是你看到的,你定义的变量是不可预见类型的,并且可以改变;强类型语言在这方便就比较严谨,运行的程序更严谨一点,试想一下,像航天、军工这些行业,你敢用Python、PHP写吗?
2、因为弱类型语言相当多的处理,都交给了计算机,所以运行速度不如C这些强类型语言更快,更轻。
这不是重点,就不重点表了,面试的时候能扯一扯就行。
# (二)数据类型
我们学js的第一节课应该就是数据类型,这是js的基础,基础中的基础,但是你真的学懂了吗?
js的数据类型分为基本数据类型和引用对象类型,又叫原始类型和对象类型,反正就这么个意思,我更习惯前一种叫法。
# 两种类型的具体区别?
# 1、基本数据类型原始值是不可改的,最经典的栗子:
let str = 'huahua'
str[0] = 'g'
console.log(str) // 'huahua'
字符串所有的方法,返回的都是一个新的字符串,之前的字符串的不会变
let a = 1
let b = a
a = 2
console.log(a, b) // 2,1
从上面得栗子可以看出来,把a赋值给b,a怎么变,b还是原来的值,这也就是我们常说的赋值;
这个地方绕不过去的讲一下js的内存管理,一个变量在创建的时候,需要被存起来,存在哪儿呢?有两个地方,一个是堆内存、一个是栈内存:
# 栈内存
1、大小固定
2、空间小
3、运行效率高
4、系统自动分配空间
为了形象一点,结合第二个例子,自己用Excel画了个图,看着下面的图,我们分析一下栈内存:
可以看出来把a赋值给b后,js的栈内存会增加一个空间来存b,并不会影响原来的a,给a重新赋值以后,b并不会有任何改变。
简单点来说就是,有人分享给你一个小电影,你给down下来,他那部被封了并不影响你的观看;
# 2、引用数据类型的赋址,也是先看一个栗子:
let huahua = {
age: 20
}
let xiaohua = huahua
huahua.age = 25
console.log(xiaohua.age) // 25
从上面的栗子可以看出来,我们把huahua这个对象赋值给xiaohua,然后修改huahua的age属性,可以看出来,xiaohua的age也跟着变了,是不是很神奇,这就是引用数据类型的赋址;
为什么是赋址呢,因为引用数据类型不是存在栈内存里,而是存在堆内存里面:
# 堆内存
1、存储的值大小不定,可动态调整
2、空间较大,运行效率低
3、无法直接操作其内部存储,使用引用地址读取 // 重点
4、通过代码进行分配空间
还是用上面的例子结合图来分析一下堆内存:
这个可以看到把huahua赋值给xiaohua,其实赋给的是huahua的堆地址而已,他们的堆地址是一样的,都是指向的那一个堆,age变了以后,两个也都变了。
简单点来说就是,有人分享给你一个小电影,只能线上看的那种,他那部被封了那你也肯定看不了了;
所以数组的一些方法,像shift、unshift、pop、push之类的都可以改变原数组。
# 基本数据类型
js的基本数据类型一共六种:
null,代表了一个变量是空的
undefined,代表了一个变量未定义(通常情况下都是,变量定义未赋值的情况)
Boolean,包含两个值true、false
number,整数或者浮点数,还包括NaN(这个比较特殊)
String,这个比较好理解,就是一些字符
symbol:es6新增,一种实例是唯一且不可改变的数据类型(唯一且不可改变)
# 1、null和undefined
null和undefined有的时候会让人傻傻分不清楚,我喜欢这么理解这两个值:
null: 表示我们给一个变量刻意的赋值给null,空的,不应有值,至少当时是不应有值的,主动技能;
undefined: 未定义,意思是一个变量以后会有值,现在还没定义呢,被动技能;
需要注意的是:
undefined == null,但是undefined !== null
Number(undefined) // NaN
Number(null) // 0
!undefined === !null
后面我们会聊到Number方法和'==='、'!'等这些运算符,这个地方主要是证明这两个值相爱相杀,有很多一样的地方但又不一样
# 2、Boolean和string
字符串和布尔值,这两个没什么需要聊的
# 3、symbol
symbol是es6新增的一个基本数据类型,它有两个特点:
1、唯一
2、不可改变
这个symbol说实话,我没用过,只是知道有这么个东西,你可以把它理解为一个身份唯一的id,通常情况下我们都是通过Symbol()函数来创建一个symbol实例:
let s1 = Symbol()
或者你也可以传一个字符串,相当于给这个symbol加了一段描述
let anyS = Symbol('any Symbol')
let s1 = Symbol()
let s2 = Symbol()
console.log(s1 === s2, s1 == s2) //false, false
这说明symbol的实例是唯一的,当你比较两个symbol的时候是不相同的
# 应用场景
1、使用Symbol来作为对象属性名(key)
我们定义一个对象,传统方式是这样:
let obj = {
age: 18,
'name': 'wanghuahua'
}
obj.age // 18
obj['name'] // 'wanghuahua'
用symbol就可以这么干:
let PERSONAL_AGE = Symbol()
let PERSONAL_NAME = Symbol()
let obj = {
[PERSONAL_AGE]: 18
}
obj[PERSONAL_NAME] = 'wanghuahua'
obj[PERSONAL_AGE] // 18
obj[PERSONAL_NAME] // 'wanghuahua'
但是,用这个方法定义的对象属性名,用for...in循环和Object.keys是没有办法获取到的,可以通过Object.getOwnPropertySymbols或者是Reflect.ownKeys获取到。JSON.stringify()同样也不好用。
所以,利用这特点,当我们封装类的时候,就可以利用这个特性,来区分对内用的和对外暴露的对象。
2、需要记录一个不被覆盖的信息
公司部门新来了一个leader(或者妹纸),大家都打听他的消息,于是:
let leaderInfo1 = {
age: 43,
job: '技术总监',
name: '大秃',
desc: '阿里来的大神,据说听牛批的'
}
let leaderInfo2 = {
desc: '大哥人挺好的'
}
你要汇总这两段信息,于是:
let target = {}
Object.assign(target, leaderInfo1, leaderInfo2)
target: {
age: 43,
job: '技术总监',
name: '大秃',
desc: '大哥人挺好的'
}
之前的信息就没得了,那肯定不行,我们就可以这么来写:
let leaderInfo1 = {
age: 43,
job: '技术总监',
name: '大秃',
[Symbol('desc')]: '阿里来的大神,据说听牛批的'
}
let leaderInfo2 = {
[Symbol('desc')]: '大哥人挺好的'
}
这个时候汇总,就变成了:
target: {
age: 43,
job: '技术总监',
name: '大秃',
Symbol('desc'): '阿里来的大神,据说听牛批的',
Symbol('desc'): '大哥人挺好的'
}
这就很优雅了有没有
# 注册和获取全局Symbol
Symbol.for()
let s1 = Symbol.for('hhh') // 这个相当于注册了一个全局的Symbol
let s2 = Symbol.for('hhh') // 获取注册的Symbol
console.log(s1 === s2) // true
因为symbol是唯一的,所以需要先注册一个全局的symbol,才能去复制一个一模一样的。
Symbol.keyFor()
let s1 = Symbol.for('hhh')
Symbol.keyFor(s1) // 'hhh'
Symbol.keyFor方法返回一个已注册的Symbol类型的值的key。
对于我们平时的项目来说,这个symbol有点鸡肋,用处不是很大,了解上面的知识,对于symbol我觉得就够了。
# 4、Number(个人觉得number是重点)
javascript只有一种数字类型,它在内部被表示为64位的浮点数,和java的double数字类型一样。与其他大多数编程语言不同的是,它没有分离出整数类型,所以1和1.0的值相同。这提供了很大的方便,避免了一大堆因数字类型导致的错误。
javascript采用IEEE754格式来表示数字,不区分整数和浮点数,javascript中的所有数字都用浮点数值表示。
还是看下最经典的案例:
0.1 + 0.2 //0.30000000000000004
为什么呢?
1、把这个浮点数转成对应的二进制数,并用科学计数法表示
2、把这个数值通过IEEE 754标准表示成真正会在计算机中存储的值
那么0.1和0.2转换成二进制是什么样呢?
(0.1)10 => (00011001100110011001(1001)...)2
(0.2)10 => (00110011001100110011(0011)...)2
由于计算机只能保存最大53位精度,所以,用科学记数法表示
0.1的二进制为1.1001100110011001100110011001100110011001100110011010e+4(52位小数)
0.2的二进制为1.1001100110011001100110011001100110011001100110011010e+3(52位小数)
两者相加,最终的这个二进制数转成十进制就是0.30000000000000004
# 解决方案
实话说有两种方案,我觉得就用简单粗暴地第二种就行了
1、除2取余,逆序排列,直到商为0时为止
2、把小数放到位整数(乘倍数),再缩小回原来倍数(除倍数)
也就是说:
(0.1*10000 + 0.2*10000) / 10000
下一篇文章,将着重聊一下引用数据类型,以及js的强性类型转换和隐性类型转换。