一文弄懂Javascript中的深拷贝和浅拷贝

一文弄懂Javascript 深拷贝与浅拷贝

1 Javascript数据存储规则

Javascript 中存在两大数据类型:

  • 基本类型

  • 引用类型

基本类型主要为以下6种,数据保存在在栈内存中:

  • Number
  • String
  • Boolean
  • Undefined
  • null
  • symbol

栈内存图解:

let a = 10;
let b = a;

b = 20;

// 给b赋值不影响a的值
console.log(a);  // 10;

栈内存

引用类型主要为以下三种,数据保存在堆内存中:

  • Object
  • Array
  • Function

引用数据类型的变量是一个指向堆内存中实际对象的引用,变量以及指向堆内存的指针存在在栈内存中;数据存在堆内存中。

堆内存图解:

var obj1 = {
    name: 'jone'
  };
// 将obj1的内存地址赋给obj2
var obj2 = obj1;

// 修改堆内存对象属性值
obj2.name = "sam";

console.log(obj1.name); // sam;对obj2的属性修改影响了obj1的属性;

堆内存

2 浅拷贝

浅拷贝,指的是创建新的栈内存数据,这个数据有着原始数据属性值的一份精确拷贝;

如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址;

即浅拷贝是拷贝一层,对于基本类型来讲,第一层栈内存即可;

在Javascript 中,绝对浅拷贝的有:

  • 赋值运算符"="

上面的案例即为浅拷贝案例,只拷贝了第一层对象的栈内存村粗的变量以及引用地址;

var obj1 = {
    name: 'jone'
  };
// 将obj1的内存地址赋给obj2
var obj2 = obj1;

// 修改堆内存对象属性值
obj2.name = "sam";

console.log(obj1.name); // sam;对obj2的属性修改影响了obj1的属性;

3 部分深拷贝

部分深拷贝的意思就是不止拷贝一层,对数据拷贝两层及以上;但没有覆盖所有存储层级,称之为部分深拷贝;

以下运算可以实现一级引用类型的深拷贝,当有多级引用类型时,二级属性之后的就是浅拷贝,

  • Object.assign
  • Array.prototype.slice()
  • Array.prototype.concat()
  • 拓展运算...符实现的复制
3.1 Object.assign

Object.assign(target,source)方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target);

var obj = {
    age: 18,
    names: {
        name1: 'jone',
        name2: 'sam'
    }
}
var newObj = Object.assign({}, Obj);

// 修改对象的第一层属性
newObj.age =20;
// 修改对象的第二层属性
newObj.names.name1="change"

// 最后的结果是
console.log(obj);
/*
修改后的obj
{
    "age": 18,
    "names": {
        "name1": "change",
        "name2": "sam"
    }
*/

我们可以看到,修改新对象的第一层属性和第二层属性,源对象的第一层属性没有发生变化,第二层属性却发生了变化,原因是Object.assign只对原始栈内存和第一层属性重新赋予了存储空间,也就是说只拷贝了两层;所以才会出现如上效果,图解如下:

修改前:
在这里插入图片描述
修改后:
在这里插入图片描述
以下三个运算符的部分深拷贝原理与此相同;

3.2 slice()

slice()方法用于提取目标数组的一部分,返回一个新数组,原数组不变。它的第一个参数为起始位置(从0开始,会包括在返回的新数组之中),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员。

const Arr = ["One", "Two", "Three"]
const newArr = Arr.slice(0)
newArr[1] = "love";

console.log(Arr) // ["One", "Two", "Three"]
console.log(newArr) // ["One", "love", "Three"]
3.3 concat()

concat方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变。

const Arr = ["One", "Two", "Three"]
const newArr = Arr.concat()
newArr[1] = "love";

console.log(Arr) // ["One", "Two", "Three"]
console.log(newArr) // ["One", "love", "Three"]
3.4 拓展运算符
const Arr = ["One", "Two", "Three"]
const newArr = [...Arr]
newArr[1] = "love";

console.log(Arr) // ["One", "Two", "Three"]
console.log(newArr) // ["One", "love", "Three"]

4 完全深拷贝

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式有:

  • _.cloneDeep()
  • 结构化拷贝
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归

深拷贝和浅拷贝区别如图所示:

区别

下面我们看一下这五种实现深拷贝的代码;

4.1_.cloneDeep()

借助lodash包的_.cloneDeep()方法实现深拷贝

const _ = require('lodash');
const obj1 = {
    a: 1,
    b: { 
        f: { 
            g: 1 
        } 
    },
    c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);

console.log(obj1.b.f === obj2.b.f);// false,两者不相等说明指向的地址不同
4.2 结构化拷贝

结构化克隆算法是由HTML5规范定义的用于复制复杂|avaScript对象的算法。通过来自Workers的postMessage()或使用 IndexedDB 存储对象时在内部使用。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环。

function structuralClone(obj){
    return new Promise(resolve =>{
        const {port1,port2}= new MessageChannel();
        port2.onmessage=ev =>resolve(ev.data);
        port1.postMessage(obj);
    });
}

const obj1 = {
    a: 1,
    b: { 
        f: { 
            g: 1 
        } 
    },
    c: [1, 2, 3]
};
const obj2 = await structuralClone(obj1);

console.log(obj1.b.f === obj2.b.f);// false,两者不相等说明指向的地址不同

此方法一般情况下能够解决大部分问题,且性能较好。但不支持Function数据类型。

4.3 json.stringify()
const obj = {
    name: 'A',
    name1: undefined,
    name3: function() {},
    name4:  Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}

这种方式存在弊端,会忽略undefined、symbol 和函数

4.4 循环递归

通过遍历对象属性进行循环浅拷贝。只是在遇到一个 object 属性时,需要再次调用拷贝函数。

function deepClone(obj){
  if(typeof obj !== "object") return;    
  let newObj = obj instanceof Array ? [] : {};
  for(let key in obj){
     if(obj.hasOwnProperty(key)){
        newObj[key] = typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key];
    }      
  }  
  return newObj;  
}
let obj = {a: 11, b: function(){}, c: {d: 22}};
deepClone(obj);  // {a: 11, b: f(), c: {d: 22}};
4.5 jQuery.extend()

与lodash库相同,jQuery也提供了深拷贝方法,使用方法如下:

const $ = require('jquery');
const obj1 = {
    a: 1,
    b: { 
        f: { 
            g: 1 
        } 
    },
    c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false,两者不相等说明指向的地址不同

5 总结

深拷贝和浅拷贝的区别就在于拷贝的层级,在日常使用中,我们可以按需去是使用拷贝方法:

  1. 基本类型使用普通赋值运算符;
  2. 引用类型:
  • 一维数据结构的深拷贝方法建议使用:Object.assign()

  • 二维数据结构及以上的深拷贝方法建议使用:JSON.parse(JSON.stringify())

  • 特别复杂的数据结构的深拷贝方法建议使用:三方API;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值