JavaScript —— 浅拷贝和深拷贝

·  阅读 289

JavaScript —— 浅拷贝和深拷贝

一、浅拷贝和深拷贝的定义

浅拷贝:浅拷贝创建一个新对象。这个新对象是原始对象属性的精确拷贝。如果原始对象属性是基本类型,则拷贝的是基本类型的值;如果原始对象属性是引用类型,则拷贝的是引用类型的内存地址。因为新对象和原始对象引用了相同的内存地址,如果其中一个对该内存地址指向的对象进行改变,另一个对象也会受到影响。

深拷贝:深拷贝是将一个对象从内存中完整地拷贝一份出来,在堆内存中开辟一个新的空间存放新对象。对于引用类型的属性值,深拷贝会将该引用类型的属性完全拷贝一份,而不是复制内存地址。新对象和原始对象发生改变互不影响。

从定义可以知道,深拷贝是更深层,更完全的拷贝。新值和原始值之间的关联性更弱。

借助ConardLi大佬以下两张图片,帮我们更好的理解两者的含义:

image.png

image.png

二、浅拷贝

1. 简单for循环遍历

最简单也最容易想到的应该就是简单for循环遍历了。循环遍历原始对象的属性并赋值给新对象的属性,因为引用类型属性中存放的是引用类型的内存地址,所以简单for循环遍历实现的是浅拷贝。


function shallowCopy(obj){

    let newObj = Array.isArray(obj)?[]:{}

    Reflect.ownKeys(obj).forEach((key)=>{
        newObj[key] = obj[key]
    })

    return newObj
}

let arr = [1,2,3,{a:1},{a:[1,2]}]
let obj = {a:1,b:2,c:[1,2,3]}

let x1 = shallowCopy(arr)
x1[1] = 0
x1[3].a = 0
console.log(x1)                            //[ 1, 0, 3, { a: 0 }, { a: [ 1, 2 ] } ]
console.log(arr)                           //[ 1, 2, 3, { a: 0 }, { a: [ 1, 2 ] } ]

let x2 = shallowCopy(obj)
x2.b = 1
x2.c[1] = 1
console.log(x2)                            //{ a: 1, b: 1, c: [ 1, 1, 3 ] }
console.log(obj)                           //{ a: 1, b: 2, c: [ 1, 1, 3 ] }

复制代码

2. Object.assign()

Object.assign(target, ...sources)

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。

Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。

String 类型和 Symbol 类型的属性都会被拷贝。


const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }


复制代码

3. 扩展运算符...

扩展运算符...实现的是浅拷贝


const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

//拷贝数组
var arr0 = [1,2,3];
var arr1 = [...arr0];
console.log(arr1);//[1, 2, 3]

//拷贝对象
var obj = {
    name:"Panda",
    age:1,
    arr:{
        a1:[1,2]
    }
}
var obj2  = {...obj};
console.log(obj2);//{ name: 'Panda', age: 1, arr: { a1: [ 1, 2 ] } }

复制代码

4. Array.prototype.concat()

本质上是运用 Array API,只对数组生效(实现了concat方法)


function shallowCopy(obj){
    return obj.concat(obj)
}

let arr = [1,2,3,{a:1},{a:[1,2]}]
let obj = {a:1,b:2,c:[1,2,3]}

let x1 = shallowCopy(arr)
x1[1] = 0
x1[3].a = 0
console.log(x1)                            //[ 1, 0, 3, { a: 0 }, { a: [ 1, 2 ] } ]
console.log(arr)                           //[ 1, 2, 3, { a: 0 }, { a: [ 1, 2 ] } ]

复制代码

5. Array.prototype.slice()

本质上是运用 Array API,只对数组生效(实现了slice方法)


function shallowCopy(obj){
    return obj.slice(obj)
}

let arr = [1,2,3,{a:1},{a:[1,2]}]
let obj = {a:1,b:2,c:[1,2,3]}

let x1 = shallowCopy(arr)
x1[1] = 0
x1[3].a = 0
console.log(x1)                            //[ 1, 0, 3, { a: 0 }, { a: [ 1, 2 ] } ]
console.log(arr)                           //[ 1, 2, 3, { a: 0 }, { a: [ 1, 2 ] } ]

复制代码

三、深拷贝

1. JSON.parse(JSON.stringify())

