【三】引用类型

前言

本篇章主要介绍了引用类型的特点,诸如正则表达式、时间类型、基本包装类、Object、数组等,至于function部分,内容较多,将另起篇章。

面试回答

1.引用类型:引用类型有array、object、function。引用类型属于堆内存,占用的空间不固定,保存和复制的是指针。引用类型常用instanceof去判断,如果本身不知道数据属于什么类型的话,就用object.prototype.toString.call方法去判断,不管是instanceof还是call方法,他们本质上都是通过原型链判断。

2.数组去重:最常用的是利用Set这种数据结构,搭配Array.from或者扩展运算符来实现。至于其他方式,不管是利用indexOf、includes、sort(sort排序后相同的元素会相邻),首先都会有一层遍历。

3.深浅拷贝:深浅拷贝的区别在于,浅拷贝只复制指针,新旧对象仍使用同一块内存。而深拷贝会创建一个一模一样的对象,开辟新的内存,因此修改新对象不会影响原对象。简单点,就用JSON.parse包裹着JSON.stringify,这种方式算深拷贝,因为它开辟了新的内存空间,只不过JSON.stringify会丢失一些数据,比如undefined、function。准确点,就需要采取递归的方式去深拷贝,判断传入的数据是不是object,如果不是就抛出去,如果是那么就继续递归。

4.new关键字:new关键字先是创建新的对象,然后继承传入的函数原型,修改this指向并且执行构造函数,最后返回新的对象。

知识点

引用类型都是堆(heap) ,有两个特征:1. 保存和复制都是指针。2.占用空间不固定(大小不定),也不会自动释放,堆内存中。

1.RegExp

基本规则

关于RegExp对象还是经常会使用到,比如限制input输入框(校验邮箱是否符合)、匹配变量值做if判断等等。RegExp类型既可以通过字面量声明的方式创建,也可以通过new的方式创建;

字面量声明:/pattern/attributes ,例:let re1 = /ABC/g

new声明:new RegExp(pattern, attributes),例let regex=new RegExp('/abc/',g)

pattern既可以是正则表达式,也可以是字符串,如果是字符串,则如下:

let reg = /hello/g
let test = reg.test("hello World")
console.log(test) // true

attributes为可选的字符串 ,含属性 "g"、"i" 和 "m",分别用于指定全局匹配、不区分大小写的匹配和多行匹配

下面着重梳理几条规则,用来能够基本的识别正则表达式,具体属性可以参考:RegExp基础规则

1./ xxx /g ,所有的正则表达式均写在/ /内,反斜杠代表转义,如\d,表示数字

2.^表示行的开头,^\d表示必须以数字开头

$表示行的结束,\d$表示必须以数字结束

有头有尾则必须完全符合规则才可匹配,如^\d{3}$则表示,连续三个数字才可匹配。

3.[]表示能够匹配的范围,匹配的是单个字符

4.如果涉及特殊字符如'-',在正则表达式中,要用'\-'转义,如,/^\d{3}\-\d{3,8}$/

5.n{X,Y}表示至少出现X个连续n ,至多出现Y个连续n时,匹配。Y可不写。闭区间,含X,Y

6.*等于{0,} 即大于等于0次 ;+代表{1,} 即大于等于1次;?代表{0,1} 即0次或1次

7.用()表示的就是要提取的分组。比如:^(\d{3})\-(\d{3,8})$

8.取反:[^abc],查找任何不在方括号之间的字符。

根据上面8条规则来识别一下下述含义:

let re = /^\d{3}\-\d{3,8}[0-9][a-z]$/;
去掉//,主体为^\d{3}\-\d{3,8}$
^,表示开始
\d{3},表示连续三个数字
\-,表示特殊字符-
\d{3,8},表示至少连续3个数字至多连续8个数字
[0-9],表示这个位置的字符必须为0-9
[a-z],表示这个位置的字符必须为a-z
$,表示结束

常用方法

RegExp 对象有 4 个方法:test()、exec() 、compile()、match()

test():test() 方法检索字符串中的指定值。返回值是 true 或 false。

match():match()方法返回值是符合规则的值(数组)。

exec():exec() 方法检索字符串中的指定值。返回值是被找到的值。如果没有发现匹配,则返回 null

