资讯专栏INFORMATION COLUMN

JavaScript 中的深浅拷贝

Tonny / 3017人阅读

摘要:基本类型指的是简单的数据段,而引用类型指的是一个对象保存在堆内存中的地址,不允许我们直接操作内存中的地址,也就是说不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。

工作中经常会遇到需要复制 JavaScript 数据的时候,遇到 bug 时实在令人头疼;面试中也经常会被问到如何实现一个数据的深浅拷贝,但是你对其中的原理清晰吗?一起来看一下吧!
一、为什么会有深浅拷贝

想要更加透彻的理解为什么 JavaScript 会有深浅拷贝,需要先了解下 JavaScript 的数据类型有哪些,一般分为基本类型(Number、String、Null、Undefined、Boolean、Symbol )和引用类型(对象、数组、函数)。

基本类型是不可变的,任何方法都无法改变一个基本类型的值,也不可以给基本类型添加属性或者方法。但是可以为引用类型添加属性和方法,也可以删除其属性和方法。

基本类型引用类型在内存中的存储方式也大不相同,基本类型保存在栈内存中,而引用类型保存在堆内存中。为什么要分两种保存方式呢? 因为保存在栈内存的必须是大小固定的数据,引用类型的大小不固定,只能保存在堆内存中,但是我们可以把它的地址写在栈内存中以供我们访问。

说来这么多,我们来看个示例:

let num1 = 10;
let obj1 = {
    name: "hh"
}

let num2 = num1;
let obj2 = obj1;

num2 = 20;
obj2.name = "kk";

console.log(num1); // 10
console.log(obj1.name); // kk

执行完这段代码,内存空间里是这样的:

可以看到 obj1 和 obj2 都保存了一个指向该对象的指针,所有的操作都是对该引用的操作,所以对 obj2 的修改会影响 obj1。

小结:

之所以会出现深浅拷贝,是由于 JS 对基本类型引用类型的处理不同。基本类型指的是简单的数据段,而引用类型指的是一个对象保存在堆内存中的地址,JS 不允许我们直接操作内存中的地址,也就是说不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。

在复制时也是一样,如果我们复制一个基本类型的值时,会创建一个新值,并把它保存在新的变量的位置上。而如果我们复制一个引用类型时,同样会把变量中的值复制一份放到新的变量空间里,但此时复制的东西并不是对象本身,而是指向该对象的指针。所以我们复制引用类型后,两个变量其实指向同一个对象,所以改变其中的一个对象,会影响到另外一个。

二、深浅拷贝 1. 浅拷贝

浅拷贝只是复制基本类型的数据或者指向某个对象的指针,而不是复制对象本身,源对象和目标对象共享同一块内存;若对目标对象进行修改,存在源对象被篡改的可能。

我们来看下浅拷贝的实现:

/* sourceObj 表示源对象
 * 执行完函数,返回目标对象
*/
function shadowClone (sourceObj = {}) {
    let targetObj = Array.isArray(sourceObj) ? [] : {};
    let copy;
    for (var key in sourceObj) {
        copy = sourceObj[key];
        targetObj[key] = copy;
    }
    return targetObj;
}
// 定义 source
let sourceObj = {
    number: 1,
    string: "source1",
    boolean: true,
    null: null,
    undefined: undefined,
    arr: [{name: "arr1"}, 1],
    func: () => "sourceFunc1",
    obj: {
        string: "obj1",
        func: () => "objFunc1"
    }
}

// 拷贝sourceObj
let copyObj = shadowClone(sourceObj);

// 修改 sourceObj
copyObj.number = 2;
copyObj.string = "source2";
copyObj.boolean = false;
copyObj.arr[0].name = "arr2";
copyObj.func = () => "sourceFunc2";
copyObj.obj.string = "obj2";
copyObj.obj.func = () => "objFunc2";

// 执行
console.log(sourceObj);
/* {
    number: 1,
    string: "source1",
    boolean: true,
    null: null,
    undefined: undefined,
    arr: [{name: "arr2"}],
    func: () => "sourceFunc1",
    obj: {
        func: () => "objFunc2",
        string: "obj2"
    }
}
*/
2. 深拷贝

深拷贝能够实现真正意义上的对象的拷贝,实现方法就是递归调用“浅拷贝”。深拷贝会创造一个一模一样的对象,其内容地址是自助分配的,拷贝结束之后,内存中的值是完全相同的,但是内存地址是不一样的,目标对象跟源对象不共享内存,修改任何一方的值,不会对另外一方造成影响。

