目录
一、数据类型:
基本数据类型:String、Boolean、Undefined、null、Number。
引用数据类型:Object(函数Functi on和数组Array是一种特别的对象)。
代码演示:
基本类型的复制:从一个变量向另外一个新变量复制基本类型的值,会创建这个值的一个副本,并将该副本复制给新变量。
let foo = 1;
let bar = foo;
console.log(foo===bar);
// 修改foo变量的值并不会影响bar变量的值
foo = 2;
console.log("foo---"+foo);
console.log("bar---"+bar);
引用类型的复制:从一个变量向另一个新变量复制引用类型的值,其实复制的是指针,最终两个变量都指向同一个对象。
let foo1 = {
name:"luobotou",
age:20
}
let bar1 = foo1;
console.log(foo1==bar1);
// 改变foo1变量的值会影响bar1变量的值
foo1.age = 19;
console.log(foo1);
console.log(bar1);
二、浅拷贝:
1. 仅仅是复制了引用,彼此之间的操作会互相影响。
2. 对对象而言,它的第一层属性值如果是基本数据类型则完全拷贝一份数据,如果是引用数据类型就拷贝内存地址值。
代码演示:
(1)对象浅拷贝:创建一个对象me把对象me的数据通过connect方法拷贝到对象me2中。
var me = {
name:"萝卜头",
age:19,
address:{
home:"HeNan"
}
};
var me2 = {
sex:"男"
}
function connect(old,n){
var n = n || {};
for(var i in old){
n[i] =old[i]
}
}
connect(me,me2);
console.log("修改值前---------");
console.log(me);
console.log(me2);
console.log("修改值后---------");
me.name="大萝卜头"
console.log(me);
console.log(me2);
可以看到老的对象已经完全拷贝到新的对象中。这是因为浅拷贝对于对象而言:它的第一层属性值如果是基本数据类型则完全拷贝一份数据,如果是引用数据类型就拷贝内存地址值。
拷贝完修改老的对象里面的name,但是新的对象却没有改变。
(2)通过Object.assign():方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象。第一个参数是目标对象后面的参数都是源对象。
let obj1 = {
name:"萝卜头",
res:{
value:123
}
}
let obj2 = Object.assign({},obj1);
obj2.res.value = 456;
console.log("是引用数据类型就拷贝内存地址");
console.log(obj2);
console.log(obj1);
console.log("第一层属性值是基本数据类型则完全拷贝一份数据");
obj2.name='haha';
console.log(obj2);
console.log(obj1);
(3)ES6的扩展运算符(spread)三个点:
let obj1 = {
name:"萝卜头",
res:{
value:123
}
}
let {...obj2} = obj1;
obj2.res.value = 456;
console.log(obj2);
console.log(obj1);
obj2.name="haha"
console.log(obj2);
console.log(obj1);
修改一个对象里面的value值,两个对象都会发生改变,因为其地址值相同。
修改一个对象的里面的简单数据类型的值另一个对象不会发生改变,因为是完全拷贝一份数据。
(4)Array.prototype.slice([begin[,end]]):
begin:提取起始处的索引(从0开始),从该索引开始提取原数组元素。如果该参数为负数,则表示从原数组中倒数第几个元素开始提取,slice(-3)表示提取原数组倒数第三个到最后一个元素(包含最后一个元素)。
如果省略begin,则slice从索引0开始。
如果begin超出原数组的索引范围,则会返回空数组。
end:提取终止处的索引(从0开始),在该索引处结束提取原数组元素。slice会提取原数组中索引begin到end的所有元素(包含begin,但不包含end)。如果该参数为负数,则表示从原数组中倒数第几个元素开始提取,slice(-3,-1)表示提取原数组倒数第三个到最后一个元素(不包含最后一个元素)。
如果end被省略,则slice会一直提取到原数组末尾。
如果end大于数组的长度,slice也会一直提取到原数组末尾。
console.log("Array.prototype.slice");
const arr1 = [
"萝卜头",
{
value:123
}
];
const arr2 = arr1.slice(0);
arr2[0] = "大萝卜头";
console.log(arr1);
console.log(arr2);
slice
不会修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。原数组的元素会按照下述规则拷贝:
如果该元素是个对象引用 (不是实际的对象),slice
会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
对于字符串、数字及布尔值来说(不是 String
、Number
或者 Boolean
对象),slice
会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
资料参考:MDN
(5)Arrays.property.concat():该concat()
方法用于合并两个或多个数组。此方法不更改现有数组,而是返回一个新数组。
该concat
方法不会更改this
或作为参数提供的任何数组,而是返回一个浅表副本,其中包含与原始数组组合的相同元素的副本。原始数组的元素被复制到新数组中 。
const arr1 = [
'萝卜头',
{
value:123
}
];
const arr2 = [].concat(arr1);
arr2[1].value = 456;
arr2[0] = "哈哈";
console.log(arr1);
console.log(arr2)
对象引用(而不是实际对象):将concat
对象引用复制到新数组中。原始数组和新数组都引用同一对象。也就是说,如果修改了引用的对象,则更改对新数组和原始数组均可见。这包括也是数组的数组参数元素。
数据类型,例如字符串,数字和布尔值(不是 String
,Number
和Boolean
对象): concat
将字符串和数字的值复制到新数组中。
资料参考:MDN。
三、深拷贝:
1. 深拷贝就是不管是基本数据类型还是引用数据类型都重新拷贝一份,不存在共用数据的现象。
2. 考虑到我们要拷贝的对象不知道有多少层深度,我们可以用递归来解决问题。如果是简单数据类型,无需拷贝,直接返回。如果是引用类型,创建一个对象,遍历需要克隆的对象,将需要克隆的对象的属性执行深拷贝后依次添加到新对象上。
JS数组中实现深拷贝的方法有多种,比如JSON.parse(JSON.stringify())和递归以及JQuery库的extend方法 都是可以实现数组和对象的深拷贝的。但是光这一点是远远不够的。
var arr1 = ['red','green'];
var arr2 = JSON.parse(JSON.stringify(arr1)); //复制
console.log(arr2);
console.log("改变color1的值");
arr1.push("black");
console.log(arr2);
console.log(arr1);
(1)最简单的深拷贝: 用递归解决问题,但是有很多的缺陷(没有考虑数组、循环引用等问题)。
const target = {
field1: 1,
field2: undefined,
field3: '萝卜头',
field4: {
child: 'child',
child2: {
child2: 'child2'
}
}
};
function clone(target){
if(typeof target === "object"){
// 创建一个对象
let cloneTarget = {};
// 遍历需要克隆的对象
for(const key in target){
cloneTarget[key] = clone(target[key])
}
return cloneTarget
}else{
// 如果是原始类型,无需拷贝,直接返回。
return target;
}
}
bb = clone(target);//先克隆
target.field1="2"//在改变原来值
console.log(target); //file1:2
console.log(bb);//file1:1
(2) 考虑数组。
第一种:
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
function clone(target){
let cloneTarget = Array.isArray(target)?[]:{};
if(typeof target === "object"){
for(const key in target){
cloneTarget[key] = clone(target[key])
}
return cloneTarget;
}else{
return target;
}
}
bb = clone(target);//先克隆
target.field1="8";
target.field4[0]=1;
console.log(target);
console.log(bb);
改变原target里面的field1和field4里面的数组值,克隆的bb里面的值并不会发生改变。
第二种:
function deepClone(obj){
let objClone = obj instanceof Object?[]:{};
if(obj && typeof obj === "object"){
for(key in obj){
if(obj.hasOwnProperty(key)){
// 判断obj子元素是否为对象,如果是,递归复制
if(obj[key] && typeof obj[key] === "obj"){
objClone[key] = deepClone(obj[key]);
}else{
// 如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
var a = {
x:1,
y:2
}
b = deepClone(a);
console.log(b);
a.x = 3;
console.log(a);
console.log(b);
(3)考虑循环引用问题。
什么是循环引用?
举个简单的例子:
var obj = {};
obj.b = obj;
当我们深拷贝obj对象时,就会循环的遍历b属性直到栈溢出。
我们的解决方案为:额外开辟一个存储空间,来存储当前对象和拷贝关系对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话就继续拷贝。
function clone(target,map = new Map()){
//map是:额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
if(typeof target === "object"){
// /拷贝对象
let cloneT = Array.isArray(target)?[]:{};
// get() 方法用来获取一个 Map 对象中指定的元素。
//当需要拷贝关系时,先去存储空间找,有没有拷贝过这个对象,如果有的话直接返回
if(map.get(target)){
return map.get(target)
}
// set是添加指定的键和元素 没有就继续拷贝。
map.set(target,cloneT);
for(const key in target ){
cloneT[key] = clone(target[key],map);
}
return cloneT;
}else{
return target;
}
}
var A = {a:1};
A.A = A;
var B = clone(A);
console.log(B);