前言
这个系列呢,说是博客其实就是笔记哈哈哈哈,感觉上班久了以后很多基础的东西反而不那么扎实了,也是进行一个梳理吧,站在巨人的肩膀上加一些自己的理解【虽然可能我会把自己绕进去,这不重要 🙈】,但我会努力讲明白哒 😘
本文主要梳理JS的基本数据类型和引用数据类型,显隐式转换规则及深浅拷贝,会持续补充更新哦!
数据存储结构
先来看看三种常见的数据存储结构:
- 栈:只允许在一段进行插入或者删除操作的线性表,是一种先进后出的数据结构。(基本数据类型)
- 队列:队列是一种先进先出(FIFO)的数据结构。(事件循环)
- 堆:堆是基于散列算法的数据结构。(引用数据类型)
基本数据类型
基本类型值指的是那些保存在栈内存中的简单数据段,即这种值是完全保存在内存中的一个位置。
Underfined 类型
只有一个值,当 var 声明变量但未初始化时,这个值为 underfined(没必要显式设置)
对于尚未声明过的变量,只能执行一项操作,即使用typeof操作符检测其数据类型,使用其他的操作都会报错。
Null 类型
只有一个值,空对象指针,当定义的变量将来用于保存对象时,建议初始化为 null 而不是其他值
underfined == null //true
Boolean 类型
true 和 false,注意是区分大小写的,也就是说 True 和 False(以及其他混合大小写形式)都不是 Boolean 的值
转换规则
我们常用的 if(变量名),表示变量不为 false,””,underfined,0 和 NaN
Boolean(underfined) //false
Boolean(null) //false
Boolean(underfined) //false
Boolean("") //false
Boolean(0) //false
Boolean(NaN) //false
Boolean({}) //true
Number 类型
IEEE754 格式来表示整数和浮点数值
整数
var num=56 //十进制56 var num=070 //八进制56 var num=0x38 //十六进制56
- 八进制第一位必须是 0,后面跟八进制序列 0 到 7,如果超出了范围,则忽略前导 0,后面的数值当做十进制解析,例如:089 会被解析为 89。(八进制字面量在严格模式下是无效的,会抛出错误。)
- 十六进制前两位必须是 0x 或 0X,后跟十六进制序列 0
9、af(不区分大小写),如果超出了范围,则会报语法错误。
浮点数
浮点数值精度最高 17 位,计算会产生舍入误差
因为两次存储时的精度丢失加上一次运算时的精度丢失,最终导致了 0.1 + 0.2 !== 0.3
正无穷、负无穷
正数除以 0 返回正无穷(Infinity),负数除以 0 返回负无穷(-Infinity) JavaScript 提供了 isFinite() 函数,来确定一个数是不是有穷的。例如:
isFinite(500) // true
isFinite(Infinity); // false
NaN
NaN(非数值)是一个特殊的数值,用于表示一个本来要返回数值的操作树未返回数值的情况(这样就不会抛出错误了)
//NaN及其本身不相等
NaN == NaN //false
会出现 NaN 的几种情况:通过isNaN()函数来确定是不是 NaN
//isNaN
isNaN(0/0) //true
isNaN(NaN/10) //true (任何涉及NaN的操作)
isNaN(10) //false
isNaN("blue") //false
isNaN(true) //false(转换为1)
转换规则
//Number()
Number(true) //1
Number(false) //0
Number(null) //0
Number(underfined) //NaN
Number("0011") //11
Number("124") //124
Number("") //0 Number("we1") //NaN
//parseInt()
parseInt(""); // NaN
parseInt("12aa"); // 12
parseInt("13.8"); // 13
//parseFloat()
parseFloat("077.2") // 77.2
parseFloat("123.11.22") // 123.11
String 类型
字符字面量(转义序列)
\n 换行、\t 制表、\b 空格、\r 回车、\f 进纸、\ 斜杠、' 单引号,在用单引号表示的字符串中使用、" 双引号,在用双引号表示的字符串中使用
转换规则
//toString()方法(undefined 和 null 值没有)
//String()
var num;
String(10) // "20" 如果值有 toString() 方法,则调用该方法(没有参数)并返回相应的结果
String(true) // "true"
String(null) // "null" (如果值是 null,则返回 "null")
String(num) // "undefined" (如果值是 undefined,则返回 "undefined")
//String()基本包装类型方法
var str='ceshi str'
str.length //字符串长度
str.trim() //删除前后所有空格
str.replace() //替换,默认只替换第一个,如果要全局替换匹配正则设为g
str.split() //分隔,指定分隔符将一个串拆分为多个串并放入数组
Synbol类型
Symbol 是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用,表示独一无二的值。
let a = Symbol('a');
let b = Symbol('a');
a===b //false
注意
- Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
- 定义属性的时候只能将Symbol值放在方括号里面,否则属性的键名会当做字符串而不是Symbol值。同理,在访问Symbol属性的时候也不能通过点运算符去访问,点运算符后面总是字符串,不会读取Symbol值作为标识符所指代的值.
- 常量的使用Symbol值最大的好处就是其他任何值都不可能有相同的值,用来设计switch语句是一种很好的方式。
BigInt类型(第3阶段提案,暂且不论)
引用数据类型
引用类型值指的是那些保存在堆内存中的对象,所以引用类型的值保存的是一个指针,这个指针指向存储在堆中的一个对象。当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值(按引用访问)。
- 除了上面的 6 种基本数据类型外,剩下的就是引用类型了,统称为 Object 类型。细分的话,有:Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型 等。
Object类型
var obj = new Object();
var obj = {}; // 优先对象字面量
valueOf() //返回对象的字符串、数值或布尔值表示(通常与toString()方法的返回值相同)。
Array类型(*)
var arr = new Array();
var arr = []; // 优先数组字面量
转换方法
Array.isArray() //判断是不是数组
Array.toString() //逗号分隔数组为字符串
Array.join(',') //指定分隔符分隔数组为字符串(默认为逗号)
处理方法
Array.unshift() //开头添加项,return修改后长度
//数组实现类似栈的行为(后进先出)
Array.push() //末尾添加项,return修改后长度
Array.pop() //末尾移除一项,return移除的项
//数组实现类似队列的行为(先进先出)
Array.push() //末尾添加项,return修改后长度
Array.shift() //开头移除一项,return移除的项
重排序方法
Array.sort() //默认升序(先toString()再比较)
Array.sort(comp) //==>comp 为比较函数,可以指定排序效果
let a=[1,2,11,4,23]
a.sort() //[1, 11, 2, 23, 4]
a.sort((a,b)=>a-b) //[1, 2, 4, 11, 23]
Array.reverse() //反转数组顺序
操作方法
Array.concat() //创建副本将参数依次添加到末尾,retrun新数组
Array.slice() //return 从start到end(不包括end)的项
Array.splice() //* return删除项,不删除则为[]
//参数的三种操作(起始位置,删除项数,插入值)
let a=[1,2,11,4,23]
//1.删除
a.splice(1,1) //[1, 11, 4, 23]
//2.插入
a.splice(1,0,5) //[1, 5, 2, 11, 4, 23]
//3.替换
a.splice(1,1,5) //[1, 5, 11, 4, 23]
位置方法
Array.indexOf() //从头开始查找项,return项所在位置,没找到为-1(全等比较)
Array.lastIndexOf() //从尾部开始查找项
迭代方法
//不会对原数组进行修改 运行函数的参为(item,index,array)
Array.filter() //过滤,retrun满足条件(为true)的项组成的数组
Array.foreach() //遍历,无return值
Array.map() //映射,return调用结果所组成的数组
Array.every() //当每一项都满足条件时,return true
Array.some() //当存在一项满足条件时,return true
归并方法
//不会对原数组进行修改 运行函数的参为(prev,cur,index,array)
Array.reduce() //从前遍历,迭代所有项返回一个最终值
Array.reduceRight() //从后向前遍历
Date类型
使用UTC(国际协调时间)
new Date() //不传参,自动获取当前日期和时间 Sun Jul 05 2020 15:20:11 GMT+0800 (中国标准时间)
Date.now() //retrun 调用这个方法时的日期和时间的毫秒数(时间戳) 1593933631402
(new Date(2010, 0, 1)).toString() //Fri Jan 01 2010 00:00:00 GMT+0800 (CST)
(new Date(2017, 4, 21)).valueOf() //1495296000000
RegExp类型
支持正则表达式(正则的相关规则单独整理)
var a=/pattern/flags
//pattern 匹配规则
//flags 标志-表明行为
//g-全局(而非匹配第一个就停止) i-不区分大小写 m-多行(继续向下查下一行)
a.exec(str) //捕获组,str为待匹配字符串,return 结果Array
a.test(str) //str为待匹配字符串,若匹配return true
Function类型(*)
函数没有重载,当函数重名时,取最后一次定义,建议使用函数声明来定义函数。
函数调用优先级:
new 调用 > call、apply、bind 调用 > 对象上的函数调用 > 普通函数调用
函数内部属性:
- arguments:参数数组
- arguments.length-传入参的个数,没有传值的命名参数为underfined,arguments不能重写值,但命名参数可以
- 递归时,用arguments.callee(指向拥有该对象的函数)来代替函数名,可以消除紧密耦合,但只能用于非严格模式。
- this:执行函数对象(全局时为window)
隐性转换和显性转换
强制(显性)类型转换
强制类型转换主要是指通过String、Number和Boolean等构造方法手动转换成对应的字符串、数字和布尔值。
自动(隐性)类型转换
自动类型转换就是不需要人为强制的进行转换,js会自动将类型转换为需要的类型,所以该转换操作用户是感觉不到的,因此又称为隐性类型转换
数据的深浅拷贝
- 浅拷贝(Shallow Copy) 只会将对象的各个属性进行依次复制,并不会进行递归复制,也就是说只会赋值目标对象的第一层属性。
- 深拷贝(Deep Copy)不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。(两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性)
浅拷贝
赋值运算符(=)
只拷贝对象的引用值
首层拷贝实现
(只有第一层是深拷贝)
//1.Object.assign()
const obj2 = Object.assign({}, obj1);\\ES6,拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。
//2.... 展开运算符
const obj2 = [...obj1];只是对对象的第一层进行深拷贝
//3.Array.prototype.slice()
const obj2 = obj1.slice();
//4.Array.prototype.concat()
const obj2 = obj1.concat();
手写一个浅拷贝
function shallowClone(obj){
let result=Array.isArray(obj)?[]:{}
Object.keys(obj).forEach(element => {
result[element]=obj[element]
});
return result;
}
深拷贝(*)
JSON.parse()和JSON.stringify() (对目标对象有要求)
const obj2 = JSON.parse(JSON.stringify(obj1));
缺点:
- undefined、function,正则表达式类型以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时);
- 它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object;
- 当出现循环引用时会报错
递归(真正意义上的深拷贝)
递归中可能出现的问题:循环引用
- 父级引用
这里的父级引用指的是,当对象的某个属性,正是这个对象本身,此时我们如果进行深拷贝,可能会在子元素->父对象->子元素…这个循环中一直进行,导致栈溢出。
解决办法:判断一个对象的字段是否引用了这个对象或这个对象的任意父级
- 同级引用
假设对象obj有a,b,c三个子对象,其中子对象c中有个属性d引用了对象obj下面的子对象a。
解决办法:父级的引用是一种引用,非父级的引用也是一种引用,那么只要记录下对象A中的所有对象,并与新创建的对象一一对应即可。
手写一个深拷贝(*)
已经处理了相关边界及循环引用问题
function isObject(obj) { //判断obj是不是一个对象,且当obj为null时原样返回而不是返回{}
return typeof obj === 'object' && obj != null;
}
function deepClone(obj, hash = new WeakMap()) { //hash用于解决循环引用
if (!isObject(obj)) return obj;
if (hash.has(obj)) return hash.get(obj); // 查hash,如果当前obj已经存在则直接取拷贝过的值
let result = Array.isArray(obj) ? [] : {} //对数组和对象进行区分
hash.set(obj, result) //obj不存在时存入hash
Object.keys(obj).forEach(element => { //遍历obj的key进行拷贝
if (isObject(obj[element])) {
result[element] = deepClone(obj[element], hash) //当key对应值仍为对象时,递归拷贝
} else {
result[element] = obj[element] //为基本数据类型则直接拷贝
}
});
return result;
}
拷贝Symbol()的情况
将Object.keys(obj)遍历key值改变为:
- 方法一:Object.getOwnPropertySymbols(…)
- 方法二:
Reflect.ownKeys(...) //等价于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
拷贝原型链上数据的情况
- for..in 进行遍历
问几个问题
Q1:为什么基础数据类型存在栈中,而引用数据类型存在堆中呢?
- 堆比栈大,栈比堆速度快。
- 基础数据类型比较稳定,而且相对来说占用的内存小。
- 引用数据类型大小是动态的,而且是无限的。
- 堆内存是无序存储,可以根据引用直接获取。
Q2:下面的代码会输出什么?
var a = { name: '前端开发' }
var b = a;
a = null;
console.log(b)
{ name: ‘前端开发’ } null是基本类型,a = null之后只是把a存储在栈内存中地址改变成了基本类型null,并不会影响堆内存中的对象,所以b的值不受影响
Q3:从内存来看 null 和 undefined 本质的区别是什么?
- 给一个全局变量赋值为null,相当于将这个变量的指针对象以及值清空,如果是给对象的属性 赋值为null,或者局部变量赋值为null,相当于给这个属性分配了一块空的内存,然后值为null, JS会回收全局变量为null的对象。
- 给一个全局变量赋值为undefined,相当于将这个对象的值清空,但是这个对象依旧存在,如果是给对象的属性赋值 为undefined,说明这个值为空值
Q4:JS判断一下数据类型?
typeof (检测基本数据类型的最佳选择)
- 对于基本类型,除 null 以外,均可以返回正确的结果。
- 对于引用类型,除 function 以外,一律返回 object。
- 对于 null ,返回 object。
- 对于 function 返回 function。
instanceof (判断 A 是否为 B 的实例)
- A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false
- instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型,不同环境下不是同一个构造函数
- Array.isArray() 方法 。该方法用以确认某个对象本身是否为 Array 类型,而不区分该对象在哪个环境中创建
constructor (构造函数)
[ ].constructor == Array true
" ".constructor == String true
- null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。
- 使用它是不安全的,因为contructor的指向是可以改变的
使用Object.prototype.toString.call(目前最优解)
能够生成固定的返回格式,进行截取得到数据类型,目前基本和引用类型全部支持
Object.prototype.toString.call('111') "[object String]"
想了两种取type的方法
let type = Object.prototype.toString.call('111')
let name = type.slice(8,Object.prototype.toString.call('111').length-1)//截取
let name = type.match(/^\[object (\w+)\]$/)[1]//正则
Q5:什么样的数据值在判断时会被转换为false?
我们使用 Boolean 函数将类型转换成布尔类型,在 JavaScript 中,只有 6 种值可以被转换成 false,其他都会被转换成 true。
console.log(Boolean()) // false
console.log(Boolean(false)) // false
console.log(Boolean(undefined)) // false
console.log(Boolean(null)) // false
console.log(Boolean(+0)) // false
console.log(Boolean(-0)) // false
console.log(Boolean(NaN)) // false
console.log(Boolean("")) // false
Q6:函数中的arguments是数组吗?若不是,如何将它转化为真正的数组?
typeof arguments=="object"
Object.prototype.toString.call(arguments)=="[object Arguments]"
不是,是类数组对象
var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"]
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"]
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"]
// 4. apply
Array.prototype.concat.apply([], arrayLike) // ["name", "age", "sex"]
// 5. ES6扩展运算符
[...arrayLike] // ["name", "age", "sex"]
Q7:函数传参数是按值还是引用?数据类型或者对象类型都一样吗?
ECMAScript中所有函数的参数都是按值来传递的,把函数外部的值复制给函数内部的参数,函数只能操作对象的属性和值,而不能操作对象本身。
- 原始值:只是把变量里的值传递给参数,之后参数和这个变量互不影响,这个很好理解,不再赘述。
- 引用值:对象变量里面的值是这个对象在堆内存中的内存地址,因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象。
巨人的肩膀
- 《JavaScript高级程序设计(第3版)》
- 《ECMAScript 6 入门》
- JavaScript深入之头疼的类型转换(上)
- 「JavaScript」带你彻底搞清楚深拷贝、浅拷贝和循环引用
- JavaScript基础心法——深浅拷贝
- 木易杨前端进阶-第 4 期:深浅拷贝原理
最后
欢迎纠错,看到会及时修改哒!❤
温故而知新,希望我们都可以保持本心,念念不忘,必有回响。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!