/* sourceObj 表示源对象
 * 执行完函数,返回目标对象
*/
function deepClone (sourceObj = {}) {
    let targetObj = Array.isArray(sourceObj) ? [] : {};
    let copy;
    for (var key in sourceObj) {
        copy = sourceObj[key];
        if (typeof(copy) === "object") {
            if (copy instanceof Object) {
                targetObj[key] = deepClone(copy);
            } else {
                targetObj[key] = copy;
            } 
        } else if (typeof(copy) === "function") {
            targetObj[key] = eval(copy.toString());
        } else {
            targetObj[key] = copy;
        }
    }
    return targetObj;
}
// 定义 sourceObj
let sourceObj = {
    number: 1,
    string: "source1",
    boolean: true,
    null: null,
    undefined: undefined,
    arr: [{name: "arr1"}],
    func: () => "sourceFunc1",
    obj: {
        string: "obj1",
        func: () => "objFunc1"
    }
}

// 拷贝sourceObj
let copyObj = deepClone(sourceObj);

// 修改 source
copyObj.number = 2;
copyObj.string = "source2";
copyObj.boolean = false;
copyObj.arr[0].name = "arr2";
copyObj.func = () => "sourceFunc2";
copyObj.obj.string = "obj2";
copyObj.obj.func = () => "objFunc2";

// 执行
console.log(sourceObj);
/* {
    number: 1,
    string: "source1",
    boolean: true,
    null: null,
    undefined: undefined,
    arr: [{name: "arr1"}],
    func: () => "sourceFunc1",
    obj: {
        func: () => "objFunc1",
        string: "obj1"
    }
}
*/

两个方法可以合并在一起:

/* deep 为 true 表示深复制,为 false 表示浅复制
 * sourceObj 表示源对象
 * 执行完函数,返回目标对象
*/ 
function clone (deep = true, sourceObj = {}) {
    let targetObj = Array.isArray(sourceObj) ? [] : {};
    let copy;
    for (var key in sourceObj) {
        copy = sourceObj[key];
        if (deep && typeof(copy) === "object") {
            if (copy instanceof Object) {
                targetObj[key] = clone(deep, copy);
            } else {
                targetObj[key] = copy;
            } 
        } else if (deep && typeof(copy) === "function") {
            targetObj[key] = eval(copy.toString());
        } else {
            targetObj[key] = copy;
        }
    }
    return targetObj;
}
三、使用技巧 1. concat()、slice()

(1)若拷贝数组是纯数据(不含对象),可以通过concat() 和 slice() 来实现深拷贝;

let a = [1, 2];
let b = [3, 4];
let copy = a.concat(b);
a[1] = 5;
b[1] = 6;
console.log(copy);
// [1, 2, 3, 4]
let a = [1, 2];
let copy = a.slice();
copy[0] = 3;
console.log(a);
// [1, 2]

(2)若拷贝数组中有对象,可以使用 concat() 和 slice() 方法来实现数组的浅拷贝。

let a = [1, {name: "hh1"}];
let b = [2, {name: "kk1"}];
let copy = a.concat(b);
copy[1].name = "hh2";
copy[3].name = "kk2";
console.log(copy);
// [1, {name: "hh2"}, 2, {name: "kk2"}]

无论 a[1].name 或者 b[1].name 改变,copy[1].name 的值都会改变。

let a = [1, {name: "hh1"}];
let copy = a.slice();
copy[1].name = "hh2";
console.log(a);
// [1, {name: "hh2"}]

改变了 a[1].name 后,copy[1].name 的值也改变了。

2. Object.assign()、Object.create()

Object.assign()、Object.create() 都是一层(根级)深拷贝,之下的级别为浅拷贝。
(1) 若拷贝对象只有一级,可以通过 Object.assign()、Object.create() 来实现对象的深拷贝;

let sourceObj = {
    str: "hh1",
    number: 10
}
let targetObj = Object.assign({}, sourceObj)
targetObj.str = "hh2"
console.log(sourceObj);
// {str: "hh1", number: 10}
let sourceObj = {
    str: "hh1",
    number: 10
}
let targetObj = Object.create(sourceObj)
targetObj.str = "hh2"
console.log(sourceObj);
// {str: "hh1", number: 10}

(2) 若拷贝对象有多级, Object.assign()、Object.create() 实现的是对象的浅拷贝。

let sourceObj = {
    str: "hh",
    number: 10,
    obj: {
        str: "kk1"
    }
}
let targetObj = Object.assign({}, sourceObj)
targetObj.obj.str = "kk2"
console.log(sourceObj);
// {
//     str: "hh",
//     number: 10,
//     obj: {
//         str: "kk2"
//     }
// }
let sourceObj = {
    str: "hh",
    number: 10,
    obj: {
        str: "kk1"
    }
}
let targetObj = Object.create(sourceObj)
targetObj.obj.str = "kk2"
console.log(sourceObj);
// {
//     str: "hh",
//     number: 10,
//     obj: {
//         str: "kk2"
//     }
// }