JSON.parse(JSON.stringify()) 可以实现简单的深拷贝。但是这个方法存在以下的问题

  • 会忽略 undefinedsymbol
  • 不能序列化函数
  • 不能解决循环引用的问题

let arr = [1, [0, 1], { a: 1 }, undefined, Symbol("1"), function fn() { console.log("1") }]

let newArr = JSON.parse(JSON.stringify(arr))

console.log(newArr)          //[ 1, [ 0, 1 ], { a: 1 }, null, null, null ]

复制代码

let obj = {
    a: 1,
    b: {
        c: 2,
    },
}

obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

复制代码

2. MessageChannel

Channel Messaging APIMessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据。

  • 注意该方法是异步的
  • 可以处理 undefined 和循环引用对象
function structuralClone(obj) {
  return new Promise(resolve => {
    const { port1, port2 } = new MessageChannel()
    port2.onmessage = ev => resolve(ev.data)
    port1.postMessage(obj)
  })
}

var obj = {
  a: 1,
  b: {
    c: 2
  }
}

obj.b.d = obj.b

// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
const test = async () => {
  const clone = await structuralClone(obj)
  console.log(clone)
}
test()
复制代码

image.png

3. 递归或迭代实现

四、如何实现一个优雅的深拷贝函数

1. 深拷贝函数需要注意什么?

  1. 正确判断数据类型

    可以参考这一篇文章 JavaScript —— 判断数据类型

  2. 分别处理不同的数据类型

    可以分为两类:可继续遍历的类型不可继续遍历的类型


// 可继续遍历的类型
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

// 不可继续遍历的类型
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

复制代码
  1. 解决循环引用

可以引入 Map 记录已拷贝属性。引入 WeakMap 可以进一步提高性能。

在拷贝某个属性时,先在 Map 中进行查找
查找失败,可知该属性是第一次拷贝,将拷贝的新值加入 Map 中,正常进行拷贝
查找成功,可知该属性已经拷贝过,可以直接将 Map 中的记录返回

2. 深拷贝函数——萌新的简单实现


const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

function deepClone(obj, hash = new WeakMap()) {
    if (!isObject(obj)) {
        return obj
    }

    const objType = getType(obj)

    // 调用构造函数constructor可以同步类型,不用另外作判断(Object,Array,Map,Set)
    let copy = new obj.constructor()

    //借助另外的数据结构存储已经拷贝的对象,避免循环应用的问题
    if (hash.get(obj)) {
        return hash.get(obj)
    } else {
        hash.set(obj, copy)
    }

    if (objType === '[object Set]') {
        obj.forEach(val => {
            copy.add(deepClone(val))
        });
        return copy
    }

    if (objType === '[object Map]') {
        obj.forEach((val, key) => {
            copy.set(deepClone(key), deepClone(val))
        });
        return copy
    }

    if (objType === '[object Function]') {
        return obj
    }

    Reflect.ownKeys(obj).forEach(x => {
        copy[x] = deepClone(obj[x])
    })

    return copy

}

function isObject(obj) {
    return (typeof obj === "object" || typeof obj === "function") && obj !== null
    // return typeof obj === "object"&& obj !== null
}

function getType(obj) {
    let objType = Object.prototype.toString.call(obj)
    return objType
}

let x = deepClone({
    "name": "Alice", "a": null, "a1": undefined, "a2": Symbol("hx"), "b": { "b1": 1, "b2": 2 }, "c": [1, [2], [[3]], null, { "A": 1 }], func() {
        console.log("hello")
    }
})

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    empty: null,
    map: new Map(),
    set: new Set(),
    bool: new Boolean(true),
    num: new Number(2),
    str: new String(2),
    symbol: Symbol(1),
    date: new Date(),
    reg: /\d+/,
    error: new Error(),
    func1: () => {
        console.log('code秘密花园');
    },
    func2: function (a, b) {
        return a + b;
    }
};


let y = deepClone(target)
console.log(y)
y.func1()
console.log(y.func2(1, 1))


console.log(x)
x.func()

复制代码

3. 深拷贝函数——大佬的惊艳实现

图片来源于 ConardLi大佬 的文章 如何写出一个惊艳面试官的深拷贝?

image.png

参考文章

如何写出一个惊艳面试官的深拷贝?
浅拷贝与深拷贝
JavaScript 深拷贝

分类:
前端
标签: