JS中的数据类型
原始值类型「基本数据类型 & 值类型」
- number 整数、小数、零、负数、NaN(不是一个有效数字,但是属于数字类型)、Infinity(无穷大的值)…
- string 字符串:“”、‘’、``(ES6中的模板字符串,更方便的实现字符串拼接)
- boolean 布尔:true/false
- null 空
- undefined 未定义
- symbol 唯一值(ES6+)
- bigint 大数(ES6+)
Symbol数据类型作用
- 想创建一个和别人不相等的值(唯一值)
- Symbol不能被 new 执行
- 每一次 “Symbol(描述)” 这样执行,都是创建一个新的唯一值出来
- console.log(Symbol() === Symbol()) //false
- 可以给对象设置一个symbol类型(唯一)的成员「属性名」
- 对象的“成员值”,可以是任意类型的值
- 但是对象的“成员”只能是:字符串、symbol类型「ES6新增的Map结构,允许对象的成员是“对象类型”」
```javascript
let obj = {
name: ‘哈哈哈’,
age: 25,
0: 100, //-> ‘0’: 100 会把成员默认转换为字符串类型
true: 200 //-> ‘true’: 200
}
console.log(obj.name, obj[‘name’]) //‘哈哈哈’ ‘哈哈哈’
console.log(obj[0], obj[‘0’]) //100 100 在进行成员访问的时候,如果访问的成员,不是字符串格式,也要先转换为字符串格式
console.log(obj.true, obj[‘true’], obj[true]) //200 200 200
let aa = { x: 10 } let bb = { y: 20 } let obj = { // aa: ‘哈哈哈’ //->成员名叫做 “aa” [aa]: ‘哈哈哈’ //->把aa变量的值,作为对象的一个成员 => ‘[object Object]’:’哈哈哈’ } obj[bb] = ‘呵呵呵’ // => ‘[object Object]’:’呵呵呵’ console.log(obj)console.log(obj[aa] === obj[bb]) //true
let sym = Symbol(‘AA’) let obj = { name: ‘哈哈哈’, age: 25, [Symbol(‘AA’)]: 200, //此成员是 Symbol 类型,不是字符串类型
} console.log(obj) console.log(obj[Symbol(‘AA’)]) //undefined 此处又创建了一个新的唯一值,和之前的唯一成员不是一个值 console.log(objsym) //300
1. 对象中“成员”的特点:
- 类型的限制:字符串、symbol
- 规则的限制:是否可删除、是否可修改、是否可枚举
> 可枚举:可以被 for/in循环 或者 Object.keys 列举的成员,被称为可枚举的「一般来讲,自己设置的成员都是可枚举的,内置的成员都是不可枚举的,当然我们可以去设置它的规则」
```javascript
let obj = {
name: '哈哈哈',
0: 100,
[Symbol()]: 200
}
let keys = Object.keys(obj) // 获取对象中 “可枚举”、“非symbol类型” 私有的成员
console.log(keys) //['0', 'name']
let keys = Object.getOwnPropertyNames(obj) // 获取对象中 “非symbol类型” 私有的成员
console.log(keys) //['0', 'name']
let keys = Object.getOwnPropertySymbols(obj) // 获取对象中 “symbol类型” 私有的成员
console.log(keys) //[Symbol()]
//需求:获取对象中所有的私有成员
let keys = Object.getOwnPropertyNames(obj)
keys = keys.concat(Object.getOwnPropertySymbols(obj))
console.log(keys) //['0', 'name', Symbol()]
//ES6提供的Reflect也可以获取
let keys = Reflect.ownKeys(obj)
console.log(keys) //['0', 'name', Symbol()]
Object.defineProperty(对象,成员,配置项)
- 作用1:给对象的某个成员设置(修改)规则「或者 给对象新增一个成员,并设置其规则」 ```javascript let obj = { name: ‘哈哈哈’, 0: 100,
} Object.defineProperty(obj, ‘name’, { configurable: false, //设置为不可删除 writable: false, //设置为不可修改 enumerable: false, //设置为不可枚举 }) Object.defineProperty(obj, ‘age’, { // 如果基于这种方式,”给对象新增成员”,在不设置其规则的情况下,默认规则都是false,也就是默认:不可删除、不可修改、不可枚举 value: 25 }) console.log(Object.getOwnPropertyDescriptor(obj, ‘age’)) //查看对象中某个成员的规则 console.log(Object.getOwnPropertyDescriptors(obj)) //查看对象所有成员的规则
- 作用2:给对象中的某个成员设置劫持函数,在获取和设置成员值的时候,不会去操作堆内存,而是走劫持函数处理
```javascript
Object.defineProperty(obj,key,{
get(){},
set(){}
})
let obj = {
name: '哈哈哈'
}
Object.defineProperty(obj, 'name', {
configurable: true,
// writable: true, //设置劫持函数后,writable和value这两个规则就不允许设置了
enumerable: true,
get() {
// 访问对象的这个成员时,就会触发get这个函数,函数的返回值,就是获取的成员值
console.log('GET劫持函数')
return '呵呵呵'
},
set(val) {
// 当修改对象的这个成员值的时候,先触发set这个函数,val就是要修改的值
console.log('SET劫持函数', val)
}
})
//给对象新增一个不可枚举的成员(可删除可修改)
const define=function define(obj,key,value){
Object.defineProperty(obj,key,{
value,//"value":value | value:value
writable:true,
configurable:true,
enumerable:false
})
}
- for/in循环的“设计缺陷”:
- 在迭代的时候,不仅去遍历私有成员,而且还会基于其原型链,遍历其原型对象上的“公有”成员「性能很差」
- 只能迭代到 “非symbol类型”、“可枚举” 的成员「限制多、功能弱」
所以,以后开发的时候,尽可能的不要用for/in循环 :::info 问题:以后对象该如何迭代?
- 先获取对象所有的私有成员「不受类型和枚举性的限制」 —> 包含所有私有成员的数组
- 迭代数组中的每个成员,再获取相应的成员值即可 :::
for (let key in obj) {
if (!obj.hasOwnProperty(key)) break //只要找到一个公有的,说明私有的都迭代完毕了,结束循环即可「目的:排除迭代公有的」
console.log(key)
} //因此依旧不建议用for/in
let keys = Reflect.ownKeys(obj) //['0', 'name', 'age', Symbol()]
keys.forEach(key => {
console.log(key, obj[key])
})
回调函数:把创建的函数callback作为“实参”值,传递给另外一个函数fn,在fn执行的过程中,根据相应的需求,把传递进来的callback执行!
function fn(a, b, callback) {
// ...
let total = a + b //30
let res = callback.call(xxx, total)
}
fn(10,20,function (x) {
console.log(x)
return true
}
)
重写forEach
Array.prototype.myForEach = function myForEach(callback) {
// this->arr 需要迭代的数组
// callback-> 传递的回调函数
let self = this
for (let i = 0; i < self.length; i++) {
callback(self[i], i)
}
}//简易版
Array.prototype.myForEach = function myForEach(callback, context) {
if (typeof callback !== 'function') throw new TypeError('callback必须是一个函数')
let len = +this.length,
k = 0
if (isNaN(len)) throw new TypeError('迭代的数据需要是一个数组/伪数组')
while (k < len) {
if (this.hasOwnProperty(k)) {
// 规避稀疏数组
let item = this[k],
index = k
callback.call(context, item, index)
}
k++
}
}
let arr = [10, 20, 30, 40]
let obj = { name: '哇咔咔' }
arr.myForEach(function (item, index) {
console.log(item, index, this)
}, obj)
创建数组: ```javascript
- 字面量方式 let arr = [10, 20, 30, 40]
- 构造函数方式 new Array() -> [] new Array(数字N) -> 创建一个长度是N,但是没有具体项的“稀疏数组” new Array(数字N).fill(null) -> 对每一项进行填充,把其变为“密集数组” new Array(数字N,数字M,字符串S) new Array(字符串S) -> 创建一个数组,括号中的实参都是数组每一项的值
Array.of(参数,…) 「ECMAScript2015」 创建一个数组,传递的参数都是数组中每一项的值 ```
处理JS的一些底层机制
- Symbol.asyncIterator
- Symbol.iterator
- Symbol.hasInstance
- Symbol.toPrimitive
- Symbol.toStringTag
-
BigInt 大数
在JS中存在最大/最小的“安全”数字
- Number.MAX_SAFE_INTEGER -> 9007199254740991
- Number.MIN_SAFE_INTEGER -> -9007199254740991
- 如果使用number类型,超过安全数字后,不论是展示还是计算,都可能会不准确
- 作用:客户端经常和服务器进行通信,服务器端存储的数字分为 init整型、longInt长整型…,
- 此时如果服务器返回一个超大数字,客户端即便获取到,一但超过最大的安全数字限制,也会丢失精准度!而BigInt就是为了解决大数的问题!!
- 如何创建BigInt类型的值
- 数字后面加一个 n 即可 -> 10n
- BigInt(数字)
- 和Symbol一样,不能被 new 执行
场景方案:
标准普通对象(纯粹的对象) 例如:{x:10,y:20}
- 其proto直接指向Object.prototype
- 基于Object.create(null)创建的对象,其没有原型链,这样的也是标准普通对象
- 标准特殊对象
- new Array 数组
- new RegExp 正则
- new Date 日期对象
- new Error 错误对象
- Set/Map 「ES6+新增的数据结构 」
- …
- 非标准特殊对象
- 原始值对应的对象数据类型值,都具备[[PrimitiveValue]]属性 带[[]]的属性我们访问不到
- 例如:new Number(1) 、new String(‘…’)、new Boolean(true)
- 例如:Object(Symbol())、Object(10n)
- null/undefined比较特殊,没有相应的对象类型值(没有Null/Undefined这样的构造函数)
- 原始值对应的对象数据类型值,都具备[[PrimitiveValue]]属性 带[[]]的属性我们访问不到
-
数据类型检测
typeof :检测数据类型的运算符
特点1:返回的结果是字符串,字符串中包含了对应的数据类型
- typeof typeof typeof [1,2,3]->”string”
- 特点2:typeof null->“object”
- typeof不能检测null
- 检测其他六种原始值类型是没有问题的
特点3:typeof 对象->“object”&&“function“
- 无法对“对象”进行细分检测(除函数对象外)
//检测是否为函数
const isFunction=function isFunction(obj){
return typeof obj==='function'
}
//笼统校验是否为对象
const isObject=function isObject(obj){
return obj !== null && /^(object|function)$/.test(typeof obj)
}
- 无法对“对象”进行细分检测(除函数对象外)
特点4:typeof未被声明的变量->“undefined”(不会报错)
typeof检测的底层处理机制
在计算机编程语言中(包括js),所有的数据类型值,在计算机底层都是按照“二进制(0\1)”进行存储的
- 操作系统是32位的。则存储的二进制值也是32位
- 操作系统是64位的,则存储的二进制值也是64位(主流)
- 不同数据类型的二进制值是有特点的
- 对象:前三位是000
- 数字:整数是以1开始、浮点数(小数)是以010开始
- 字符串:以100开始
- 布尔:以110开始
- null:64位都是0
- undefined比较特殊,他存储的是-2^30
- …
- typeof 就是按照计算机底层存储的二进制值来进行检测的(优势:效率高、检测快)
- 检测的时候,主要以开始的数字来判断,例如:以000开始的认为对象…
因为null存储的都是0,前三位肯定也是0,所以被误识别为对象
typeof null->”object” - 如果被检测出来是对象,接下来会再看:当前对象是否实现了 [[Call]]
如果没有,则返回”object”
如果实现了,则返回”function”
- 检测的时候,主要以开始的数字来判断,例如:以000开始的认为对象…
- 但是在所有的检测之前,先看”要被检测的值”是否声明了,如果没有声明,直接返回”undefined”
typeof在实战中的运用
- 检测除null以外的原始值类型(性能高)
- 笼统的校验是否为对象
- 检测是否为函数 => isFunction—>if(typeof obj===”function”){…}
处理浏览器兼容(ES6+语法规范,都不兼容IE)
- 所谓的兼容,无外乎就是:高版本浏览器有这个东西,而低版本浏览器没有这个东西(直接访问会报错xxx is not defined)
- if(typeof Symbol !== ‘undefined’){
//浏览器兼容Symbol(浏览器支持ES6+的语法)
} - 以此原理可封装 获取对象所有私有成员(兼容IE、不受枚举和类型限制)方法 如下:
const ownKeys = function ownKeys(obj) {
// 确保传递的是对象
if (!isObject(obj)) throw new TypeError('传递的obj不是一个对象')
// 验证是否支持Reflect
if (typeof Reflect !== 'undefined') return Reflect.ownKeys(obj)
return Object.getOwnPropertyNames(obj)
}
//或以下方法 实际效果一样
const ownKeys = function ownKeys(obj) {
if (!isObject(obj)) throw new TypeError('传递的obj不是一个对象')
let keys = Object.getOwnPropertyNames(obj)
if (typeof Symbol !== 'undefined') {
keys = keys.concat(Object.getOwnPropertySymbols(obj))
}
return keys
}
数据类型转换
把其他数据类型转换为Number
Number([val])
一般用于浏览器的“隐式转换”中,如:
- 数学运算
- isNaN检测
- ==比较
规则:
- 字符串转换为数字:空字符串变为0,如果出现任何非有效数字字符,结果都是NaN
- 把布尔转换为数字:true->1 false->0
- null->0 undefined->NaN
- Symbol无法转换为数字,会报错:Cannot convert a Symbol value to a number
- BigInt去除“n”(超过安全数字的会丢失精准度;再长的数字,会按照科学计数法处理)
- 把对象转换为数字:
- 先调用对象的 Symbol.toPrimitive 这个方法,如果不存在这个方法
- 再调用对象的 valueOf 获取原始值,如果获取的值不是原始值
- 再调用对象的 toString 把其变为字符串
- 最后再把字符串基于Number方法转换为数字
let obj = { x: 10 }
// obj[Symbol.toPrimitive] --> undefined 不具备这个方法
// obj['valueOf']() --> {x: 10} 结果不是原始值
// obj['toString']() --> '[object Object]' Object.prototype.toString
// Number('[object Object]') --> NaN
console.log(Number(obj))
let arr = [10]
// arr[Symbol.toPrimitive] --> undefined 不具备这个方法
// arr['valueOf']() --> [10] 结果不是原始值
// arr['toString']() --> '10' Array.prototype.toString
// Number('10') --> 10
console.log(Number(arr))
let time = new Date()
// time[Symbol.toPrimitive] --> 在Date.prototype上有Symbol.toPrimitive这个函数
// 具备这个函数,则把这个函数直接执行「可以传递的值:'number'、'string'、'default'」
// time[Symbol.toPrimitive]('number') --> 1689154535927
//「时间戳:距离1970/01/01 00:00:00之间的毫秒差」
console.log(Number(time))
let num = new Number(10)
// num[Symbol.toPrimitive] --> undefined 不具备这个方法
// num['valueOf']() --> 10
console.log(Number(num))
parseInt([val]) parseFloat([val])
一般用于手动转换
规则:
从[val]左侧开始进行查找,找到所有符合[radix]进制的内容,然后把其按照[radix]进制转换为10进制!!
- [radix]是设置的进制,取值有效范围是2~36之间,如果不在有效范围内,结果就是NaN
[radix]不写或者设置的为0,默认就是10「特殊情况:如果[val]是以“0x”开始的,则默认值是16」
把其他数据类型转换为String
转化规则:
拿字符串包起来
对象转字符串
String([val]) 或者 [val].toString()
- “+”除数学运算,还可能代表的字符串拼接
- 有两边,一边是字符串,肯定是字符串拼接
- 有两边,一边是对象,则可能是字符串拼接,还有可能是数学运算
- 只出现在左边,例如:+”10” 这种方式就是把其它值转换为数字
-
把其他数据类型转换为Boolean
转换规则
除了“0/NaN/空字符串/null/undefined”五个值是false,其余都是true
出现情况:
Boolean([val]) 或者 !/!!
-
“==”与“===”
“==”相等,两边数据类型不同,需要先转为相同类型,然后再进行比较
对象==字符串 对象转字符串「Symbol.toPrimitive -> valueOf -> toString」
- null==undefined -> true null/undefined和其他任何值都不相等
- null===undefined -> false
- 对象==对象 比较的是堆内存地址,地址相同则相等
- NaN!==NaN NaN和任何值(包含本身)都不相等
除了以上情况,只要两边类型不一致,剩下的都是转换为数字,然后再进行比较的
“===”绝对相等,如果两边类型不同,则直接是false,不会转换数据类型「推荐」
Object.is([val1],[val2]) 检测两个值是否相等「ES6新增的」
核心用的是“===”
- 特殊:Object.is(NaN,NaN) => true
面试题
第一题
```javascript var a =? if (a == 1 && a == 2 && a == 3) { console.log(‘OK’) } // 思路一:利用“==”比较会转换数据类型,如果a是一个对象,每一次和数字比较,都会基于 Number 把a先转为数字,再进行比较,而基于Number把对象转数字,会检测对象的 Symbol.toPrimitive、valueOf、toString,只要我们把这三个中的任意一个方法重写,让其执行的时候,分别返回 1/2/3 即可!! var a = { i: 0 } a[Symbol.toPrimitive] = function () { //a[valueOf] = function () { //a[toString] = function () { // this->a return ++this.i//a.i } if (a == 1 && a == 2 && a == 3) { console.log(‘OK’) } //这种是一个思路 var a = [1, 2, 3] a.toString = a.shift if (a == 1 && a == 2 && a == 3) { console.log(‘OK’) }
//思路二:在全局上下文中,基于 var 声明变量,是给 window 设置一个新的成员;当我们访问 a 的时候,是获取 window.a 的值;既然这样,我们就可以基于 Object.defineProperty 给 window 对象中的 a 这个成员,做数据劫持「get/set」! var i = 0 Object.defineProperty(window, ‘a’, { get() { return ++i } }) if (a == 1 && a == 2 && a == 3) { console.log(‘OK’) }
<a name="uD0Ij"></a>
#### 第二题
```javascript
let arr = [27.2, 0, '0013', '14px', 123]
arr = arr.map(parseInt)
console.log(arr) //[27,NaN,1,1,27]
第一轮迭代:parseInt(27.2,0)
parseInt('27.2',10)
+ 查找符合10进制的内容 '27'
+ 把其按照10进制转换为10进制 27
第二轮迭代:parseInt(0,1)
+ NaN 因为没有1进制
第三轮迭代:parseInt('0013',2)
+ 查找符合2进制的内容 '001'
+ 把其按照2进制转换为10进制
第四轮迭代:parseInt('14px',3)
+ 查找符合3进制的内容 '1'
+ 把其按照3进制转换为10进制
第五轮迭代:parseInt(123,4)
parseInt('123',4)
+ 查找符合4进制的内容 '123'
+ 把其按照4进制转换为10进制
parseInt(0013,2)
// 浏览器认为只要以0开始的“数字”,默认都是8进制的,需要隐式转换为10进制的数字
// 0013 -> 8转10
// 0*8^3+0*8^2+1*8^1+3*8^0 => 11
parseInt(11,2)
// 找到符合2进制的内容 -> '11'
// 在把2进制转为10进制 1*2^1+1*2^0 => 3
第三题
console.log(0.1 + 0.2 === 0.3) //false
不论是整数还是小数(浮点数),在计算机底层都是按照2进制进行存储的
拓展:把10进制的数字转换为2进制
整数:除以2,取其余数,然后用商值继续除以2,直到商值变为0,最后把所有余数倒过来拼接
浮点数:乘以2,取整数部分,直到取整后,变为0
整数变为2进制不会出现无限循环的情况,但是浮点数是很容易出现的,而计算机底层存储的2进制值,最多64位,所以对于浮点数来讲,很可能存储值的时候,就已经丢失了精准度
浮点数的运算,可能会导致最后的结果出现很长的小数点位数(精准度丢失)
0.1+0.2=0.30000000000000004
let num=16
console.log(num.toString())//“16”
console.log(num.toString(2))//把10进制的数字,转换为指定radix进制的字符串“10000”
问题:如何解决浮点数计算不精准的问题
方案1:基于toFixed保留小数点位数(一般两位),自带四舍五入机制
let num=0.1+0.2
console.log(+num.toFixed(2)===0.3)//num.toFixed(2)是字符串
方案2:让计算的浮点数,乘以统一的系数(系数需要按照最大的来,其目的是把浮点数变为整数),最后再除以系数
let num=0.1*10+0.2*10
console.log(num/10===0.3)//true
小结
- Object.keys(obj) // 获取对象中 “可枚举”、“非symbol类型” 私有的成员
- Object.getOwnPropertyNames(obj) // 获取对象中 “非symbol类型” 私有的成员
- Object.getOwnPropertySymbols(obj) // 获取对象中 “symbol类型” 私有的成员
- Reflect.ownKeys(obj)//返回一个由目标对象自身的属性键组成的数组
- Object.defineProperty(对象,成员,配置项)
- //作用1:给对象的某个成员设置(修改)规则「或者 给对象新增一个成员,并设置其规则」 ```javascript Object.defineProperty(obj, ‘name’, { configurable: false, //设置为不可删除 writable: false, //设置为不可修改 enumerable: false, //设置为不可枚举 }) Object.defineProperty(obj, ‘age’, { // 如果基于这种方式,”给对象新增成员”,在不设置其规则的情况下,默认规则都是false,也就是默认:不可删除、不可修改、不可枚举 value: 25 })
console.log(Object.getOwnPropertyDescriptor(obj, ‘age’)) //查看对象中某个成员的规则 console.log(Object.getOwnPropertyDescriptors(obj)) //查看对象所有成员的规则
- Object.getOwnPropertyDescriptor(obj, 'age')) //查看对象中某个成员的规则
- Object.getOwnPropertyDescriptors(obj)) //查看对象所有成员的规则
作用2:给对象中的某个成员设置劫持函数,在获取和设置成员值的时候,不会去操作堆内存,而是走劫持函数处理
```javascript
let obj = {
name: '哈哈哈'
}
Object.defineProperty(obj, 'name', {
configurable: true,
// writable: true, //设置劫持函数后,writable和value这两个规则就不允许设置了
enumerable: true,
get() {
// 访问对象的这个成员时,就会触发get这个函数,函数的返回值,就是获取的成员值
console.log('GET劫持函数')
return '呵呵呵'
},
set(val) {
// 当修改对象的这个成员值的时候,先触发set这个函数,val就是要修改的值
console.log('SET劫持函数', val)
}
})
Symbol.toPrimitive
valueOf