compile():compile()方法用于改变 RegExp。

2.Date

定义

Date获取的时间为本地时间,即电脑或手机上的设备时间,如果你设备时间不准确,那么你获取的时间同样也会不准确;如若涉及时区问题,可以使用UTC时间 (零时区) 。

基础API

//获取当前时间
let b = new Date()
//获取年份
b.getFullYear() 
//获取月份
b.getMonth()+1
//获取天
b.getDate() 
//获取小时
b.getHours()
//获取分钟
b.getMinutes() 
//获取秒数
b.getSeconds()
//获取毫秒
b.getMilliseconds() 
//获取星期几
b.getDay() 


//获取字符串形式
b.toString() 
//字符串形式仅时间部分
b.toTimeString()
//字符串形式仅日期部分
b.toDateString() 
//获取时间戳
b.getTime()(当地时区)或者b.valueOf() 
//获取UTC时间
b.toUTCString() 
//获取指定日期的时间类型
new Date(yyyy,mth,dd,hh,mm,ss);   mth为0-11
new Date('2017-05-24 12:30:23')
//日期加减,年月日时分秒同理,可查询相关API
let dt = new Date('2021-11-10');
dt.setDate(dt.getDate() + 10);
console.log(dt.toString()); // Tue Nov 20 2021 08:00:00 GMT+0800 (中国标准时间)

应用场景

1.针对要求高精确性的时间,举个例子:

假设:本地时间、服务器时间不一致,而这个假设发生的概率很大。

需求:一个案件需要点击拨打电话后才可提交案件

操作:点击拨打按钮记录的是本地时间(用来展示);提交案件记录的是服务器的时间(后端需要记录案件提交的时间);而这时候时间不一致会带来许多分析上的麻烦。比如你明明12:30:00(本地时间)打了电话,提交案件的时间却是12:00:00(服务器时间)。理论上提交案件的时间应该晚于拨打时间,却因为时间不一致,导致数据分析有异常。

解决办法:通过调用接口获取服务器时间,统一时间上的管理,避免造成数据分析的麻烦。

经验总结:如果涉及到时间问题,特别是对时间精确要求较高的需求时,需要考虑时间获取来源的统一。

2.针对时间格式的统一与转换

问题缘由:不同的浏览器对于new Date()的解析支持是不同的,比如ios就只支持"2021/12/12 12:21:21",而部分浏览器则只支持"2021-12-12 12:21:21"。而有时候需求对时间格式有着另外的要求,也需要对时间进行统一的处理。

经验总结:

首先,涉及到时间问题,首先要考虑时间格式能不能被识别兼容

其次,需要与后端统一好时间格式,避免由于时间格式产生的报错,诸如显示、转换等,推荐方法是前后端传值统一使用时间戳,时间格式由前端进行转换。

最后,涉及时间格式的转换,常见的便是new Date() 、elementUI中的dateTimepicker,而时间格式的转换我这边推荐插件moment.js或者dayjs或者自行封装一个简易的时间处理函数。

3.基本包装类型

本质

为了便于操作基本类型值,ECMAScript 还提供了3 个特殊的引用类型:Boolean、Number和String。而boolean、number、string基本类型之所以能够直接操作是因为在调用方法的过程中,默默进行基本包装类型的处理,如下:

let str1 = 'hello'; //string 基本类型
let str2 = str.indexOf('o'); //在执行到这一句的时候 后台会自动完成以下动作 :
( 
 var _str = new String('hello') // 过对应的包装对象创建出一个和基本类型值相同的对象
 var s2 = _str.indexOf('o'); // 通过包装对象进行方法的调用,并赋值给s2
 _str = null;  //    销毁包装对象
 ) 

相关知识点

装箱:基础类型-->引用类型

string:new String()

boolean:new Boolean()

number:new Number()

拆箱:引用类型-->基础类型

let numObj = new Number(123)
let strObj = new String('zxc')
let booObj = new Boolean(true)
console.log( typeof numObj ) //object
console.log( typeof strObj ) //object 
console.log( typeof booObj ) //object 
//拆箱
console.log( typeof numObj.valueOf() ); //number
console.log( typeof strObj.valueOf() ); //string 
console.log( typeof booObj.valueOf() ); //boolean

4.Object

属性的增强写法

const name ='chiji'
const age  = '18'
const obj = {
    name,        //name:name
    age        //age:age
}
console.log(obj.name)    //chiji
console.log(obj.age)    //18    

JSON的理解

JSON是一种轻量级的数据交换格式,主要是为了跨平台交流数据用的,因此他的格式要求也相对规范严格,比如JSON的属性名必须有双引号,如果值是字符串,也必须是双引号。由于在请求接口中使用的JSON格式,所以对于前端代码中的入参基本都是通过序列化的方式进行入参的处理,如:

let o = {a:1}
let ojson = JSON.stringify(o) //'{"a":1}'  string类型
let oobj = JSON.parse(ojson) //{a: 1}     object类型

遍历方法

let obj= {
    a:'1',
    b:2,
    c:false
}

Object.keys

Object.keys(obj) 
//['a', 'b', 'c']

Object.values

const obj = {
    100:'a',
    2:'b',
    7:'c'
};
Object.values(obj) 
//['b','c','a']
PS:如果属性名为数值的属性,是按照数值大小,从小到大遍历的

Object.entries(obj)

Object.entries(obj)
//[['a', '1'],['b', 2],['c', false]]

for...in:数组也可以使用,但是该方法会遍历对象的整个原型链,性能非常差,且遍历顺序不确定,不推荐使用

for(let key in obj){
    console.log(key + '---' + obj[key])
}
//a---1
//b---2
//c---false

Object.getOwnPropertyNames(obj):

Object.getOwnPropertyNames(obj)
// ['a', 'b', 'c']

Reflect.ownKeys(obj)

Reflect.ownKeys(obj)
// ['a', 'b', 'c']

对象转数组

Object.keys({name:'张三',age:14}) //['name','age']
Object.values({name:'张三',age:14}) //['张三',14]
Object.entries({name:'张三',age:14}) //[[name,'张三'],[age,14]]

对象合并

对象:
let obj1 = {
    a:1,
    b:'zxp',
    c:function(){},
    d:{
        abc:'123',
        qwe:{}
    }
}
let obj2 = { a: 5 };

---------合并--------
let obj3 = {...obj1, ...obj2};
//如果有重复的key,则后面的会将前面的值覆盖掉

//Object.assign(target, source1, source2,...);
//let obj3 = Object.assign({},obj1, obj2);
//如果不使用{}作为target,合并后obj1的数据也会修改
//{
    a:5,
    b:'zxp',
    c:function(){},
    d:{
        abc:'123',
        qwe:{}
    }
}

5.Array

与Object的区别

1.数组有序,Object无序

2.数组的元素可以没有属性名(但是有索引),对象的元素必须有值

3.数组只能用整数作为数组元素的索引,而不能用字符串,且数组元素的使用只能通过方括号(arr[0])来获取

4.数组的空元素empty表示空位, 它不是一种数据类型, 而是由于人为修改arr.length 或者写入时多写了逗号造成的。empty和undefined在操作数组时的主要区别在于:使用数组的forEach()方法遍历时会自动忽略空位, 而使用for循环则会将empty转换为undefined并遍历。

let arr = [1,2,3,4,5]
delete arr[3]
console.log(arr)

Tip:使用对象或数组的方法前一定要先判断其是否存在,因为数组经常会连带方法使用,比如arr.indexOf('123'),此时如果arr不存在,那么程序会直接报错。

不修改原数组的方法

1、2:判断数组中是否包含一个元素
3、4:找出第一个符合条件的数组成员
5:判断数组是否为空,由于数组是对象,因此存在引用地址的问题,所以不能使用 arr === [] 来判断数组是否为空
6、7:合并数组可以使用,不过6、7都属于浅拷贝,即仅适用于对不包含引用对象的一维数组的拷贝,就是对象不是那种多层嵌套的
8:复制数组,不过8属于浅拷贝,即仅适用于对不包含引用对象的一维数组的拷贝,就是对象不是那种多层嵌套的
9:适合用于将数组元素拼接后转换成字符串,但如果元素为undefined或null,它会被转换为空字符串。

let arr = [1,'2',false,null,{a:1}]


1.includes(searchvalue, start):searchvalue为查找元素,start为查找起始index
arr.includes(1)   
//true        


2.indexOf:同1
arr.indexOf(1)>-1 
//true


3.find(function(currentValue, index, arr)):currentValue为当前元素,index为元素索引,arr为当前数组
arr.find((n)=>n>0)  
//1        1为值


4.findIndex():同3
arr.findIndex(function(value){
    return value >0
})   
//0        0为索引


5.length
arr.length ===0 
//false


6.Spread扩展运算符(...):
const arr1 = [11,22,33]
const arr2=[...arr,...arr1]
console.log(arr2)     
//[1, '2', false, null, {a:1}, 11, 22, 33]


7.concat(arr1,arr2)
let combine = arr.concat([11,22,33])
//[1, '2', false, null, {a:1}, 11, 22, 33]


8.slice(start,end):返回一个新数组,start为起始index,end为结束index
let copy = arr.slice()
copy
// [1,'2',false,null,{a:1}]


9.join(str)
let str =arr.join(',')
str
//'1,2,false,,[object Object]'

修改原数组的方法

let arr = [1,'2',false,{a:1},null]

1.push(item): 从数组最后面添加元素(一个或多个),返回值为添加完后的数组的长度
arr.push(666)   // 6
arr // [1,'2',false,{a:1},null,666]

2.unshift(unshift):从前面添加元素, 返回值是添加完后的数组的长度
arr.unshift(666)    // 6
arr  //[666,1,'2',false,{a:1},null]

3.pop():从数组最后面删除一个元素,返回值是删除的元素
arr.pop()    // null
arr  //[1,'2',false,{a:1}]

4.shift():从前面删除元素,只能删除一个,返回值是删除的元素
arr.shift()    // 1
arr  //['2',false,{a:1},null]

5.reverse():颠倒数组中元素的排列顺序
arr.reverse(); 
arr // [null, {a:1}, false,'2',1]

6.sort():直接使用sort()方法,默认的排序方法会将元素转换为字符串,然后比较字符串中字符的UTF-16的编码顺序来进行排序,所以这里接收一个函数,返回值是比较这两个数的相对顺序的值
var arr = [3,4,8,5,1,6,7]
var brr = arr.sort((a,b)=>a-b)
//[1,3,4,5,6,7,8]

7.splice(index,howmany,item1,...,item2):index为起始位置,howmany为处理的数据数量是个number,item1为新元素。若是删除,则返回被删除元素组成的数组,不传值默认全删
let item = arr.splice(0, 3); 
console.log(item);//[1, '2', false]
console.log(arr); [{a:1}, null]

8.fill(value,start,end):将数组中指定区间的所有元素的值,都替换成某个固定的值,并返回这个数组。value是要填充的数字,start是起始索引(数组下标),end是结束索引(数组下标),含头不含尾,如果start/end 为负数,则实际为start+length/end+length
arr.fill(9);// [9, 9, 9, 9, 9]
arr.fill(9,2);// [1, '2', 9, 9, 9]
arr.fill(9,1,3);//[1, 9, 9, {…}, null]
arr.fill(9,3,3);// [1, '2', false, {…}, null]
arr.fill(9,-3,-1);// [1, '2', 9, 9, null] ,可以理解为从数组倒数第三个开始,到数组倒数第一个结束(非数组下标)

9.copyWithin(target,start,end):在当前数组内部,将指定位置的元素复制到其他位置,并返回这个数组。target,索引从该位置开始替换数组项(包含)。start,索引从该位置开始读取数组项,默认为0.如果为负值,则从右往左读,end,索引到该位置停止读取的数组项,默认是Array.length,如果是负值,表示倒数,含头不含尾。
let arr = [1,'2',false,{a:1},null,3,3,3,3,3,3,3,3,3,3]
arr.copyWithin(3,0,4);
// 0,4 读取的是 1,'2',false,{a:1} ,只用这四个值依次替换1遍
// 3 从数组下标为3的位置开始替换(包含)
// [1, '2', false, 1, '2', false, {…}, 3, 3, 3, 3, 3, 3, 3, 3]

遍历方法

let arr = [1,'2',false,{a:1},null]

1.for...of:
for (const value of arr) {
  console.log(value);
}
//1
//'2'
//false
//{a: 1}
//null

2.for...in:尽量别用,Object能用的方法Arry都能用,不过输出的值不一定是你要的
for(let key in arr){
    console.log(arr[key])
}
//1
//'2'
//false
//{a: 1}
//null

3.array.forEach():
arr.forEach((val,index)=>{
    console.log('val:'+val+';index:'+index)
})
//val:1;index:0
//val:2;index:1
//val:false;index:2
//val:[object Object];index:3
//val:null;index:4

4.arr.map():
arr.map((item,index)=>{
    console.log('index',index)
    console.log('item',item)
})
//    index 0    item 1
//    index 1 item 2
//    index 2    item false
//    index 3    item {a: 1}
//    index 4    item null
PS:forEach()和map()的区别
1.forEach()方法不会返回执行结果,而是undefined;而map()方法会得到一个新的数组并返回
2.由于map()底层做了深度优化,同样的一组数组,map()的执行速度优于forEach()

5.arr.every((item,index)=>{
    //这里写判断,需要每一项都符合,最后才返回true,否则返回false
    return item >100
})

6.arr.some((item,index)=>{
    //这里写判断,只要有一项都符合,便true,否则返回false
    return item >100
})

7.arr.filter((item,index)=>{
    //符合条件的,将作为数组的元素,最后返回一个数组
    return typeof item==='number'
})
//[1]

8.arr.reduce((prev,cur,index,arr)=>{
    return prev + cur
},init)
//arr 表示原数组;
//init 表示初始值
//prev 表示上一次调用回调时的返回值,或者初始值 init;
//cur 表示当前正在处理的数组元素;
//index 表示当前正在处理的数组元素的索引,若提供 init 值,则索引为0,否则索引为1;
如果没有init,输出如下:
'null[object Object]false21'

9.arr.reduceRight((prev,cur,index,arr)=>{
    return prev + cur
},init)
//reduceRight()方法的功能和reduce()功能是一样的,不同的是reduceRight()从数组的末尾向前执行

数组合并

数组:
let arr1 = [false,'',1,'5']
let arr2 = [9,'8',true,function(){}]

let arr3 = arr1.concat(arr2); 
//let arr3 = [...arr1,...arr2)];     
//arr1.push(...arr2);  会改变原数组 
//[false,'',1,'5',9,'8',true,function(){}]

数组去重

let obj = {
    a:1,
    b:'zxp',
    c:function(){},
    d:{
        abc:'123',
        qwe:{}
    }
}
let arr = [1,1,1,'chiji',obj,obj,'c','c']

Set对象+Spread扩展运算符
[...new Set(arr)]    //[1,'chiji',obj,'c']

缺点:其实也不能算是缺陷,只是满足不了需求
如果数组如下,
let arr = [1,1,1,'chiji',obj,obj,{a:1},{a:1},'c','c',{}]
[...new Set(arr)]    //[1,'chiji',obj,{a:1},{a:1},'c',{}]
因为本质上两个{a:1}的内存存储地址不一样,所以不会被去重,而往往在需求上,这种数据是需要我们去重的,空对象也是无法被删除的。

PS:这边可以想想,如何去除{}、[]、{a:1}等情况,以及object的去重,搞个公共方法出来

数组扁平化

