js数据类型的前世今生( 数据类型判断、浅拷贝、深拷贝)
js数据类型
首先我们需要知道的是js数据类型分为 基本数据类型 和 引用数据类型 ,我们说的深拷贝或浅拷贝都是针对的 引用数据 。
基本数据类型
- String类型
- Null类型
- Undefined类型
- Boolean
- 数字类型(Number、BigInt)
- 符号类型(Symbol)
引用数据类型
- Object
- Data
- Function
- RegExp
- Array
- ....
两者的区别
基本数据类型因为其占用空间小,大小固定等特性,js在保存时会直接将其保存到栈内存中,而引用数据类型则因为占据空间大、占用内存不固定,直接保存到栈内存会程序运行的性能,所以引用数据类型会直接在堆内存中创建,栈内存保存指针,通过指针在堆内存中获得实体。
可以参考这张图:
所以一般说到深拷贝和浅拷贝,我们针对的是引用数据类型
怎样判断我们的数据类型是哪种?
1. typeof
typeof 运算符返回一个字符串,表示操作数的类型。
我们一般用于检测我们的基本数据类型
console.log(typeof 42); // "number"
console.log(typeof 'blubber'); // "string"
console.log(typeof true);// "boolean"
console.log(typeof undeclaredVariable); // "undefined"
// 由于js历史原因,typeof null时,会返回“object”
console.log(typeof null); // "object"
下表罗列了**typeof**的返回值
| 类型 | 结果 |
|---|---|
| Undifined | “undifined” |
| Boolean | "boolean" |
| Number | "Number" |
| BigInt | "bigInt" |
| String | "string" |
| Symbol | "Symbol" |
| Null | "object" 原因 |
| Function | "function" |
| 其他任何对象 | "Object" |
2.instanceof
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
instanceof返回一个Boolean值
let obj = {}
obj instanceof Object // true
let arr = []
arr instanceof Array // true
const fun = function() {}
fun instanceof Function // true
const date = new Date()
date instanceof Date // true
判断基本数据类型时,一定要通过new关键词实例化才是正确的
let str1 ='abc'
let str2 = new String('abc');
str1 instanceof String // false
str2 instanceof String // true
所以一般**instanceof**我们用于判断引用数据类型
3.Object.prototype.toString()
该方法统一返回 [object Xxx]的字符串,调用该方法时我们需要使用call方法改变this指向问题,否则永远指向Object.prototype
// 未使用call方法
Object.prototype.toString([1, 2, 3]); // "[object Object]"
Object.prototype.toString({}); // "[object Object]"
Object.prototype.toString(new Date()); // "[object Object]"
// 使用call方法
// 引用类型
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call({}) // '[object Object]'
Object.prototype.toString.call(function(){}) // "[object Function]'
Object.prototype.toString.call(/test/g) // '[object RegExp]'
Object.prototype.toString.call(new Date()) // '[object Date]'
Object.prototype.toString.call(new Error()) // '[object Error]'
Object.prototype.toString.call(new Map()) // '[object Map]'
Object.prototype.toString.call(new Set()) // '[object Set]'
Object.prototype.toString.call(new WeakMap()) // '[object WeakMap]'
Object.prototype.toString.call(new WeakSet()) // '[object WeakSet]'
Object.prototype.toString.call(document) // '[object HTMLDocument]'
Object.prototype.toString.call(window) // '[object Window]'
// 原始类型
Object.prototype.toString.call(1) // '[object Number]'
Object.prototype.toString.call('1') // '[object String]'
Object.prototype.toString.call(true) // '[object Boolean]'
Object.prototype.toString.call(1n) // '[object BigInt]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
Object.prototype.toString.call(Symbol('a')) // '[object Symbol]'
4.我们封装一个自己的方法(取长补短)
function getDataType(data) {
// 先处理基本数据类型
// 处理null
if (data === null) return null;
// 处理非null情况 (基本数据类型、function)
let type = typeof data;
if (type !== 'object') return type;
// 处理引用类型
let reg = /^[object (\S*)]$/;
// 统一返回小写
return Object.prototype.toString.call(data).replace(reg, '$1').toLocaleLowerCase();
}
测试一下
// 基本数据类型
console.log(getDataType('a'));
console.log(getDataType(null));
console.log(getDataType(undefined));
console.log(getDataType(true));
console.log(getDataType(1));
console.log(getDataType(Symbol('symbol')));
// 引用数据类型
console.log(getDataType({}));
console.log(getDataType([1, 2, 3]));
console.log(getDataType(function () {}));
console.log(getDataType(new Date()));
console.log(getDataType(new RegExp()));
输出结果
string
null
undefined
boolean
number
symbol
object
array
function
date
regexp
浅拷贝与深拷贝
前置知识
由上文介绍的引用数据类型,我们可以得知,引用数据类型保存的时候,仅保存其内存地址,所以当我们改变同一个内存地址的一个值时,所有被赋值的引用数据类型都会发生变化
const student1 = {
name: '小在',
age: 18,
};
const student2 = student1;
student1.age = 20;
console.log(student1); //{ name: '小在', age: 20 }
console.log(student2); //{ name: '小在', age: 20 }
console.log(student1 == student2); // true
这种情况下,在实际业务中肯定会存在很多问题,所以我们保存两份数据,每个份数据互不干扰
浅拷贝
顾名思义,浅拷贝,就是浅浅的拷贝一层,像上面的student1,所有的value值是基本数据类型,我们通过浅拷贝就可以实现
1.Object.assign
const student1 = {
name: '小在',
age: 18,
};
const student2 = Object.assign({}, student1);
student1.age = 20;
console.log(student1);//{ name: '小在', age: 20 }
console.log(student2);//{ name: '小在', age: 18 }
const subject1 = ['语文', '数学', '英语'];
const subject2 = Object.assign([], subject1);
subject1[0] = '化学';
console.log(subject1); //[ '化学', '数学', '英语' ]
console.log(subject2); //[ '语文', '数学', '英语' ]
2.扩展运算符 ...
const student1 = {
name: '小在',
age: 18,
};
const student2 = { ...student1 };
student1.age = 20;
console.log(student1);//{ name: '小在', age: 20 }
console.log(student2);//{ name: '小在', age: 18 }
const subject1 = ['语文', '数学', '英语'];
const subject2 = [...subject1];
subject1[0] = '化学';
console.log(subject1); //[ '化学', '数学', '英语' ]
console.log(subject2); //[ '语文', '数学', '英语' ]
3.数组的一些自身方法Array.from() 、slice、concat
const subject1 = ['语文', '数学', '英语'];
const subject2 = Array.from(subject1);
const subject3 = subject1.slice(0);
const subject4 = [].concat(subject1);
subject1[0] = '化学';
console.log(subject1); //[ '化学', '数学', '英语' ]
console.log(subject2); //[ '语文', '数学', '英语' ]
console.log(subject3); //[ '语文', '数学', '英语' ]
console.log(subject4); //[ '语文', '数学', '英语' ]
浅拷贝存在一种问题,就是当我们的引用数据类型里面又包含一个引用数据类型,那么还会出现指针指向同一个内存的问题
const student1 = {
name: '小在',
age: 18,
area: {
province: '北京',
city: '北京市',
region: '朝阳区',
},
};
const student2 = { ...student1 };
student1.area.province = '上海';
student1.area.city = '上海市';
student1.area.region = '徐汇区';
console.log(student1);
// {
// name: '小在',
// age: 18,
// area: { province: '上海', city: '上海市', region: '徐汇区' }
// }
console.log(student2);
// {
// name: '小在',
// age: 18,
// area: { province: '上海', city: '上海市', region: '徐汇区' }
// }
深拷贝
当出现
1.JSON.parse(JSON.stringify(obj))
既然基本数据类型不存在指针问题,可以单独存储到内存中,那么我们就可以将引用数据类型先转化为基本数据类型,然后再转回到引用数据,就可以解决
const student1 = {
name: '小在',
age: 18,
area: {
province: '北京',
city: '北京市',
region: '朝阳区',
},
};
const student2 = JSON.parse(JSON.stringify(student1));
student1.area.province = '上海';
student1.area.city = '上海市';
student1.area.region = '徐汇区';
console.log(student1);
// {
// name: '小在',
// age: 18,
// area: { province: '上海', city: '上海市', region: '徐汇区' }
// }
console.log(student2);
// {
// name: '小在',
// age: 18,
// area: { province: '北京', city: '北京市', region: '朝阳区' }
// }
// 但是这样有一个问题,当我们的对象中有symbol,undefined,function时,此方法无法转化
const student3 = {
name: '小在',
age: 18,
class: undefined,
id: Symbol('001'),
skill: function () {
console.log('say hello');
},
};
const student4 = JSON.parse(JSON.stringify(student3));
console.log(student3);
// {
// name: '小在',
// age: 18,
// class: undefined,
// id: Symbol(001),
// skill: [Function: skill]
// }
console.log(student4);
// { name: '小在', age: 20 }
2.我们封装一个自己的方法
先上最终效果,我们在一步一步讲
function deepClone(targe) {
// 特殊处理null
if (targe === null) return targe;
// 基本数据类型
if (typeof targe !== 'object') return targe;
// 特殊处理date
if (targe instanceof Date) return new Date(targe);
// 特殊处理正则
if (targe instanceof RegExp) return new RegExp(targe);
// 判断targe类型为object或array,动态创建{}或[]
const cloneTarge = new targe.constructor();
// 使用Reflect.ownKeys属性可获取包括Symbol类型在内的可枚举属性
Reflect.ownKeys(targe).forEach((key) => {
cloneTarge[key] = deepClone(targe[key]);
});
return cloneTarge;
}
先写一个最简单的拷贝方法,通过for...in遍历值,赋给新值
function deepClone(target) {
const newTarget = {};
for (const key in target) {
newTarget[key] = target[key];
}
return newTarget;
}
这样只是最基础的浅拷贝,我们通过递归解决一下对象内部的引用类型
function deepClone(target) {
// 特殊处理null
if (target === null) return target;
// 基本数据类型
if (typeof target !== 'object') return target;
const newTarget = {};
for (const key in target) {
newTarget[key] = deepClone(target[key]);
}
return newTarget;
}
我们再加一些特殊的引用数据类型处理,如Date 、RegExp
function deepClone(target) {
// 特殊处理null
if (target === null) return target;
// 基本数据类型
if (typeof target !== 'object') return target;
// 特殊处理date
if (target instanceof Date) return new Date(target);
// 特殊处理正则
if (target instanceof RegExp) return new RegExp(target);
const newTarget = {};
for (const key in target) {
newTarget[key] = deepClone(target[key]);
}
return newTarget;
}
我们在处理一下引用数据类型为数组的情况,可以通过target.constructor来为Object或Array
function deepClone(target) {
// 特殊处理null
if (target === null) return target;
// 基本数据类型
if (typeof target !== 'object') return target;
// 特殊处理date
if (target instanceof Date) return new Date(target);
// 特殊处理正则
if (target instanceof RegExp) return new RegExp(target);
// 判断targe类型为object或array,动态创建{}或[]
const newTarget = new target.constructor();
for (const key in target) {
newTarget[key] = deepClone(target[key]);
}
return newTarget;
}
当对象的键位symbol类型时,我们也需要处理一下
function deepClone(target) {
// 特殊处理null
if (target === null) return target;
// 基本数据类型
if (typeof target !== 'object') return target;
// 特殊处理date
if (target instanceof Date) return new Date(target);
// 特殊处理正则
if (target instanceof RegExp) return new RegExp(target);
// 判断targe类型为object或array,动态创建{}或[]
const newTarget = new target.constructor();
// 使用Reflect.ownKeys属性可获取包括Symbol类型在内的可枚举属性
Reflect.ownKeys(target).forEach((key) => {
newTarget[key] = deepClone(target[key]);
});
return newTarget;
}
我们验证一下
const student1 = {
name: '小在',
age: 18,
area: {
province: '北京',
city: '北京市',
region: '朝阳区',
},
class: undefined,
id: Symbol('001'),
skill: function () {
console.log('say hello');
},
birthday: new Date(),
regExp: /^1/,
specialty: ['游泳', '跑步'],
};
let symbolIDCard = Symbol('IDCard');
student1[symbolIDCard] = '10086';
let student2 = deepClone(student1);
student1.area.province = '上海';
console.log(student1);
// {
// name: '小在',
// age: 18,
// area: { province: '上海', city: '北京市', region: '朝阳区' },
// class: undefined,
// id: Symbol(001),
// skill: [Function: skill],
// birthday: 2022-10-20T10:32:39.645Z,
// regExp: /^1/,
// specialty: [ '游泳', '跑步' ],
// [Symbol(IDCard)]: '10086'
// }
console.log(student2);
// {
// name: '小在',
// age: 18,
// area: { province: '北京', city: '北京市', region: '朝阳区' },
// class: undefined,
// id: Symbol(001),
// skill: [Function: skill],
// birthday: 2022-10-20T10:32:39.645Z,
// regExp: /^1/,
// specialty: [ '游泳', '跑步' ],
// [Symbol(IDCard)]: '10086'
// }
3.使用第三方插件库lodash
import { cloneDeep } from 'lodash';
let student2 = deepClone(student1);