JavaScript对象的拷贝

本文主要介绍js对象的拷贝,包括浅拷贝和深拷贝,侧重实际方式,简单介绍概念。

一、js数据类型

js数据有不同的划分方式,此处以数据在内存中的储存形式划分,可以分为两种类型:基本类型和复杂类型

  1. 基本类型主要包括:stringnumberbooleanundefinednullsymbol
  2. 复杂类型主要包括:objectarrayfunction

二、js中的栈堆

在js引擎中对数据的存储主要有两种位置,堆内存和栈内存。

栈内存主要用于存储各种基本类型的变量,包括stringnumberbooleanundefinednullsymbol,以及复杂类型的指针

而堆内存主要负责复杂数据(objectarrayfunction)的内容部分的存储。

如图:


栈内存和堆内存

栈:

  1. 由系统自动分配释放;
  2. 一般有固定大小和范围上限;
  3. 存放基本数据或复杂数据的指针等;
  4. 读取顺序是先进后出。

堆:

  1. 一般由程序员分配释放, 若程序员不释放,程序结束时可能由系统回收;
  2. 存储大小未知,由程序执行时产生的数据大小自动确定;
  3. 用于存放复杂数据;
  4. 读取顺序是先进先出。

三、数据的拷贝

数据拷贝,一般指栈堆中的拷贝。如果是基本数据,那么拷贝的就是数据;如果是复杂数据,那么拷贝的是指针,可能会导致多个指针指向同一个对象。

具体体现为,基本数据拷贝后,修改拷贝后的数据,不会被影响原数据;复杂数据拷贝后,修改拷贝后的数据,会作用到原数据,因为此时修改的是同一个指针指向的数据。

1. 基本数据的拷贝
var a = 10;
var b = a;
console.log(a);  // 10
console.log(b);  // 10

var c = 10;
var d = c;       // 拷贝的是数据
d = 20;          // 修改拷贝之后的数据
console.log(c);  // 10
console.log(d);  // 20
2. 复杂数据的拷贝
var obj = { a: 10 };
var obj2 = obj;
console.log(obj);  // { a: 10 }
console.log(obj2); // { a: 10 }

var obj = { a: 10 };
var obj2 = obj;    // 拷贝的只是指针,不是数据
obj2.a = 20;       // 修改拷贝之后的数据
console.log(obj);  // { a: 20 }
console.log(obj2); // { a: 20 } 

四、对象的拷贝

所谓对象的拷贝,其实就是基于复杂数据在拷贝时的异常处理,我们将复杂数据的默认拷贝定义为浅拷贝;就是只拷贝复杂数据的地址,而不拷贝值。那么与之对应的就是不拷贝地址,只拷贝值,也被称为深拷贝。

一般情况下,等号赋值,函数传参,都是浅拷贝,也就是只拷贝了数据的地址。

let foo = {
    title:"hello obj"
}

// 等号赋值
let now = foo;
now.title = "this is new title";

console.log(foo.title);       // this is new title
console.log(now.title);       // this is new title

// 函数传参
function change(o){
    o.title = "this is function change title";
}
change(foo);
console.log(foo.title);       // this is function change title

如果要实现拷贝数据的值,需要借助一些措施,或处理方式。如:

1. 遍历复杂数据,拷贝内部属性或值
let foo = {
    title:"hello obj"
}

let now = {};
for(let attr in foo){
    now[attr] = foo[attr];
}
// 深拷贝成功
console.log(foo === now);     // false
now.title = "this is new title";

console.log(foo.title);       // hello obj
console.log(now.title);       // this is new title

这种方式比较复杂,并且在遇到多层复杂数据嵌套时,只能深拷贝一层数据,如果修改内层复杂数据,依然会影响原数据,无法解决本质问题

let foo = {
    title:{
        sTitle:"this is small title"
    }
}
let now = {};
for(let attr in foo){
  now[attr] = foo[attr];
}
// 外层对象深拷贝成功
console.log(foo === now);                // false
// 内层对象依然是浅拷贝
console.log(foo.title === now.title);    // true 
now.title.sTitle = "this is new small title";

console.log(foo.title.sTitle);           // this is new small title
console.log(now.title.sTitle);           // this is new small title
但可以利用函数递归的方式解决这个问题 ---- 强烈推荐
function deepClone(target){
    // 判断要处理的目标是数组还是对象
    let newObj = target.constructor === Array ? [] : {};
    // 遍历目标:in运算符会拿到原型上继承到的属性
    for (let attr in target){
        // 判断属性是否在当前对象自身
        if (target.hasOwnProperty(attr)) {
            // 如果值是对象或数组,就递归一下
            if (target[attr] && typeof target[attr] === "object") {
                newObj[attr] = target[attr].constructor === Array ? [] : {};
                newObj[attr] = deepClone(target[attr]);
            }else if(typeof target[attr] === "function"){
                // 如果是函数,就复制一个新函数
                newObj[attr] = target[attr].bind(target);
            }else{
                // 如果不是对象,数组,函数,就直接赋值
                newObj[attr] = target[attr];
            }
        }
    }
    return newObj;
}

// 测试
let foo = {
    title:{
        sTitle:"this is small title",
        show:function(){
            console.log(this);
        },
        list:["hello","world",{a:10,b:20}]
    }
}

let now = deepClone(foo);

// 外层对象深拷贝成功
console.log(foo === now);                // false
// 内层对象深拷贝成功
console.log(foo.title === now.title);    // false
// 内层函数深拷贝成功
console.log(foo.title.show === now.title.show);     // false
// 内层数组深拷贝成功
console.log(foo.title.list === now.title.list);     // false
// 内层属性深拷贝成功
now.title.sTitle = "this is new small title";
console.log(foo.title.sTitle);           // this is small title
console.log(now.title.sTitle);           // this is new small title

这样一来,这种深拷贝,就是最安全的方式 ---- 强烈推荐

2. 利用js中对json的解析方法
let foo = {
    title:{
        sTitle:0,
        list:[1,2,{a:3,b:4}]
    }
}

let now = JSON.parse(JSON.stringify(foo));

console.log(foo);            // {title: {sTitle: 0, list: [1, 2, {a: 3, b: 4}]}}
console.log(now);            // {title: {sTitle: 0, list: [1, 2, {a: 3, b: 4}]}}
// 深拷贝成功
console.log(foo === now);    // false

可以拷贝多层对象,但受json数据的限制,无法拷贝函数,undefined,NaN属性

let foo = {
    title:{
        show:function(){},
        num:NaN,
        empty:undefined
    }
}

let now = JSON.parse(JSON.stringify(foo));

// 虽然实现了深拷贝,但show和empty属性丢失,num属性被转成了null
console.log(foo);            // {title: {{num: NaN, empty: undefined, show: ƒ}}}
console.log(now);            // {title: {num: null}}
console.log(foo === now);    // false
3. 利用ES6 提供的 Object.assign()
let foo = {
    title:{
        show:function(){},
        num:NaN,
        empty:undefined
    }
}

let now = {};

Object.assign(now, foo);

console.log(foo);   // {title: {{num: NaN, empty: undefined, show: ƒ}}}
console.log(now);   // {title: {{num: NaN, empty: undefined, show: ƒ}}}

// 外层对象深拷贝成功
console.log(foo === now);                // false
// 内层对象依然是浅拷贝
console.log(foo.title === now.title);    // true 

只能可以拷贝一层数据,无法拷贝多层数据

4. 利用ES6 提供的展开运算符:...
let foo = {
    title:{
        show:function(){},
        num:NaN,
        empty:undefined
    }
}

let now = {...foo};

console.log(foo);   // {title: {{num: NaN, empty: undefined, show: ƒ}}}
console.log(now);   // {title: {{num: NaN, empty: undefined, show: ƒ}}}

// 外层对象深拷贝成功
console.log(foo === now);                // false
// 内层对象依然是浅拷贝
console.log(foo.title === now.title);    // true 

只能可以拷贝一层数据,无法拷贝多层数据

五、总结

  1. 除了以上拷贝方式之外,数组还有slice方法,concat方法等,也可以实现深拷贝,但只能对数组进行操作,不能操作对象;
  2. 直接使用赋值运算符复制对象,只拷贝了地址,没有拷贝值,是浅拷贝;
  3. 直接遍历对象,使用赋值运算符拷贝属性,只能拷贝单层数据,无法拷贝多层数据;
  4. json数据和对象的解析,可以拷贝多层数据,但是如果对象中存在functionundefinedNaN,就会发生数据丢失;
  5. js自身提供的诸多方法,如:Object.assign,展开运算符...,数组的slice,数组的concat等,只能拷贝单层数据,无法拷贝多层数据;
  6. 使用递归思想,封装函数,借助赋值运算符,才是真正的深拷贝。最终解决方案:递归!

你可能感兴趣的:(JavaScript对象的拷贝)