这是《温故而知新系列总结》,开篇先来讲讲数据结构这个最基础最基础的东西,可能很多同学都没有认认真真研究过,写的不到位的地方还请大大们多多指教!
JavaScript
是一门神奇的语言,就连数据类型这个入门科目都够你捣鼓半天
- 本文参考了最新的
ECMAScript2022
规范,附上链接- ECMAScript® 2022 Language Specification (tc39.es)
一、数据类型分类
JavaScript
将数据类型分为7
种基本数据类型(Undefined,Null,Boolean,Number,String,Symbol,BigInt)
和1
种复杂数据类型(Object)
- 基本数据类型存放的是原始值
primitive value
- 复杂数据类型存放的是对象
an object
,即一个引用值reference value
,其中有一个原型对象prototype object
,可能为空(如顶级对象Object
的prototype object
即为空{}
)
1. typeof
操作符
typeof
并不是一个函数,而是一个操作符,后面接一个操作数(一个表示对象或原始值的表达式),返回操作数的类型
//两种写法,返回值以字符串表示
typeof operand
typeof(operand)
复制代码
比较特殊:注意这里返回的类型并不是
和数据类型一一对应的,下表进行一个总结
类型 | 值举例 | typeof 操作结果 |
---|---|---|
Undefined | undefined(仅此一值) | "undefined" |
Null | null(仅此一值) | "object" |
Boolean | true,false(仅此两值) | "boolean" |
Number | -0,+0,1,2.1,Infinity,NaN... | "number" |
BigInt | 10n,20n... | "bignit" |
String | "","jack","it is a joke"... | "string" |
Symbol | Symbol("aaa")... | "symbol" |
宿主对象 | Person创建的对象person1 | "Person"取决于具体实现 |
Function对象 | const foo = function(){} | "function" |
其他任何对象 | Array|Date|RegExp|Set|WeakSet|Map|WeakMap|Object|原始值包装对象... | "object" |
2. Undefined
只有一个值,就是undedined
2.1 声明未初始化
let a
console.log(a) //undefined
console.log(typeof a) //undefined
复制代码
2.2 未声明直接使用会报错
console.log(b) //ReferenceError: b is not defined
console.log(typeof b) //undefined
复制代码
建议: 即使未初始化的变量会被自动赋予
undefined
值,但我们仍然建议在声明变量的同时进行初始化。这样,当typeof
返回"undefined"
时,你就会知道那是因为给定的变量尚未声明,而不是声明了但未初始化
2.3 undefined
是一个假值
console.log(Boolean(undefined)) //false
复制代码
3. Null
只有一个值,就是null
3.1 表示空对象指针
console.log(typeof obj) //object
复制代码
3.2 null
是一个假值
console.log(Boolean(null)) //false
复制代码
3.3 undefined
是null
的派生用法
console.log(undefined == null) //true
复制代码
4. Boolean
只有两个值true\false
4.1 True
和False
并不是布尔值,而是标识符
4.2 其他类型转布尔
Boolean()
是转型函数,可以用于转换任意数据类型
//1.String
console.log(Boolean('')) //false
console.log(Boolean('li')) //true
//2.Number
console.log(Boolean(0)) //false
console.log(Boolean(NaN)) //false
console.log(Boolean(1)) //true
console.log(Boolean(Infinity)) //true
console.log(Boolean(-Infinity)) //true
//3.Object
console.log(Boolean({name: 'li'})) //true
//4.Undefined
console.log(Boolean(undefined)) //false
//5.Null
console.log(Boolean(null)) //false
复制代码
5. Number
日常使用数值没那么复杂,一般不会涉及到很多边界情况
5.1 如果数值本身是整数,小数点后面的0将被忽略
console.log(3.0) //3
复制代码
5.2 浮点数计算有精度丢失
这是IEEE754数值的通病,永远不要测定某个特定的浮点值
console.log(0.1 + 0.2) //0.30000000000000004
复制代码
5.3 值的范围
ECMAScript
可以表示的最小数值保存在Number.MIN_VALUE
中,这个值在多数浏览器中是 5e-324
;可以表示的最大数值保存在 Number.MAX_VALUE
中,这个值在多数浏览器中是 1.797 693 134 862 315 7e+308
,如果某个计算的数值超出了JavaScript
所能表示的范围,那么这个数值会被转换为正无穷+Infinity
和负无穷-Infinity
,这里我做了一个有意思的测试。
let max = Number.MAX_VALUE //1.7976931348623157e+308
let a = max + 1
console.log(isFinite(a)) //true
console.log(a) //1.7976931348623157e+308
复制代码
分析:按理来说,Number.MAX_VALUE+1
后已经超出了js数值表示范围,为什么结果不是Infinity
而依然是Number.MAX_VALUE
,我的个人理解是,1这个值在Number.MAX_VALUE
面前微乎其微被直接忽略了,那到底加一个多大的数才不会被忽略变为Infinity
呢,测试吧!!
let max = Number.MAX_VALUE //1.7976931348623157e308
let a291 = max + 1.7976931348623157e291
let b292 = max + 1.7976931348623157e292
console.log(isFinite(a291), isFinite(b292)) //true false
console.log(a291, b292) //1.7976931348623157e+308 Infinity
复制代码
可以看出:
a291
与Number.MAX_VALUE
相差了17
个数量级,b292
与Number.MAX_VALUE
相差了16
个数量级,因此数量级差大于等于17个数量级的数在Number.MAX_VALUE
面前将被忽略,数量级差小于等于16
个数量级的数在Number.MAX_VALUE
面前将被正常考虑,当然咱们老百姓一般不可能用到这么大的数值的,以上测试纯属娱乐,仅个人观点,如有错误还请大佬们指正。-Infinity
同理,只是多加了一个负号,表示负无穷。
针对Number.MIN_VALUE
来说,它是可表示的最小数,经过测试,它减去一个任意比它小的数之后得到的值依然是Number.MIN_VALUE
,使用isFinite()
始终返回true
let min = Number.MIN_VALUE
let min1 = min - 5e-500
let min2 = min - 5e-325
console.log(isFinite(min1), isFinite(min2)) //true true
console.log(min1, min2) //5e-324 5e-324
复制代码
这回清爽了,终于理解了什么叫可以表示的最大值和最小值,即使有些值肉眼上看是大于最大值,小于最小值,但其实js
计算后依然是这个值,如果值再大一步,那就变成了Infinity
5.4 其他类型转数值
5.4.1 Number()
Number
是转型函数,可以用于转换任意数据类型
//null
console.log(Number(null)) //0
//undefined
console.log(Number(undefined)) //NaN
//Boolean
console.log(Number(true)) //1
console.log(Number(false)) //0
复制代码
字符串的转换规则要稍微特殊一点
有几条规则总结了一下:空字符串返回0
,只要含有非进制字符返回NaN
,整数和浮点数前面忽略0
,显示进制数不忽略前面的0
,其他情况一律返回NaN
console.log(Number('')) //0
console.log(Number('a11')) //NaN
console.log(Number('011a11')) //NaN
console.log(Number('01.1a11')) //NaN
console.log(Number('011')) //11
console.log(Number('011.1')) //11.1
console.log(Number('0b11')) //3 二进制
console.log(Number('0o11')) //9 八进制
console.log(Number('0x11')) //17 16进制
复制代码
对象的转换,将调用valueOf
方法,再调用上述规则,如果转换结果为NaN
,则调用toString
方法再按字符串规则转换。这是红宝书上的叙述,目前我并未遇到NaN再转toString的情况,也没有想到相关的例子,如果有大佬知道,还请赐教
简单分析两个示例
//a是一个对象Boolean{false},调用valueOf方法返回false
let a = new Boolean('')
//等价console.log(Number(a.valueOf())) <=>console.log(Number(false))
console.log(Number()) //0
let b = new Date()
console.log(Number(b))//得到的是当前时间的毫秒数
复制代码
5.4.2 parseInt
可接收两个参数,第一个参数为待转换的字符串,第二个参数为转换的进制,当有第二个参数时,可省略第一个参数中的进制表示0b,0o,0x
parseInt()
函数更专注于字符串是否包含数值模式。字符串最前面的空格会被 忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()
立即返回 NaN
。这意味着空字符串也会返回NaN
(这一点跟 Number()
不一样,它返回 0
)。如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。parseInt()
函数也能识别不同的整数格式(十进制、八 进制、十六进制)
console.log(parseInt('')) //NaN
console.log(parseInt('a1a11')) //NaN
console.log(parseInt('1a11')) //1
console.log(parseInt(' -11.1')) //-11
console.log(parseInt('0x13')) //19
console.log(parseInt('13', 16)) //16进制 19
复制代码
5.4.3 parseFloat
类似parseInt
,它只认第一个小数点,对于字符串开头的0
如果数值是0.xxx
则不忽略,如果是非十进制数则直接返回0
,因此它只能解析十进制值,不能指定底数(进制数),如果小数点后只有一个0
则也会返回整数
console.log(parseFloat(' a0.11a')) //NaN
console.log(parseFloat(' 01.1.1aa)) //1.1
console.log(parseFloat(' -0.1a')) //-0.1
console.log(parseFloat(' 0b11')) //0
console.log(parseFloat(' 02.0a')) //2
复制代码
5.5 NaN
表示不是一个数值,not a number
,本来要返回数值的操作失败了
console.log(0 / 0) //NaN
console.log(1 / 0) //Infinity
console.log(1 / -0) //-Infinity
复制代码
-
任何涉及
NaN
的操作都返回NaN
-
NaN
不等于包括NaN
在内的任何值console.log(NaN == NaN) //false 复制代码
-
isNaN(param)
用于检验这个参数是否不是数值
,把一个值传给isNaN
后,该函数会尝试将它转换为数值,能转为数值则返回false
,不能转换为数值则返回true
//能转换为数值的有 字符串数值如'10.1'或布尔值 //任何不能转换为数值的值都会导致该函数返回true console.log(isNaN(true)) //1---false console.log(isNaN(false)) //0---false console.log(isNaN('10.1')) //10.1---false console.log(isNaN('1')) //1---false console.log(isNaN(NaN)) //true console.log(isNaN('blue')) //true console.log(isNaN(undefined)) //true console.log(isNaN(null)) //0---false 复制代码
6. BigInt
Number
中有最大最小安全整数,超出他们的范围的数值运算被认为是不安全的,为解决这个问题,就产生了BigInt
,它一一种内置对象,typeof
操作符返回"bigint"
,并不是引用类型,而是存的原始值,使用在整数字面量后面加n
的方式来表示,或者调用函数BigInt()
,不可与new
操作符配合使用,因为他构造出的不是对象实例,并不是引用类型
//BigIntConstructor. (value: bigint | boolean | number | string): bigint
let a = 1n
let b = BigInt(true)
console.log(typeof a) //bigint
console.log(a, b) //1n 1n
复制代码
6.1 操作符运算
以下操作符可以和 BigInt
一起使用: +
、-
、*
、/
、**
、%
。除 >>>
(无符号右移)之外的也可以支持。因为 BigInt
都是有符号的, >>>
(无符号右移)不能用于 BigInt
。
console.log(1n + 1n) //2n
console.log(1n - 2n) //-1n
console.log(1n * 2n) //2n
console.log(5n / 2n) //2n 向下取整,带小数的部分会被舍弃
console.log(1n % 2n) //1n
console.log(3n ** 2n) //9n
console.log('程序员最喜欢的数字是: ', 1n << 10n) //程序员最喜欢的数字是: 1024n
console.log(10n >> 2n) //2n 分析:10n / (2n**2n) <=> 10n / 4n <=> 2n
复制代码
6.2 比较
6.2.1 与Number相比较宽松相等,不严格相等
console.log(1n == 1) //true
console.log(1n === 1) //false
复制代码
6.2.2 可以和Number进行大小的比较
console.log(1n < 1) //false
console.log(2n >= 2) //true
复制代码
6.3 toString和valueOf
覆盖Object
原型上的对应方法,toString()
将返回以基数表示的字符串,valueOf()
返回指定的bigint原始值
console.log(1n.toString()) //1
console.log(1n.valueOf()) //1n
复制代码
7. String
这个应该是使用最频繁的数据类型了,表示零个或多个
16
位Unicode
字符序列,三种表示方法:双引号"a"
,单引号'a'
,反引号(``)
7.1 转义字符
以\
开头表示类似回车、换行等操作,与其他编程语言类似
\xnn
表示以十六进制编码 nn
表示的字符(其中 n
是十六进制数字 0~F
),例如\x41
等于"A"
\unnnn
表示以十六进制编码 nn
表示的Unicode
字符(其中 n
是十六进制数字 0~F
),例如\u03a3
等于"Σ"
,emoji
的Unicode
编码为E63E
到E757
7.2 其他类型转换为字符串
7.2.1 String()转型函数
null\undefined
分别返回null\undefined
,其他类型参考下面的toString
方法
console.log(String(null)) //null
console.log(String(undefined)) //undefined
复制代码
7.2.1 toString()方法
null\undefined
没有该方法,数值、布尔值、对象和字符串都有该方法,数值、布尔值、和字符串在使用该方法是其实是自动转为了包装对象来使用的,后面会细说,对象直接调用其toString
方法,有的重写了有的没有重写,没有重写的toString
返回[object type]
let a = 1
console.log(a.toString()) //1
console.log(true.toString()) //true
console.log(false.toString()) //false
//对象
let obj1 = {
'': 'a' //键可以为空哦,不过一般不这么用
}
console.log(obj1.toString()) //[object Object]
let obj2 = {
'': 'obj2',
toString() {
return this['']
}
}
console.log(obj2.toString()) //[object Object]
复制代码
当数值使用该方法时可以添加底数,即添加一个进制数参数,表示即将转换为什么进制
let a = 10
console.log(a.toString()) //10
console.log(a.toString(2)) //1010
console.log(a.toString(8)) //12
console.log(a.toString(16)) //a
复制代码
7.3 模板字符串
ES6新增语法,使用**``**来表示字符串,内部将保留换行符,可以跨行定义字符串
let s = `
hello`
console.log(s.length) //6
console.log(s[0] === '\n') //true
console.log(s[1] === ' ') //true
复制代码
7.3.1 插值语法
字符串插值通过在${}
中使用JavaScript
表达式实现,可以使用任何JavaScript
表达式,会将最后结果强制转换为字符串
let a = 1
let s = `the number is ${a}`
console.log(s) //the number is 1
复制代码
7.3.2 标签函数
标签函数tag function
的理解:
接收到的第一个参数:原始字符串数组,指的是因插值语法分隔开的字符串数组,包含前后的空字符串,后序的参数是每个插值表达式的求值结果。
function foo(x, y, z) {
console.log(x) //[ 'hello', 'world', '' ]
console.log(y) //second param
console.log(z) //third param
return 'foo function'
}
const res = foo`hello${'second param'}world${'third param'}`
console.log(res) //foo function
复制代码
这玩意可能在react中的利用率会比较高吧,暂时尚未涉足,后面考虑学一波
7.3.3 原始字符串
可以通过模板字符串获取原始的字符串内容,包括换行符或Unicode
字符,使用默认的标签函数String.raw
,标签函数的第一个参数有一个raw
属性可以获取每个字符串的原始内容
function foo(str) {
console.log('实际显示的字符串为:')
for (const s of str) {
console.log(s)
}
console.log('原始值为:')
for (const s of str.raw) {
console.log(s)
}
}
foo`\u00a9${'none of my business!'}\n`
//实际显示的字符串为:
// [ '©', '\n' ]
// ©
// (这里是一个换行符)
//
// 原始值为:
// \u00a9
// \n
复制代码
8. Symbol
基本数据类型,Symbol([description])
函数会返回Symbol
类型的唯一值,即使可选描述符一样,生成的符号也不相等,可以作为对象属性的标识符,这也是该数据类型诞生的使命。
let s1 = Symbol()
let s2 = Symbol('foo')
let s3 = Symbol('foo')
console.log(s1, s2, s3) //Symbol() Symbol(foo) Symbol(foo)
console.log(s2 === s3) //false
console.log(s2.description) //foo
复制代码
8.1 全局共享的Symbol
使用Symbol.for()
和Symbol.keyFor()
方法来创建和获取symbol
,这种方法创建的符号可以跨文件使用
let s1 = Symbol.for('foo')
let des1 = Symbol.keyFor(s1)
console.log(des1) //foo
let s2 = Symbol.for('foo')
console.log(s1 === s2) //true
复制代码
8.2 属性描述符的3中使用方式
const s1 = Symbol('s1_Symbol')
const s2 = Symbol('s2_Symbol')
const s3 = Symbol('s3_Symbol')
/**
* 1. 定义字面量直接使用,使用计算属性
*/
const obj = {
name: 'dd',
age: 19,
[s1]: 'abc',
}
/**
* 2.属性名赋值,使用类似索引的赋值方式
*/
obj[s2] = 'bcd'
/**
* 3.使用Object.defineProperty
*/
Object.defineProperty(obj, s3, {
configurable: true,
enumerable: true,
writable: true,
value: 'cde',
})
console.log(obj)
/**
{
name: 'dd',
age: 19,
[Symbol(s1_Symbol)]: 'abc',
[Symbol(s2_Symbol)]: 'bcd',
[Symbol(s3_Symbol)]: 'cde'
}
*/
复制代码
8.3 遍历
对象中的普通属性都可以通过in
和for...in
得到,但是Symbol比较特殊,没法通过他们得到,必须使用Object.getOwnPropertySymbols()
方法来获取,这是一个专门的方法。
console.log(Object.getOwnPropertySymbols(obj)) //此处的obj为8.2中的obj
//[ Symbol(s1_Symbol), Symbol(s2_Symbol), Symbol(s3_Symbol) ]
复制代码
9. Object
对象通过new
操作符后面跟上一个对象类型名称来常见,类似java
,但是跟java
又不一样,因为JavaScript
中其实并没有真正意义上的类的概念,Object上有很多属性和方法,创建出来的对象上依然会有,除非被重写,后期复习到原型原型链的时候再详细说吧,常见的属性和方法如下:
-
constructor
用于创建当前对象函数 -
hasOwnProperty(propertyName)
用于判断当前对象实例(不是原型)上是否存在给定的属性
-
isPrototypeOf(object)
用于判断当前对象是否为另一个对象的原型 -
propertyIsEnumerable(propertyName)
用于判断给定的属性是否可枚举,如属性的enumerable
为false
时,无法使用in\for...in
进行遍历 -
toLocaleString()
返回对象的字符串表示,该字符串反映对象所在的本地化执行环境 -
toString()
返回对象的字符串表示 -
valueOf()
返回对象对应的字符串、数值或布尔值表示,通常与 toString()的返回值相同
关于对象,需要讲的东西太多,后期会专门写一篇来对其进行深入剖析
10. 包装类型(历史遗留问题)
什么是包装类型呢?学过java
的的童鞋应该听说过一个自动装配
的概念,有点类似,js
中提供了Boolean,Number,String
这三种基本数据类型的包装引用类型,可以通过new
操作符进行使用,至于为什么要这么做,我的理解是:对象可以充分利用其属性及方法进行相关操作,而基本数据类型存的原始值没办法享受这份红利,所以为了让我们能非常方便的操作他们,就给他们做了包装,当然我们一般不会显示的使用他们,当要用到某个原始值的方法或属性时,后台会自动帮我们创建,使用后会自动销毁,无需我们进行其他操作。
let str1 = 'hello world.'
let str2 = str1.toUpperCase() //HELLO WORLD.
//等价于
let str1 = new String("hello world.") //创建一个String类型的实例
let str2 = str1.toUpperCase() //调用实例上的特定方法
str1 = null //销毁实例
复制代码
2.1 自动生命流程
自动创建的原始值包装对象只存在于访问它的那行代码执行期间,这意味着不能在运行时给原始值添加属性和方法
let s1 = 'hello world.'
s1.info = 's1'
console.log(s1.info) //undefined
复制代码
分析
- 第2行代码创建了一个String实例,并在它身上加上了info属性,该行代码执行完毕该实例就会被销毁
- 第3行代码
又
创建了一个新的实例,并在它身上加上了info属性,但是并未进行赋值,该行代码执行完毕该实例就会被销毁
2.2 Boolean、Number、String
咱一般不显示的new
它们,知道有这么个东西就行了,这是JavaScript
历史遗留问题,当然各包装对象有一些方法还是要了解,具体可以参考MDN
文档进行学习
链接
JavaScript | MDN (mozilla.org)
数据类型就总结到这里吧,还有一些内置对象类型,诸如Date、RegRex、Set、Map等等基本都是一些API的使用,后期有时间的话再总结一波。