前端面试必会Javascript系列22-深浅拷贝

113 阅读8分钟

深浅拷贝

经典真题

  • 深拷贝和浅拷贝的区别?如何实现

深拷贝和浅拷贝概念

首先,我们需要明确深拷贝和浅拷贝的概念。

  • 浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)。浅拷贝只复制指向某个对象的指针(引用地址),而不复制对象本身,新旧对象还是共享同一块内存。

  • 深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。

浅拷贝方法

接下来我们来看一下对象有哪些浅拷贝方法。

1. 直接赋值

直接赋值是最常见的一种浅拷贝方式。例如:

var stu = {
    name: 'lao'wang',
    age: 18
}
// 直接赋值
var stu2 = stu;
stu2.name = "zhangsan";
console.log(stu); // { name: 'zhangsan', age: 18 }
console.log(stu2); // { name: 'zhangsan', age: 18 }

2. Object.assign 方法

我们先来看一下 Object.assign 方法的基本用法。

该方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。

如下:

var stu = {
    name: 'laowang'
}
var stu2 = Object.assign(stu, { age: 18 }, { gender: 'male' })
console.log(stu2); // { name: 'laowang', age: 18, gender: 'male' }

在上面的代码中,我们有一个对象 stu,然后使用 Object.assign 方法将后面两个对象的属性值分配到 stu 目标对象上面。

最终得到 { name: 'laowang', age: 18, gender: 'male' } 这个对象。

通过这个方法,我们就可以实现一个对象的拷贝。例如:

const stu = {
    name: 'laowang',
    age: 18
}
const stu2 = Object.assign({}, stu)
stu2.name = 'zhangsan';
console.log(stu); // { name: 'laowang', age: 18 }
console.log(stu2); // { name: 'zhangsan', age: 18 }

在上面的代码中,我们使用 Object.assign 方法来对 stu 方法进行拷贝,并且可以看到修改拷贝后对象的值,并没有影响原来的对象,这仿佛实现了一个深拷贝。

然而,Object.assign 方法事实上是一个浅拷贝。

当对象的属性值对应的是一个对象时,该方法拷贝的是对象的属性的引用,而不是对象本身。

例如:

const stu = {
    name: 'laowang',
    age: 18,
    stuInfo: {
        No: 1,
        score: 100
    }
}
const stu2 = Object.assign({}, stu)
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'laowang', age: 18, stuInfo: { No: 1, score: 90 } }
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }

3. ES6 扩展运算符

首先我们还是来回顾一下 ES6 扩展运算符的基本用法。

ES6 扩展运算符可以将数组表达式或者 string 在语法层面展开,还可以在构造字面量对象时,将对象表达式按 key-value 的方式展开。

例如:

var arr = [1, 2, 3];
var arr2 = [3, 5, 8, 1, ...arr]; // 展开数组
console.log(arr2); // [3, 5, 8, 1, 1, 2, 3]

var stu = {
    name: 'laowang',
    age: 18
}
var stu2 = { ...stu, score: 100 }; // 展开对象
console.log(stu2); // { name: 'laowang', age: 18, score: 100 }

接下来我们来使用扩展运算符来实现对象的拷贝,如下:

const stu = {
    name: 'laowang',
    age: 18
}
const stu2 = {...stu}
stu2.name = 'zhangsan';
console.log(stu); // { name: 'laowang', age: 18 }
console.log(stu2); // { name: 'zhangsan', age: 18 }

但是和 Object.assign 方法一样,如果对象中某个属性对应的值为引用类型,那么直接拷贝的是引用地址。如下:

const stu = {
    name: 'laowang',
    age: 18,
    stuInfo: {
        No: 1,
        score: 100
    }
}
const stu2 = {...stu}
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'laowang', age: 18, stuInfo: { No: 1, score: 90 } }
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }

4. 数组的 sliceconcat 方法

javascript 中,数组也是一种对象,所以也会涉及到深浅拷贝的问题。

Array 中的 sliceconcat 方法,不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。

例如:

// concat 拷贝数组
var arr1 = [1, true, 'Hello'];
var arr2 = arr1.concat();
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 1, true, 'Hello' ]

arr2[0] = 2;
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 2, true, 'Hello' ]
// slice 拷贝数组
var arr1 = [1, true, 'Hello'];
var arr2 = arr1.slice();
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 1, true, 'Hello' ]

arr2[0] = 2;
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 2, true, 'Hello' ]

但是,这两个方法仍然是浅拷贝。如果一旦涉及到数组里面的元素是引用类型,那么这两个方法是直接拷贝的引用地址。如下:

// concat 拷贝数组
var arr1 = [1, true, 'Hello', { name: 'laowang', age: 18 }];
var arr2 = arr1.concat();
console.log(arr1); // [ 1, true, 'Hello', { name: 'laowang', age: 18 } ]
console.log(arr2); // [ 1, true, 'Hello', { name: 'laowang', age: 18 } ]

