基础数据类型:Number、String、Boolean、null、undefined、Symbol、BigInt 引用数据类:Object
由于JavaScript对基数据本类型和引用数据类型的数据处理不同,故出现了深浅拷贝这个问题,基本数据类型的数据存储在栈中,而引用数据类型的真实数据存放在堆中,存储在栈中的是对象的引用地址;而JavaScript不允许我们直接操作内存中的地址,也就是不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。
在复制时,如果我们复制一个基本数据类型的值时,会创建一个新值,并把它保存在新的变量的位置上。而如果我们复制一个引用数据类型的值时,同样会把变量中的值复制一份放到新的变量空间里,但此时复制的东西并不是对象本身,而是指向该对象的指针。所以我们复制引用数据类型后,两个变量其实指向同一个对象,改变其中一个对象,会影响到另外一个。
深浅拷贝针对的是引用数据类型,而他们最直接的区别就是新旧对象是否共用同一块内存。
浅拷贝(潜复制)
浅拷贝:只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
Object.assign
ES6 定义了 Object.assign(..) 方法来实现浅复制。Object.assign(..) 方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举(enumerable)的自有键(owned key)并把它们复制(使用 = 操作符赋值)到目标对象,最后返回目标对象,使用方法如下例子:
var oldObj1 = {a:1};
var oldObj2 = {b:1};
var newObj = Object.assign({},oldObj1,oldObj2);
console.log(newObj);//{a:1,b:1}
当对象是单层对象时,Object.assign能实现深拷贝,而当对象为多层对象时,Object.assign只能视为浅拷贝。
var oldObj1 = {a:1,b:2};
var newObj = Object.assign({},oldObj1);
newObj.a = 'newObj a';
console.log(oldObj1,newObj);
var oldObj2 = {a:1,b:{b1:'b1',b2:'b2'}};
var newObj2 = Object.assign({},oldObj2);
newObj2.a = 'newObj2 a';
newObj2.b.b1 = 'newObj2 b1';
console.log(oldObj2,newObj2);
Es6 扩展运算符
Es6展开语法对单层对象时深拷贝,对多层对象是浅拷贝
var oldObj1 = {a:1,b:2};
var newObj = {...oldObj1};
newObj.a = 'newObj a';
console.log(oldObj1,newObj);
var oldObj2 = {a:1,b:{b1:'b1',b2:'b2'}};
var newObj2 = {...oldObj2};
newObj2.a = 'newObj2 a';
newObj2.b.b1 = 'newObj2 b1';
console.log(oldObj2,newObj2);
jQuery的$.extend(添加true就是深拷贝,否则为浅拷贝)
concat 针对数组
数组的 concat 方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过 concat 只能用于数组的浅拷贝,使用场景比较局限。
let arr1 = [1, 2, 3, { a: 1 }];
let newArr1 = arr1.concat();
newArr1[0] = 100;
newArr1[3].a = 100;
console.log(arr1,newArr1);
slice 针对数组
slice 方法也比较有局限性,因为它仅仅针对数组类型。slice 方法会返回一个新的数组对象,这一对象由该方法的前两个参数来决定原数组截取的开始和结束位置,是不会影响和改变原始数组的。但是,数组元素是引用类型的话,也会影响到原始数组。
let arr1 = [1, 2, 3, [4,5]];
let newArr1 = arr1.slice(0);//Array.slice([开始位置,结束位置])
newArr1[0] = 100;
newArr1[3][0] = 100;
console.log(arr1,newArr1);
Object.create()
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。Object.create(proto,[propertiesObject])接收两个参数一个是新创建对象的__proto__, 一个属性列表
var oldObj2 = {a:1,b:{b1:'b1',b2:'b2'}};
var newObj2 = Object.create(oldObj2,Object.getOwnPropertyDescriptors(oldObj2));
newObj2.a = 'newObj2 a';
newObj2.b.b1 = 'newObj2 b1';
console.log(oldObj2,newObj2);
深拷贝(深复制)
深拷贝:完全克隆一个对象或者数组,所有的嵌套对象和数组也都会被克隆出来,二者之间是完全独立的。
JSON.parse(JSON.stringify(...))
是我们日常开发中所常用的一种能实现深拷贝的方案
如果对象中含以下数据,使用此方法会报错或造成数据不准确:
- 值为Function,Symbol,Undefined,key为Symbol,序列化后会丢失;
- 对象存在循环引用会报错
- Date对象, 序列化后会变为字符串;
- RegExp、Error对象,序列化后得到空对象;
- NaN、Infinity,-Infinity,序列化的结果会变成null
jQuery的$.extend
手写递归
function isObj(tar) {
//判断数据类型
let type = typeof tar;
return tar !== null && (type === 'object' || type === 'function');
}
function initObj(tar) {
//初始化数据
let con = tar.constructor;
return new con();
}
function forEach(arr,fun) {
let index = -1;
let len = arr.length;
while (++index < len){
fun(arr[index],index);
}
return arr;
}
function otherType(type,tar) {
//其他类型数据处理
switch (type) {
case '[object Number]':
case '[object Boolean]':
case '[object String]':
case '[object Error]':
case '[object Date]':
return new tar.constructor(tar);
case '[object RegExp]':
let regFlags = /\w*$/;
let result = new tar.constructor(tar.source,regFlags.exec(tar));
result.lastIndex = tar.lastIndex;
return result;
case '[object Function]':
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /((.?))\s+{/;
const funcString = tar.toString();
if (tar.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
console.log('普通函数',param,body);
if (body) {
console.log('匹配到函数体:', body[0]);
if (param) {
const paramArr = param[0].split(',');
console.log('匹配到参数:', paramArr);
return new Function(...paramArr, body[0]);
} else {
console.log('未匹配到参数:1');
return new Function(body[0]);
}
} else {
console.log('未匹配到参数:2');
return null;
}
} else {
return eval(funcString);
}
case '[object Symbol]':
return Object(Symbol.prototype.valueOf.call(tar));
default:
return null;
}
}
function clone(tar,map = new Map()) {
//判断类型
if(!isObj(tar)) return tar;//原始数据类型 直接返回
//获取详细类型
let type = Object.prototype.toString.call(tar);
let types = ['[object Set]','[object Map]','[object Object]','[object Array]'];
if(types.indexOf(type) < 0){
return otherType(type,tar);
}
//初始化
let obj = initObj(tar);
//循环引用
if(map.get(tar)){
return map.get(tar);
}
map.set(tar,obj);
//开始具体处理
switch (type) {
case '[object Set]':
//set 处理
tar.forEach(val=>{
obj.add(clone(val,map));
});
return obj;
case '[object Map]':
//map 处理
tar.forEach((val,key) =>{
obj.set(key,clone(val,map));
});
return tar;
case '[object Object]':
case '[object Array]':
//数组 对象处理
let keys = type === '[object Array]' ? undefined : Object.keys(tar);
forEach(keys || tar,(val,key)=>{
if(keys) key = val;
obj[key] = clone(tar[key],map);
});
return obj;
}
}
//测试数据
function text() {
const map = new Map();
map.set('key', 'value');
map.set('China', 'ChengDu');
const set = new Set();
set.add('China');
set.add('ChengDu');
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8],
empty: null,
map,
set,
bool: new Boolean(true),
num: new Number(2),
str: new String(2),
symbol: Object(Symbol(1)),
date: new Date(),
reg: /\d+/,
error: new Error(),
func1: () => {
console.log('China ChengDu');
},
func2: function (a, b) {
return a + b;
}
};
let newtar = clone(target);
console.log(target,newtar);
}
text();