JS的浅拷贝和深拷贝,看这篇就就够了!

136 阅读8分钟

真正的大师永远都怀着一颗学徒的心。 --无极剑圣

图片

浅拷贝和深拷贝作为前端开发中一个重要的知识点,不管在面试中还是在日常开发工作中都会经常遇到。 因此,弄清楚什么是浅拷贝什么是深拷贝,浅拷贝和深拷贝的区别以及它们的实现方式还是很有必要的! ! ! 下面就开始吧。

一、数据类型

弄懂深拷贝和浅拷贝之前,我们需要对JS的数据类型有一个基本的了解。

1、数据类型

基本数据类型:string、number、boolean、null、undefined、symbol、bigInt。

引用(复杂)数据类型:object(Array、Date、Function、RegExp等)。

2、基本数据和引用数据类型的区别

基本数据类型:存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接访问。

引用数据类型:引用类型(object)是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况来进行特定的分配。引用数据类型保存在堆内存中,然后在栈内存中保存了一个对堆内存中实际对象的引用,即数据在堆内存中的地址,JS对引用数据类型的操作都是操作对象的引用而不是实际的对象,如果obj1拷贝了obj2,那么这两个引用数据类型就指向了同一个堆内存对象,具体操作是obj1将栈内存的引用地址复制了一份给obj2,因而它们共同指向了一个堆内存对象。

例如:

var a = 1;
// 栈内存会开辟一个新的内存空间,此时b和a都是相互独立的
b = a
b = 2;
console.log(a); // 1 

例如:

var arr = [1234];
//arr将栈内存的引用地址复制了一份给arr1,因而它们共同指向了一个堆内存对象;
newArr = arr
newArr[2] = 9;
console.log(arr); //[1,2,9,4]

基本数据类型值不可变,JS中的原始值(undefined、null、boolean、number和string)与对象(包括数组和函数)有着根本区别。 

原始值是不可更改的,任何方法都无法更改一个原始值。对数字和布尔值来说显然如此,改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来假改字符串中的字符。实际上,JS是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。

基本数据类型的值是不可变的,动态修改了基本数据类型的值,它的原始值也是不会改变的。有很多操作字符串的方法,但是这些方法都是返回一个新的字符串,并没有改变其原有的数据。

例如:

var str = "abc";
str[1] = "d";
console.log(str); //abc

引用类型的值是可变的。

例如:

var a = [1, 2, 'a', 'b'];
a[1] = 3;
console.log(a[1]); // 3

二、浅拷贝和深拷贝

         通过上面第一部分我们对数据类型有了一个基本了解后,在学习浅拷贝和深拷贝的区别就容易多了。

假如有一个管理后台的列表页,现在需要去编辑修改某个字段A,在对话框中修改字段A的值,会发现列表中对应当前行的字段A的值,也会随着变化。理论上应该是我们修改提交保存后,刷新列表字段A的值才会更新。这就看起来很奇怪,这不是我们想要的效果。

我们往往需要拷贝的是一个新的对象,改变新对象值的同时不改变原对象的值,这时就需要用到深拷贝和浅拷贝了。

区别

浅拷贝和深拷贝都只针对于引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟旧对象不共享内存,修改新对象不会改到原对象;浅拷贝只复制对象的第一层属性,深拷贝可以对对象的属性进行递归复制。

image.png

三、浅拷贝的实现方式

                    

1、for...in只循环第一层

function shallowCopy(obj) {
    let newObj = Array.isArray(obj) ? [] : {};
    for (let i in obj) {
        newObj[i] = obj[i];
    }
    return newObj;
}
let obj = {
    name: '张三',
  age: 18,
  score: {
    chinese: 96,
    english: 72
  },
  likes: ['唱', '跳', 'rap']
}
const obj1 = shallowCopy(obj);
obj1.name = '坤坤';
obj1.age = 20;
obj1.score.english = 90;
obj1.likes[3] = '打篮球';

console.log(obj.name); // 张三
console.log(obj1.name); // 坤坤

console.log(obj.age); // 18
console.log(obj1.age); // 20

console.log(obj.score.english); // 90
console.log(obj1.score.english); // 90

console.log(obj.likes); // ['唱', '跳', 'rap', '打篮球']
console.log(obj1.likes); // ['唱', '跳', 'rap', '打篮球']

如上代码,修改新对象obj1中name和age的属性值,原对象obj中name和age的值没有改变,但修改新对象obj1.score.english和obj1.likes的属性值,原对象obj1.score.english和obj1.likes的值发生了改变。这是为什么呢?这不难理解,obj中的name和age属于基本数据类型,而obj中的score和likes属于引用数据类型,拷贝的是score和likes的内存地址,修改obj1.score.english和obj1.likes的值,原对象的值也对随之变化。

2、Object.assign()

let obj = {
  name: '张三',
  age: 18,
  score: {
    chinese: 96,
    english: 72
  },
  likes: ['唱''跳''rap']
}
const obj1 = Object.assign({}, obj);

obj1.name = '坤坤';
obj1.likes[3] = '打篮球';

console.log(obj.name); // 张三
console.log(obj1.name); // 坤坤

console.log(obj.likes); // ['唱''跳''rap''打篮球']
console.log(obj1.likes); // ['唱''跳''rap''打篮球']

如上代码,obj中的name和age属于基本数据类型,而obj中的score和likes属于引用数据类型,拷贝的是score和likes的内存地址,修改obj1.likes的值,原对象的值也对随之变化。

3、Object.create()

let obj = {
  name: '张三',
  age: 18,
  score: {
    chinese: 96,
    english: 72
  },
  likes: ['唱''跳''rap']
}
const obj1 = Object.create(obj);

obj1.name = '坤坤';
obj1.likes[3] = '打篮球';

console.log(obj.name); // 张三
console.log(obj1.name); // 坤坤

console.log(obj.likes); // ['唱''跳''rap''打篮球']
console.log(obj1.likes); // ['唱''跳''rap''打篮球']

四、深拷贝的实现方式

                    

1、JSON.parse(JSON.stringify(obj))

JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象。

let obj = {
  name'张三',
  age18,
  score: {
    chinese96,
    english72
  },
  likes: ['唱''跳''rap'],
  sing:function() {
    console.log('坤坤爱唱歌')
  },
  skipnew RegExp("e"),
}

const obj1 = JSON.parse(JSON.stringify(obj))

obj1.name = '坤坤';
obj1.likes[3] = '打篮球';

console.log(obj.name// 张三
console.log(obj1.name// 坤坤

console.log(obj.likes// ['唱', '跳', 'rap']
console.log(obj1.likes// ['唱', '跳', 'rap', '打篮球']

新对象打印结果如下:

图片

这种方法使用很简单,可以满足基本的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是它却有如下缺点。

 

缺点

(1)如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是时间对象;

(2)如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象(打印结果可以看出,skip为{});

(3)如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失; 

(4)如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null; 

(5)JSON.stringify()只能序列化对象的可枚举的自有属性,如果obj中的对象是有构造函数生成的,深拷贝后,会丢弃对象的constructor; 

(6)如果对象中存在循环引用的情况也无法正确实现深拷贝。

 

2、递归实现

let data = {
  name'张三',
  age18,
  score: {
    chinese96,
    english72
  },
  likes: ['唱''跳''rap'],
  sing:function() {
    console.log('坤坤爱唱歌')
  },
  skipnew RegExp("e"),
}

function judgeType(obj) {
    // tostring会返回对应不同的标签的构造函数
    const toString = Object.prototype.toString;
    const map = {
        '[object Boolean]''boolean',
        '[object Number]''number',
        '[object String]''string',
        '[object Function]''function',
        '[object Array]''array',
        '[object Date]''date',
        '[object RegExp]''regExp',
        '[object Undefined]''undefined',
        '[object Null]''null',
        '[object Object]''object',
    };
    if (obj instanceof Element) {
        return 'element';
    }
    return map[toString.call(obj)];
}

function deepCopy(obj) {
  const type = judgeType(obj)
  let result;
  if (type === "object") {
    result = {};
  } else if (type === "array") {
    result = [];
  } else {
    return obj;
  }
  for (const k in obj) {
    const e = obj[k];
    if (type === "object" || type === "array") {
      result[k] = deepCopy(e);
    } else {
      result[k] = e;
    }
  }
  return result;
}


const obj1 = deepCopy((data))
console.log(obj1)

obj1.name = '坤坤';
obj1.likes[3] = '打篮球';

console.log(data.name// 张三
console.log(obj1.name// 坤坤

console.log(data.likes// ['唱', '跳', 'rap']
console.log(obj1.likes// ['唱', '跳', 'rap', '打篮球']

图片

3、递归的另一种方法

function deepCopy(obj) {
    //判断 传入对象 为 数组 或者 对象
    var result = Array.isArray(obj) ? [] : {};
    // for in 遍历
    for (var key in obj) {
        // 判断是否为自身的属性值(排除原型链干扰)
        if (obj.hasOwnProperty(key)) {
            // 判断 对象的属性值 中 存储的 数据类型 是否为对象
            if (obj[key] && typeof obj[key] === 'object') {
                // 递归调用
                result[key] = deepCopy(obj[key]);   //递归复制
            } 
            // 不是的话 直接 赋值 copy
            else {
                result[key] = obj[key];
            }
        }
    }
    // 返回 新的对象
    return result;
}

4、函数库lodash

此外,我们可以借助第三方工具库来实现,比如借助函数库lodash提供的_.cloneDeep来实现深拷贝。

5、jquery的$.extend

jquery实现深拷贝,jquery 提供一个$.extend可以用来做深拷贝。

let obj = {
  name'张三',
  age18,
  score: {
    chinese96,
    english72
  },
  likes: ['唱''跳''rap'],
  sing:function() {
    console.log('坤坤爱唱歌')
  },
  skipnew RegExp("e"),
}

const newObj = $.extend(true, {}, obj);

五、总结

                    

感谢您的阅读!通过本篇文章的阅读可以对深浅拷贝有一个基本的了解和使用,了解的深拷贝和浅拷贝的区别,可以帮助我们更有效的搬砖。