arr2[0] = 2;
arr2[3].age = 19;
console.log(arr1); // [ 1, true, 'Hello', { name: 'laowang', age: 19 } ]
console.log(arr2); // [ 2, true, 'Hello', { name: 'laowang', age: 19 } ]
// concat 拷贝数组
var arr1 = [1, true, 'Hello', { name: 'laowang', age: 18 }];
var arr2 = arr1.slice();
console.log(arr1); // [ 1, true, 'Hello', { name: 'laowang', age: 18 } ]
console.log(arr2); // [ 1, true, 'Hello', { name: 'laowang', age: 18 } ]

arr2[0] = 2;
arr2[3].age = 19;
console.log(arr1); // [ 1, true, 'Hello', { name: 'laowang', age: 19 } ]
console.log(arr2); // [ 2, true, 'Hello', { name: 'laowang', age: 19 } ]

深拷贝方法

说完了浅拷贝,接下来我们来看如何实现深拷贝。

总结一下,大致有如下的方式。

1. JSON.parse(JSON.stringify)

这是一个广为流传的深拷贝方式,用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

示例如下:

const stu = {
    name: 'laowang',
    age: 18,
    stuInfo: {
        No: 1,
        score: 100
    }
}
const stu2 = JSON.parse(JSON.stringify(stu));
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'laowang', age: 18, stuInfo: { No: 1, score: 100 } }
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }

这种方式看似能够解决问题,但是这种方法也有一个缺点,那就是不能处理函数。

这是因为 JSON.stringify 方法是将一个 javascript 值(对象或者数组)转换为一个 JSON 字符串,而 JSON 字符串是不能够接受函数的。同样,正则对象也一样,在 JSON.parse 解析时会发生错误。

例如:

const stu = {
    name: 'laowang',
    age: 18,
    stuInfo: {
        No: 1,
        score: 100,
        saySth: function () {
            console.log('我是一个学生');
        }
    }
}
const stu2 = JSON.parse(JSON.stringify(stu));
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'laowang', age: 18, stuInfo: { No: 1, score: 100, saySth: [Function: saySth] }}
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }

可以看到,在原对象中有方法,拷贝之后,新对象中没有方法了。

2. 手写递归方法

最终,还是只有靠我们自己手写递归方法来实现深拷贝。

示例如下:

// 类型判断的通用方法
function getType (obj) {
    return Object.prototype.toString.call(obj).slice(8, -1)
}

function deepClone(obj, hash=new Map()) {
    // hash存储克隆过的对象,如果已经克隆过,就直接返回
    if (hash.has(obj)) {
        return obj
    }
    const type = getType(obj);
    const references = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"];
    let res = {};
    if (type === "Object") {
        hash.set(obj);
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) {
                // 这里也可以用obj.hasOwnProperty(key),除非定义了一个同名方法
                res[key] = deepClone(obj[key], hash);
            }
        }
    } else if (type === "Array") {
        res = []
        obj.forEach((item, i) => { 
            res[i] = deepClone(item)
        })
    }
    else if (references.includes(type)) {
        res = new obj.constructor(obj);
    } 
     else {
        res = obj;
    }
    return res;
}

在上面的代码中,我们封装了一个名为 deepClone 的方法,在该方法中,通过递归调用的形式来深度拷贝一个对象。

下面是 2 段测试代码:

// 测试1
const stu = {
    name: 'laowang',
    age: 18,
    stuInfo: {
        No: 1,
        score: 100,
        saySth: function () {
            console.log('我是一个学生');
        }
    }
}
const stu2 = deepClone(stu)
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'laowang', age: 18, stuInfo: { No: 1, score: 100, saySth: [Function: saySth] }}
console.log(stu2); // { name: 'laowang', age: 18, stuInfo: { No: 1, score: 90, saySth: [Function: saySth] }}
// 测试2
var arr1 = [1, true, 'Hello', { name: 'laowang', age: 18 }];
var arr2 = deepClone(arr1)
console.log(arr1); // [ 1, true, 'Hello', { name: 'laowang', age: 18 } ]
console.log(arr2); // [ 1, true, 'Hello', { name: 'laowang', age: 18 } ]

arr2[0] = 2;
arr2[3].age = 19;
console.log(arr1); // [ 1, true, 'Hello', { name: 'laowang', age: 18 } ]
console.log(arr2); // [ 2, true, 'Hello', { name: 'laowang', age: 19 } ]

真题解答

  • 深拷贝和浅拷贝的区别?如何实现

参考答案:

  • 浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)

    浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。

  • 深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。

浅拷贝方法

  1. 直接赋值
  2. Object.assign 方法:可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。当拷贝的 object 只有一层的时候,是深拷贝,但是当拷贝的对象属性值又是一个引用时,换句话说有多层时,就是一个浅拷贝。
  3. ES6 扩展运算符,当 object 只有一层的时候,也是深拷贝。有多层时是浅拷贝。
  4. Array.prototype.concat 方法
  5. Array.prototype.slice 方法
  6. jQuery 中的 .extend:在jQuery中,.extend*:在 *jQuery* 中,*.extend(deep,target,object1,objectN) 方法可以进行深浅拷贝。deep 如过设为 true 为深拷贝,默认是 false 浅拷贝。

深拷贝方法

  1. JSON.parse(JSON.stringify):用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。
  2. 手写递归

示例代码如上:

-EOF-