//1.Array.flat(n)是ES10扁平数组的api,n表示维度,n值为Infinity时维度为无限大
[1,[2,3]].flat(1) //[1,2,3]
[1,[2,3,[4,5]]].flat(2) //[1,2,3,4,5]
[1,[2,3,[4,5]]].toString()  //'1,2,3,4,5'
[1[2,3,[4,5[...]].flat(Infinity) //[1,2,3,4...n]



//2.递归
let arr = [1,[2,3],[4,[5,6]],[7,[8,[9,10]]]]
function toOne(array){
    let newArr = []
    let deepArr = (arr) =>{
        if(typeof arr === 'object'){
            arr.map(item=>{
                let itemType = Object.prototype.toString.call(item)
                if(itemType === '[object Array]'){
                    deepArr(item)
                }else{
                    newArr.push(item)
                }
            })
        }else{
            newArr.push(arr)
        }
      }
      deepArr(array)
      return newArr
}
toOne(arr)    //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

应用

1.关键字

前端关键字有:delete、switch、case、default、break(跳出循环,map方法中不可用)、for、in、continue(跳出本次循环)、while、if、else、return(停止方法的执行)、try、catch、this、do、with、finally、throw、let、var、function、instanceof、typeof、new 等,这边挑选部分进行解释。

delete关键字

let arr = [1,2,3,4,5,6]
let obj= {
    "a":"1",
    "b":"2",
    "c":"3",
    "d":"4",
}
delete arr[0]
console.log(arr) //[empty, 2, 3, 4, 5, 6]
delete obj.b
console.log(obj) // {a: '1', c: '1', d: '1'}

new关键字

接下来描述一些new操作符的的原理、过程及实现:

new操作符的作用是通过构造函数来创建一个实例对象,那么new操作符做的事包括:
1.创建新的对象
2.继承传入函数的原型
3.修改this指向并执行构造函数
4.返回新对象

实现:
function _new(Fn, ...args) {
  let obj = {};                        //创建一个空对象
  obj.__proto__ = Fn.prototype;        //继承函数原型就是,使实例对象的__proto__(原型)= 构造函数的prototype(原型)
//上面两步也可以合并为:let obj=Object.create(Fn.prototype);
  let res = Fn.call(obj, ...args);        //修改this指向并执行构造函数
//上面这步也可以写为:let res = Fn.apply(obj, args);     区别是一个参数为数组,一个参数为xx,xx,xx的形式
  return res instanceof Object ? res : obj;    //如果new作用的目标是基础类型,则返回obj基础类型,如果是引用类型object,则返回res
}

验证:
// 构造函数
function Person(name, age){
    this.name = name;
    this.age = age;
    this.test = {
        a:'test',
        b:function(){}
    }
    return this.test  //如果构造函数有return,且为Object,则p为这个,否则p为obj
}
let p = _new(Person, 'Tom', 20)
console.log(p)
//{a: 'test', b: ƒ}

PS:至于为什么不能写成let p = _new Person('Tom', 20)的形式,是因为_new是我们自定义的方法,需要以方法的形式调用;而let p = new Person(xxxx)之所以可以如此使用,是因为new是ECMA规定的内置语法,如果你非要写成_new Person()形式,就相当于新增了一个内置语法,等同于ES5升级到ES6

2.解构赋值

数组:
let [a, [[b], c]] = [1, [[2], 3]];
// a = 1    b = 2    c = 3

let [a = 1, b] = []; 
// a = 1, b = undefined

let [a, ...b] = [1, 2, 3];
//a = 1    b = [2, 3]

let [a = 3, b = a] = [1, 2];
// a = 1, b = 2

对象:
let obj = {p: ['hello', {y: 'world'}] };
let {p: [x, { y }] } = obj;
// x = 'hello'    y = 'world'

let {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40};
// a = 10     b = 20        rest = {c: 30, d: 40}

let {a: aa = 10, b: bb = 5} = {a: 3};
// aa = 3; bb = 5;

3.深浅拷贝

深浅拷贝的区别在于,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

具体表现即,深拷贝的数据与被拷贝对象完全没有关系,不管被拷贝对象如何改动,都不会影响深拷贝的数据;而浅拷贝的数据如果内层不嵌套,则与深拷贝的表现一致,如果内层嵌套,则被拷贝对象嵌套部分的数据修改会使拷贝对象同步修改。

数据:

let obj = {
    a:1,
    b:'zxp',
    c:function(){},
    d:{
        abc:'123',
        qwe:{}
    }
}
let target = { a: 1, b: 1 };
let arr = [1,'chiji',obj,'c']

浅拷贝:

//Object.assign(target,source):将源对象(source)的所有可枚举属性,复制到目标对象(target),只能拷贝一层。
Object.assign(target, obj);

//数组
let copy = Array.from(new Set(arr));     //   注意:该方法不能用在原数组里有重复项,会被去重
//let copy = [...arr];     
//let copy = arr.slice();
//let copy = Array.of (...arr);
//let copy = new Array(...arr);        
//let copy = arr.concat();//concat() 方法用于连接两个或多个数组        


arr[0] = 2
//arr修改,copy不变
arr[2]['d']['abc'] = 987
//arr修改,copy修改,obj修改    

深拷贝:

let copy = JSON.parse(JSON.stringify(arr)) 
//简便情况下可以使用该方法,但是该方法存在缺陷:null,NaN, undefined, Infinity,Date对象等数据类型在转化过程中会出现丢失、异常等情况,至于具体每个异常情况自己去搜索一下

arr[0] = 2
//arr修改,copy不变
arr[2]['d']['abc'] = 987
//arr修改,copy不变

递归深拷贝:

function deepCopy(data){
    let target 
    if(!data && typeof data !== 'object'){
        //如果是基础类型,那么就直接返回
        target = data
    }else{
        target = data instanceof Array ? [] : {}
        //遍历数据,将key输出为数组,方便进行遍历
        Object.keys(data).forEach((key)=>{
            if(data[key] && typeof data[key] === 'object'){
                //如果数据的值为对象,那么递归继续遍历下去
                target[key] = deepCopy(data[key])
            }else{
                target[key] = data[key]
            }
        })
    }
    return target
}

4.类型判断

let bool = true
let num = 1
let str = 'abc'
let und= undefined
let nul = null
let arr = [1,2,3,4]
let obj = {name:'chiji',age:25}
let fun = function(){console.log('hello')}
let sym = Symbol()

typeof:typeof可以识别出基本类型boolean,number,undefined,string,symbol,但是不能识别引用数据类型,会把null、array、object统一归为object,但是可以识别出function。

console.log(typeof bool)    //boolean
console.log(typeof num)        //number
console.log(typeof str)        //string
console.log(typeof und)        //undefined
console.log(typeof sym)        //symbol
console.log(typeof fun)        //function
console.log(typeof nul)        //object
console.log(typeof arr)        //object
console.log(typeof obj)        //object

instanceof:可以识别引用类型,如array、object、function。

由于js的继承都是采用原型链来继承的,因此使用instanceof还可以检测出new声明的类型的多层继承关系。

console.log(bool instanceof Boolean)        // false
console.log(num instanceof Number)            // false
console.log(str instanceof String)            // false
console.log(und instanceof Object)            // false
console.log(nul instanceof Object)            // false
//此处的基本类型均非通过基本包装类型new生成,因此原型链上找不到原型返回false
console.log(arr instanceof Array)            // true
console.log(obj instanceof Object)            // true
console.log(fun instanceof Function)        // true
console.log(sym instanceof Symbol)            // false

Object.prototype.toString.call:此方法可以相对较全的判断js的数据类型,但是比较长,嫌麻烦的话可以根据场景选择上述方法;如果检测的是个基本类型,实际上会自动转成对应的引用类型,比如string会转成new String(),但对于自定义类型,就只能用 instanceof 了 。

console.log(Object.prototype.toString.call(bool))        //[object Boolean]
console.log(Object.prototype.toString.call(num))        //[object Number]
console.log(Object.prototype.toString.call(str))        //[object String]
console.log(Object.prototype.toString.call(und))        //[object Undefined]
console.log(Object.prototype.toString.call(nul))        //[object Null]
console.log(Object.prototype.toString.call(sym))        //[object Symbol]
console.log(Object.prototype.toString.call(arr))        //[object Array]
console.log(Object.prototype.toString.call(obj))        //[object Object]
console.log(Object.prototype.toString.call(fun))        //[object Function]

Array.isArray:此方法为ES5新增,用于判断JS数组类型

console.log(Array.isArray(arr))        // true

5.intanceof操作符的原理及实现

MDN定义:instanceof运算符用于测试构造函数的prototype属性是否出现在对象原型链中的任何位置
理解:原理也就是前面理解的原型链知识点。这里的主体一个是构造函数的原型,一个是实例对象的原型。如果构造函数的原型在实例对象的原型上,则返回true,反之,false。

left instanceof right //实际上left等同于实例对象,right等同于构造函数

function _instanceof (left, right) {
  let l = left.__proto__;    //实例对象的原型
  while (l) {
    if (right.prototype === l) {
      return true;
    }
    l = l.__proto__;
  }
  return false;
}

const obj = new Object({a:1})
const res = _instanceof(obj,Object)    //此处用法原因与new的PS一致
console.log(res)

最后

走过路过,不要错过,点赞、收藏、评论三连~

你可能感兴趣的:(前端javascript)