修改了 targetObj.obj.str 的值之后,sourceObj.obj.str 的值也改变了。

3. 对象的解构

对象的解构同 Object.assign() 和 Object.create(),都是一层(根级)深拷贝,之下的级别为浅拷贝。

(1)若拷贝对象只有一层,可以通过对象的解构来实现深拷贝;

let sourceObj = {
    str: "hh1",
    number: 10
}
let targetObj = {...sourceObj};
targetObj.str = "hh2"
console.log(sourceObj);
// {str: "hh1", number: 10}

(2)若拷贝对象有多层,通过对象的解构实现的是对象的浅拷贝。

let sourceObj = {
    str: "hh",
    number: 10,
    obj: {
        str: "kk1"
    }
}
let targetObj = {...sourceObj};
targetObj.obj.str = "kk2"
console.log(sourceObj);
// {
//     str: "hh",
//     number: 10,
//     obj: {
//         str: "kk2"
//     }
// }
4. JSON.parse()

用 JSON.stringify() 把对象转成字符串,再用 JSON.parse() 把字符串转成新的对象,可以实现对象的深复制。

let source = ["hh", 1, [2, 3], {name: "kk1"}];
let copy = JSON.parse(JSON.stringify(source));
copy[2][1] = 4;
copy[3].name = "kk2";
console.log(source);
// ["hh", 1, [2, 3], {name: "kk1"}]

可以看出,虽然改变了 copy[2].name 的值,但是 source[2].name 的值没有改变。

JSON.parse(JSON.stringify(obj)) 不仅能复制数组还可以复制对象,但是几个弊端:
1)它会抛弃对象的 constructor,深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成 Object;
2)这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。RegExp 对象是无法通过这种方式深拷贝。
3)只有可以转成 JSON 格式的对象才可以这样用,像 function 没办法转成 JSON。

5. 可以使用的库

以下两种库都能实现深浅拷贝,有各自的使用方法。

jQuery

具体使用可以参考:官方文档

Lodash

具体使用可以参考:官方文档

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100282.html

相关文章

  • Javascript对象的深浅拷贝

    摘要:开门见山,有人叫对象的复制为深复制浅复制,也有人叫深拷贝浅拷贝。高级属性修改深拷贝满足对象的复制,浅拷贝影响原数组。关于对象的深浅拷贝,暂且探索到这里,后续有新发现再进行补充。 showImg(https://segmentfault.com/img/remote/1460000014305581); 开门见山,有人叫对象的复制为深复制浅复制,也有人叫深拷贝浅拷贝。其实都是copy。 ...

    qieangel2013 评论0 收藏0
  • JavaScript中的深浅拷贝

    摘要:深浅拷贝从上面的例子可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。此时需要深拷贝上场深拷贝深拷贝最简单的实现办法就是使用来解决。发现只拷贝了而忽略了和。 深浅拷贝 let a = { age: 1 } let b = a a.age = 2 console.log(b.age) // 2 从上面的例子可以发现,如果给一个变量...

    dantezhao 评论0 收藏0
  • 复习Javascript专题(四):js中的深浅拷贝

    摘要:基本数据类型的复制很简单,就是赋值操作,所以深浅拷贝也是针对,这类引用类型数据。它会抛弃对象的。另外,查资料过程中还看到这么一个词结构化克隆算法还有这一篇资料也有参考,也写得比较详细了的深浅拷贝 基本数据类型的复制很简单,就是赋值操作,所以深浅拷贝也是针对Object,Array这类引用类型数据。 浅拷贝对于字符串来说,是值的复制,而对于对象来说则是对对象地址的复制;而深拷贝的话,它不...

    MobService 评论0 收藏0
  • 深浅拷贝

    摘要:深复制实现代码如下第一种方法通过递归解析解决第二种方法通过解析解决作者六师兄链接原生深拷贝的实现处理未输入新对象的情况通过方法构造新的对象 深浅拷贝针对的是 对象类型,如果是字符串的数组用[...arr],还是不会影响 要区分针对数组的深浅拷贝(默认情况为里面没有对象的数组),与针对对象的深浅拷贝 JavaScript数组深拷贝和浅拷贝的两种方法 let a1 = [1, 2]; ...

    Karrdy 评论0 收藏0
  • JavaScript基础心法——深浅拷贝

    摘要:原文地址基础心法深浅拷贝欢迎。上面的代码是最简单的利用赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着和改变,和也随着发生了变化。展开运算符结论实现的是对象第一层的深拷贝。 原文地址:JavaScript基础心法——深浅拷贝 欢迎star。 如果有错误的地方欢迎指正。 浅拷贝和深拷贝都是对于JS中的引用类型而言的,浅拷贝就只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生...

    keithxiaoy 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<