前端面试题--深拷贝和浅拷贝

89 阅读4分钟

在JS中,数据类型分为基本数据类型和引用数据类型两种,对于基本数据类型来说,它的值直接存储在栈内存中,而对于引用类型来说,它在栈内存中仅仅存储了一个引用,而真正的数据存储在堆内存中。

1. 当我们对数据进行操作的时候,会发生两种情况

基本数据类型:

let a = 3;
let b = a;
b = 5;
console.log(a); // 3
console.log(b); // 5

可以看到的是对于基本类型来说,我们将一个基本类型的值赋予 a 变量,接着将 a 的值赋予变量 b ; 然后我们修改 b ;可以看到 b 被修改了,而 a 的值没有被修改,两个变量都使用的是独立的数据;

引用数据类型:

let obj1 = {a: 1,b: 2,c: 3}
let obj2 = obj1;
obj2.a = 5;
console.log(obj1.a); // 5
console.log(obj2.a); // 5

可以看到的是,两个对象的值全部被修改了。

对象是引用类型的值,对于引用类型来说,我们将 obj1 赋予 obj2 的时候,我们其实仅仅只是将 obj1 存储在栈堆中的的引用赋予了 obj2 ,而两个对象此时指向的是在堆内存中的同一个数据,所以当我们 修改任意一个值的时候,修改的都是堆内存中的数据,而不是引用,所以只要修改了,同样引用的对象的值也自然而然的发生了改变。

2. 为什么要使用深拷贝

我们希望在改变新的数组(对象)的时候,不改变原数组(对象)

3. 深拷贝

深拷贝作用在引用类型上!例如:Object,Array

深拷贝不会拷贝引用类型的引用,而是将引用类型的值全部拷贝一份,形成一个新的引用类型,这样

就不会发生引用错乱的问题,使得我们可以多次使用同样的数据,而不用担心数据之间会起冲突。

4. 实现一层深拷贝(浅拷贝)

4.1 concat

//数组
let arr1 = [10,20,30]
//concat(数组元素,数组,...):拼接数组元素,不改变原数组,返回新数组
let arr2 = arr1.concat()

4.2 slice

//数组
let arr1 = [10,20,30]
//slice(start,end):截取数组,含头不含尾,不改变原数组,返回新数组
let arr2 = arr1.slice(0)

4.3 扩展运算符

//数组和对象
let obj1 = {
    name: "张三",
    age: 18,
    friend: {
        name: "李四"
    }
}
//一层深拷贝
let obj2 = { ...obj1 }
​
obj1.name = '李素';
console.log(obj1.name);//李素
console.log(obj2.name);//张三
​
obj1.friend.name = "王五";
console.log(obj1.friend.name);//王五
console.log(obj2.friend.name);//王五

4.4 for-in

//数组和对象
let obj1 = {
    name: "张三",
    age: 18,
    friend: {
        name: "李四"
    }
}
//一层深拷贝
function clone(obj) {
    let o = {}
    for (const key in obj) {
        o[key] = obj[key]
    }
    return o
}
​
let obj2 = clone(obj1)
​
obj1.name = '李素';
console.log(obj1.name);//李素
console.log(obj2.name);//张三
​
obj1.friend.name = "王五";
console.log(obj1.friend.name);//王五
console.log(obj2.friend.name);//王五

5. 如何进行多层深拷贝

5.1 使用JSON.stringify()以及JSON.parse()

let _tem = JSON.stringify(obj);//将对象装换为json字符串形式
let result = JSON.parse(_tem);//将转换而来的字符串转换为原生js对象
let obj1 = {
    a: 10,
    b: 20
}
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
obj2.a = 100;
console.log(obj1);
console.log(obj2);

可以看到没有发生引用问题,修改obj2的数据,并不会对obj1造成任何影响

但是使用JSON.stringify()以及JSON.parse()它是不可以拷贝 undefined , function, RegExp 等等 类型的

5.2 递归

递归:在函数内部调用自己,必须有结束递归的条件。

递:一层一层递进去(调用函数)

归:一层一层归出来(函数返回值)

使用递归的方式实现数组、对象的深拷贝

// 使用递归的方式实现数组、对象的深拷贝
function deepClone(data) {
    //判断是否是对象
    if (typeof data === 'object' && data !== null) {
        //判断是否是数组
        let res = Array.isArray(data) ? [] : {};
        //读取对象或者数组的属性
        for (const key in data) {
            //判断是否包含某个属性,不统计原型上的属性
            if (data.hasOwnProperty(key)) {
                res[key] = deepClone(data[key])
            }
        }
        //返回最终结果
        return res;
​
    } else {
        return data;
    }
}
​
// 对象
let obj1 = {
    name: "张三",
    friend: {
        name: "李四"
    }
}
let obj2 = deepClone(obj1);
obj1.friend.name = "王五";
console.log(obj1);
console.log(obj2);
​
// 数组
let arr1 = [10, { age: 18 }]
let arr2 = deepClone(arr1);
arr1[1].age = 100;
console.log(arr1);
console.log(arr2);

5.3 lodash

let obj1 = {
    name: "张三",
    friend: {
        name: "李四"
    }
}
// 深拷贝:_.cloneDeep(value)  value: 要深拷贝的值  返回拷贝后的值。
var obj2 = _.cloneDeep(obj1);
​
obj2.friend.name = "王五";
console.log(obj1.friend.name);//李四
console.log(obj2.friend.name);//王五

总结:

如果一层深拷贝:

对象:扩展运算符、for-in遍历

数组:扩展运算符、for-in遍历、slice、concat等方法。

如果多层深拷贝:

第一种方法:可以使用JSON.stringify()和JSON.parse()对数据进行转换,但是这种方法有弊端,如果数据中有函数、undefined等不可以转换。

第二种方法:自己手写递归函数、思路:判断数据是否是对象或者数组,但是排除null,如果是可以再判断数据的类型是数组还是对象,然后遍历数组,拿到数据在递归判断。

第三种方法:其实我工作中使用lodash库多一些,使用_.cloneDeep(value),如果项目中已经使用lodash库,可以直接使用里面的方法,如果项目一次也没有引用过lodash库,只单纯的想使用一下lodash里面封装的深拷贝的方法,引入一个库,不划算,就可以自己封装递归函数。