前端面试|详细整理 赋值、浅拷贝、深拷贝的区别

655 阅读5分钟

赋值(copy)

赋值就是将某一数值或对象赋给某个变量的过程,分为下面2部分

  • 基本数据类型:赋值,赋值之后两个变量互不影响
  • 引用数据类型:赋址,两个变量具有相同的引用,指向同一个对象,互相影响
let a = { age: 1 }
let b = a
a.age = 2
console.log(b.age) // 2

从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

拷贝就是拷贝指向对象的指针,意思就是说:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。

浅拷贝(Shallow Copy)

浅拷贝是按位拷贝对象,它会创建一个对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

简单来说可以理解为浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址

那么上面的例子出现的问题怎么解决?

  • 首先可以通过 Object.assign() 来解决这个问题。

Object.assign()方法用于将所有可枚举属性的一个或多个源对象复制到目标对象。并返回目标对象。

let a = { 
    age: 1,
    book:{
        title: "宋00的笔记",
        price: "66"
    }
}
let b = Object.assign({}, a)
a.age = 2
a.book.price = "100"
console.log(b.age) // 1
console.log(b) 
// { 
//    age: 1,
//    book:{
//        title: "宋00的笔记",
//        price: "100"
//    }
}

上面代码改变对象a之后,对象b的基本属性保持不变。但是当改变对象a中的对象book时,对象b相应的位置也发生了变化。

  • 当然我们也可以通过展开运算符Spread(...)来解决
let a = { 
    age: 1,
    book:{
        title: "宋00的笔记",
        price: "66"
    }
}
let b = {...a}
a.age = 2
console.log(b.age) // 1
  • Array.prototype.slice()

slice()方法返回一个新的数组对象,这一对象是一个有beginend(不包括end)决定的原数组的浅拷贝。原始数组不会改变。

let a = [0, "1", [2, 3]];
let b = a.slice(1);
console.log(b);
// ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]

console.log(b);
//  ["1", [4, 3]]

可以看出,改变 a[1] 之后b[0]的值并没有发生变化,但改变 a[2][0] 之后,相应的 b[1][0] 的值也发生变化。说明 slice()方法是浅拷贝,相应的还有concat等,在工作中面对复杂数组结构要额外注意。

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

let a = {
 age: 1,
 jobs: {
    first: 'FE'
    } 
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

深拷贝(Deep Copy)

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响

深拷贝使用场景

  • 通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。
let a = {
    age: 1,
    jobs: {
         first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

完全改变变量a之后对b没有任何影响,这就是深拷贝的魔力。但是该方法也是有局限性的:

  • 会忽略 undefined
  • 会忽略symbole
  • 不能序列化函数
  • 不能解决循环引用的对象
  • 不能正确处理new Date()
  • 不能处理正则 undefinedsymbole和函数这三种情况会直接被忽略。
let obj = {
    name: 'muyiy',
    a: undefined,
    b: Symbol('muyiy'),
    c: function() {}
}
console.log(obj);
// {
// 	name: "muyiy", 
// 	a: undefined, 
//  b: Symbol(muyiy), 
//  c: ƒ ()
// }

let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy"}
let obj = {
    a: 1,
    b: {
        c: 2,
        d: 3
    }
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

如果你有这么一个循环引用对象。你会发现你不能通过该方法深拷贝,在遇到函数或者undefined 的时候,该对象也不能正常的序列化

 let a = {
     age: undefined,
     jobs: function() {},
     name: 'yck' 
 }
 let b = JSON.parse(JSON.stringify(a))
 console.log(b) // {name: "yck"}

你会发现在上述情况中,该方法会忽略掉函数和undefined 。但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。当然如果你的数据中含有以上三 种情况下,可以使用 lodash 的深拷贝函数。如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用MessageChannel

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: b
}}
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
const clone = await structuralClone(obj);

自己实现:

function deepClone(obj) {
    if (typeof obj === 'object') {
        var result = obj.constructor === Array ? [] : {};

        for (var i in obj) {
            result[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i];
        }

    } {
        var result = obj;
    }
    return result;
}

总结

*和原始数据是否指向同一对象第一层数据为基本数据类型原始数据中包含子对象
赋值改变会使原数据一同改变改变会使原数据一同改变
浅拷贝改变不会使原数据一同改变改变会使原数据一同改变
深拷贝改变不会使原数据一同改变改变不会使原数据一同改变

参考

详细解析赋值、浅拷贝和深拷贝的区别