【Js基础】深拷贝和浅拷贝

220 阅读10分钟

引言:从内心来讲,我对他们的区别一点都不感冒,奈何面试频频被问到,自己问自己都模棱两可,面试官问深了,甚至有点懵!此前研究过,但没在心里留下痕迹,全忘了!所以想种个草,方便日后查阅!

先参考如下网上的ES发展史图:

image.png

Tips:你需要大致了解Es规范出现的时间,其次从Es5开始,知道Es6、Es7、Es8、Es9、Es10等新语法,面试易问,工作也易用,入口:ES6、ES7、ES8、ES9、ES10、ES11、ES12、ES13新特性大全

数组的常见方法,MDN中数组的详细解释

可变方法(可变函数)不可变方法(不可变函数)
push、 unshift、 pop、 shift、 reverse、 splice、 sortcopyWithinfillArray.prototype.push.apply(arr1,arr2)、 .length修改长度(或者对应下标改变值)concat、 join、 slice、 findIndex、 forEach、map、 reduce等等
    //  Array.prototype.push.apply(arr1, arr2)
    const a = [1, 2, 3, 4, 5, 6]
    const b = ['a']
    Array.prototype.push.apply(a,b)
    console.log(a); // [1, 2, 3, 4, 5, 6, 'a'] 
    console.log(b); // ['a']

Tips:上面的数组方法,新版本发布可能就会有新方法的增加,并不是一成不变的。

区分小写string和大写String:

typeof运算符(返回的是primitive type:即原始值)内置构造函数类型Object.prototype.toString.call(xxx)
返回类型为字符串: 'undefined'、 'boolean'、 'string' 'number'、 'object' (对象类型的变量或值,或者null(这个是js历史遗留问题,将null作为object类型处理)、 'function、 'symbol'(Symbol (ECMAScript 6 新增))九种:Number、String、Boolean、Object、Array、Function、Date、RegExp、Error返回类型为字符串: 原始值: '[object Boolean]'、 '[object Number]'、 '[object String]'、 '[object Null]'、 '[object Undefined]'、 '[object Symbol]'、 '[object Bigint]'; 引用类型: '[object Object]'、 '[object Function]'、 '[object Array]'; 其他情况: '[object Error]'、 '[object RegExp]'、 '[object Date]'、 '[object Arguments]'、 '[object Math]'、 '[object JSON]'、 '[object Document]'、 '[object HTMLDivElement]'、 '[object Window]'

Tipsxxx.prototype.hasOwnProperty('toString'),用于判断xxx这个构造函数是否有这个方法; String() 属于强制转换,可以转换任何类型,而toString()时继承自构造函数,所以对null和undefined报错

判断数组还是对象的方案

  1. instanceof:A instanceof B ,用于判断实例A的原型链上是否有B的构造方法(判断一个引用类型是否属于某构造函数,还可以在继承关系中判断一个实例是否属于它的父类型)。A instanceof Array ,true对应是数组,false是除数组以外的类型

  2. Array.isArray():true对应是数组,false是除数组以外的类型

  3. constructor:A.constructor == Array,ture对应数组,false对应其他; B.constructor == Object,ture对应对象,false对应其他(包括数组)

  4. Object.prototype.toString.call()

  • 有没有思考过为什么不是Array.prototype.toString.call() ?因为Array.prototype.toString.call()重写了Object.prototype.toString.call()方法,导致不准确
  • *有没有思考过可不可以用apply和bind代替这个call的写法?完全可以,但没必要。Object.prototype.toString.call(xxx) 等价于Object.prototype.toString.apply(xxx)和Object.prototype.toString.bind(xxx)()
  • *有没有思考过拆分?拆分成Object.prototype.toString()xxx.call()
    // toString()
    ...
    2.toString() // 会报错,是因为这个小数点被器判断为一个小数点而不是方法调用, 换成 (2).toString()即可
    {}.toString() // 会报错,是因为这个花括号{}被解释器判断为定义代码块,换成({}).toString()即可
    null.toString() // 会报错,是因为null没有对应的构造函数 
    // String() 属于强制转换,可以转换任何类型,而toString()时继承自构造函数,所以对null和undefined报错
    ... 

Object.prototype.toString.call(obj) === '[object Array]',ture对应数组,false对应其他;Object.prototype.toString.call(obj) === '[object Object]',ture对应对象,false对应其他(包括数组)

回过头来,我们来聊聊为什么出现深拷贝和浅拷贝的概念?

强调下前提:复杂数据类型才会有深拷贝和浅拷贝的概念

浅拷贝深拷贝
创建一个新对象,这个对象有着栈内存原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址 。所以如果其中一个对象改变了这个 地址 ,就会影响到另一个对象如果基本类型改变了 值 不会影响原对象将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

常见的基础代码:

    let num = 1
    let num2 = num
    num = 2
    console.log(num2); // 1

    let obj = {name:'liuxiao'}
    let obj2 = obj
    obj.age = 2
    console.log(obj2); // {name: 'liuxiao', age: 2}

Tips原始值是按照值去访问的,而引用值是按照引用去访问的,所以复杂数据类型(也就是引用值)才有'拷贝'的概念,直白一点说大前提是:复杂数据类型才会有深拷贝和浅拷贝的概念!!!

实现深拷贝的几种方式

1、JSON.parse(JSON.stringify()):深拷贝

先聊聊JSON.stringify()和JSON.parse():

    var obj = {
      a: 1,
      b: 'abc',
      c: true,
      d: null,
      e: [1, 2, 3],
      f: {
        a: 1
      },
      g: new Date(),
      h: /abc/,
      i: undefined,
      j: function () {}
    }

    console.log(JSON.stringify(obj)); // {"a":1,"b":"abc","c":true,"d":null,"e":[1,2,3],"f":{"a":1},"g":"2022-06-22T07:42:47.112Z","h":{}}
    // JSON.stringify(),Date类型被转成了字符串输出,正则表达式转成了空对象, undefined和Function都被直接忽略了
    console.log(JSON.parse(JSON.stringify(obj))); //{a: 1,b: "abc",c: true,d: null,e: [1, 2, 3],f: {a: 1},g: "2022-06-22T07:44:28.049Z",h: {}}
    // JSON.parse(JSON.stringify(obj)),Date类型被转成了字符串输出,正则表达式转成了空对象, undefined和Function都被直接忽略了

Tips:JSON.stringify()和JSON.parse(JSON.stringify(obj)),Date类型被转成了字符串输出正则表达式转成了空对象undefined和Function都被直接忽略了。参考MDN中JSON.stringify()JSON.parse()的用法,参数并非一个会有你想不到的用法!

    // 数组 的深拷贝
    let arr = [1, 3, {username: '拜登'}, function () {}];
    let newArr = JSON.parse(JSON.stringify(arr));
    newArr[2].username = '特朗普';// 引用类型数据,改了
    newArr[0] = 'a'; // 非引用类型数据,改了
    console.log(arr) // [1, 3, {username: '拜登'}, function () {}]
    console.log(newArr) // ['a',1, 3, {username: '特朗普'}, null]

    // 对象 的深拷贝
    let obj = {msg:{ name:'拜登'},num:10}
    let newObj = JSON.parse(JSON.stringify(obj));
    newObj.msg.name = '特朗普'; // 引用类型数据,改了
    newObj.num = 90; // 非引用类型数据,改了
    console.log(obj); // {msg:{ name:'拜登'},num:10}
    console.log(newObj); // {msg:{ name:'特朗普'},num:90}

Tips: JSON.parse(JSON.stringify()),发生的是深拷贝,且不会根据数组或者对象的元素是否是引用类型的数据而做不同的判断的,切记别搞混了!!!

缺点:只能复制具有 JSON 支持的键和值的属性,一些不受支持的键和值将被忽略,如:obj里有RegExp、Error对象,则序列化的结果会变成空对象{}

2、递归函数实现:深拷贝

  • 对象,深拷贝第一层:
    function deepClone(obj){
      let newObj = {}
      for(let key in obj){
        newObj[key] = obj[key]
      }
      return newObj
    }
    let newObj = deepClone(obj)
    obj.a = 10 // for...in遍历了第一层,里面数据单个值的赋值,这个是深拷贝,
    obj.b.c = 10 // 因为没有遍历第二层所以,第二层的数据还是浅拷贝
    console.log(newObj);
  • 对象,深拷贝前三层:
    function deepClone(obj) {
      let newObj = {}
      for (let key in obj) {
        if (typeof obj[key] == 'object' && !Array.isArray(obj[key]) && obj[key] != null) {
          newObj[key] = {}
          for (let i in obj[key]) {
            newObj[key][i] = obj[key][i]
            if (typeof obj[key][i] == 'object' && !Array.isArray(obj[key][i]) && obj[key][i] != null) {
              obj[key][i] = {}
              for(let j in obj[key][i]){
                newObj[key][i][j] = obj[key][i][j]
              }
            }else{
              newObj[key][i] = obj[key][i]
            }
          }
        } else {
          newObj[key] = obj[key]
        }
      }
      return newObj
    }
  • 对象,深拷贝n层,可用递归代替:递归即最终写法
    function deepClone(obj, newObj) {
      var newObj = newObj || {} // 这里用let会报重复声明的错,因为 **形参是let声明的**
      for (let key in obj) {
        if (typeof obj[key] == 'object' && !Array.isArray(obj[key]) && obj[key] != null) {
          newObj[key] = {}
          deepClone(obj[key], newObj[key])
        } else {
          newObj[key] = obj[key]
        }
      }
      return newObj
    }

Tips: 上述代码也是面试官想知道的内容!!!

3、jquery的extend方法:深拷贝

对于jq用法不做记录,请自行百度!

4、lodash库:深拷贝

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

实现通用深度复制


function deepCopy(original) {
  if (Array.isArray(original)) {
    const copy = [];
    for (const [index, value] of original.entries()) {
      copy[index] = deepCopy(value);
    }
    return copy;
  } else if (typeof original === 'object' && original !== null) {
    const copy = {};
    for (const [key, value] of Object.entries(original)) {
      copy[key] = deepCopy(value);
    }
    return copy;
  } else {
    // Primitive value: atomic, no need to copy
    return original;
  }
}

// 简洁写法:
function deepCopy(original) {
  if (Array.isArray(original)) {
    return original.map(elem => deepCopy(elem));
  } else if (typeof original === 'object' && original !== null) {
    return Object.fromEntries(
      Object.entries(original)
        .map(([k, v]) => [k, deepCopy(v)]));
  } else {
    // Primitive value: atomic, no need to copy
    return original;
  }
}

实现浅拷贝的几种方式

拓展运算符和Object.assign()的区别Object.assign()复制属性时,它会调用 setter;而解构赋值不会。

1、concat()、slice()、Array.from()、拓展运算符:浅拷贝

    let crr = ['台湾','地震'] // 原数组的元素是简单数据类型:字符串
    
    let drr = crr.concat()  // 同:[].concat(crr)和[].concat(...crr)
    // let drr = crr.slice()
    // let drr = Array.from(crr)
    // let drr = [...crr]
    drr.push('浙江感受到了!')
    console.log(crr); // ['台湾', '地震']
    console.log(drr); // ['台湾', '地震', '浙江感受到了!']
  • 结论:原数组元素类型简单数据类型时,值的改变不会互相影响,即按值访问,但这并不是深拷贝的范畴
    let arr = [{province:'台湾省'}] // 原数组的元素引用类型数据:对象
    let newArr = [].concat(arr)
    // let newArr = arr.slice()
    // let newArr = Array.from(arr)
    // let newArr = [...arr]
    newArr[0].province = '中华台北'
    console.log(arr); // [{province:'中华台北'}]
    console.log(newArr); // [{province:'中华台北'}]
    let arr = [[1,2,[5,6]],[3,4],2] // 原数组的元素引用类型数据:数组
    let brr = [].concat(arr)
    brr[0][2][0] = 'aa' // 引用类型数据,发生了浅拷贝
    brr[2] = 'bb' // 非引用类型数据,发生了深拷贝
    console.log(arr); // [[1,2,['aa',6]],[3,4],2]
  • 结论:原数组元素类型引用数据类型时,值的改变会互相影响,即按引用访问,但这并不是浅拷贝的范畴
    let arr = [1,2,3] // 元素类型是非引用类型
    let newArr = [...arr]
    newArr[0] = 90
    console.log(arr); // [1, 2, 3]
    console.log(newArr); // [90, 2, 3]

    let arr = [{msg:{ name:'拜登'},num:10}] // 元素类型是引用类型
    let newArr = [...arr]
    newArr[0].msg.name = '特朗普'
    newArr[0].num = 90
    console.log(arr); // {msg:{ name:'特朗普'},num:90}
    console.log(newArr); // {msg:{ name:'特朗普'},num:90}
    // 上述代码换成Array.from(),结论是一样的,不做赘述
  • 结论:数组的拓展运算符元素类型简单数据类型时,值的改变不会互相影响,即按值访问,但这并不是深拷贝的范畴;是引用数据类型时,值的改变会互相影响,即按引用访问,但这并不是浅拷贝的范畴。

Tips: 在使用concat()、slice()、Array.from()、拓展运算符的前提下,需要遵循这一规则:原数组的元素是引用类型数据,会拷贝到这个"引用"到新数组中,即共用同一个引用,值的改变互相会影响**;原数组的元素是简单类型数据,会拷贝到这个"值"到新数组中,即值的改变不互相会影响。但复杂数据类型才存在“深浅拷贝”的概念,所以通常说concat()、slice()、Array.from()、拓展运算符是浅拷贝

2、Object.assign(): 浅拷贝

    let obj = {
      a: {
        b: "拜登",
        c: 39
      },
      d:'美国'
    };
    let newObj = Object.assign({}, obj);
    newObj.d = '中国' // 第一层
    newObj.a.d = "特朗普"; // 多层
    console.log(obj); // {a: {b: "拜登",c: 39,d: "特朗普"},d:"美国"}
    console.log(newObj); // {a: {b: "拜登",c: 39,d: "特朗普"},d:"中国"}
    
    let arr = [1, 3, {username: '拜登'}, function () {}];
    let newArr = Object.assign({}, arr);
    newArr[2].username = '特朗普'
    newArr[0] = 'one'
    console.log(arr); // [1, 3, {username: '特朗普'}, function () {}]
    console.log(newArr); // {0: 'one', 1: 3, 2: {username: '特朗普'}, 3: function () {}}
  • **结论:参考MDN中Object.assign()的用法,浅拷贝顾名思义:只拷贝了最外层,这层的数据已经彻底没关系了,其他层的数据有关系,这就是浅拷贝。

3、对象中,浅拷贝的一般使用

es3中的,实现对象的浅拷贝,浅拷贝有这个概念,基本类型改变了 值 ,不会影响原对象,如下:

    let obj = {
      name:'liuxiao',
      age:'18'
    }
    function simpleClone1(){
      let newObj = {}
      for(let key in obj){
        newObj[key] = obj[key]
      }
      return newObj
    }
    let newObj = simpleClone1()
    newObj.name = 'lwx'
    console.log(obj); // {name: 'liuxiao', age: '18'}
    console.log(newObj); // {name: 'lwx', age: '18'}

es6中的,实现对象的浅拷贝,浅拷贝有这个概念,基本类型改变了 值 ,不会影响原对象,如下:

    function simpleClone2() {
      let newObj = {}
      for (let [key, value] of Object.entries(obj)) {
        newObj[key] = value
      }
      // for(let key of Object.keys(obj)){
      //   newObj[key] = obj[key]
      // }
      return newObj
    }
    let newObj = simpleClone2()
    newObj.name = 'lwx'
    console.log(obj); // {name: 'liuxiao', age: '18'}
    console.log(newObj); // {name: 'lwx', age: '18'}

es5中的,实现对象的浅拷贝,这个比较麻烦

浅拷贝有这个概念,基本类型改变了 值 ,不会影响原对象,如下:

    function simpleClone3() {
      let newObj = {}
      let ownPropertyNames = Object.getOwnPropertyNames(obj) // ['name', 'age']
      Object.getOwnPropertyNames(obj).forEach(function (key) {
        let ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, key) //如: {configurable: true , enumerable: true , value: "lwx" , writable: true}
        Object.defineProperty(newObj, key, ownPropertyDescriptor)
        newObj[key] = obj[key]
      })
      return newObj
    }
    let newObj = simpleClone3()
    newObj.name = 'lwx'
    console.log(obj); // {name: 'liuxiao', age: '18'}
    console.log(newObj); // {name: 'lwx', age: '18'}

赶紧收藏种草吧!!!

// 浅拷贝如下5种
// `arr` is an array 
const clone = (arr) => arr.slice(0);

// Or
const clone = (arr) => [...arr];

// Or
const clone = (arr) => Array.from(arr);

// Or
const clone = (arr) => arr.map((x) => x);

// Or
const clone = (arr) => arr.concat([]);

// 深拷贝如下2种
// `arr` is an array
const clone = (arr) => JSON.parse(JSON.stringify(arr));

// Or
const clone = (arr) => structuredClone(arr);