前端面试常考 | 浅拷贝与深拷贝



一. 前言

1. 概述

深拷贝与浅拷贝在其它语言中也经常被提及,在我们日常的开发中也常常需要利用到这个知识点,当然在前端面试中深浅拷贝也是一个高频的面试点。

2. 数据类型

在介绍浅拷贝和深拷贝之前我们先得需要知道js中的数据类型及其存储的方式:
js的数据类型分别有两大类总共有8种如下所示:
在这里插入图片描述
关于数据类型详解可以参考:点击跳转

3. 存储区别

基本数据类型:直接将数据储存在栈内存中。
引用数据类型:其地址存在栈内存中,而真实数据存储在堆内存中。
如下图所示:
在这里插入图片描述

二. 深浅拷贝

1.深浅拷贝的定义

浅拷贝:在栈内存中重新开辟一块内存空间,并将拷贝对象储存在栈内存中的数据存放到其中。
深拷贝:基于浅拷贝的过程如果栈内存里面存储的是一个地址,那么其在堆内存空间中的数据也会被重新拷贝一个并由栈空间新创建的地址指向。

2. 基本数据类型的拷贝

在对于基本类型的赋值操作都属于深拷贝如下面的例子:

let a = 10;
let b = a;
a = 20;
console.log(a); // 20
console.log(b); // 10 

拷贝过程: 首先会在栈中开辟另一块空间,并将被拷贝对象的栈内存数据完全拷贝到该块空间中,这样两个变量其实指向的并不是同一个数据。

3. 引用类型的拷贝

当引用类型数据进行赋值拷贝时都属于浅拷贝如下面例子:

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

拷贝过程:在上面的例子中赋值的过程中只是将栈内存中指向地址的数据拷贝了一份,其实本质上两个地址都是指向同一份数据,当一方进行数据的修改,两方拿到的都是被修改的数据。

4. 实现 “浅” 拷贝

除了上面我们演示的对于赋值操作,下面将介绍一些开发中可能会用到,当然也可以会被面试官问到的实现深浅拷贝的方法。

1. Object.assign()

方法解释:方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象,它将返回目标对象可以实现一个浅拷贝的效果。
参数一:目标对象
参数二:源对象
示例

var obj1 = {
    
    
            a: 1,
            b: 2,
            c: ['c', 't', 'r']
        }
var obj2 = Object.assign({
    
    }, obj1);
obj2.c[1] = 5;
obj2.b = 3
console.log(obj1); // {a:1,b:2,c:["c", 5, "r"]}
console.log(obj2); // {a:1,b:3,c:["c", 5, "r"]}
console.log(obj1.c); // ["c", 5, "r"]
console.log(obj2.c); // ["c", 5, "r"]

注意:可见Object.assign()方法对于一维数据是深拷贝效果,但是对于多维数据是浅拷贝效果

2. slice()

方法解释:数组进行截取,如果不传参数,会使用默认值,得到一个与原数组元素相同的新数组。
参数一:截取的起始位置
参数二:截取的结束位置
示例

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

注意:可见slice()方法也只是对一维数据进行深拷贝,但是对于多维的数据还是浅拷贝效果。

3. concat()方法

方法解释:数组的拼接(将多个数组或元素拼接形成一个新的数组),不改变原数组,如果不传参数,会使用默认值,得到一个与原数组元素相同的新数组 (复制数组)。
示例

var a = [1, 2, [3, 4]]
var c = [];
var b = c.concat(a);
a[0] = 99
b[2][1] = 88
console.log(a); // [99,2,[3,88]]
console.log(b); // [1,2,[3,88]]

注意:可见concat()方法也只对一维数据具有深拷贝效果,对于多维的数据任然只是浅拷贝

4. es6展开运算符

示例

var a = [1, 2, [3, 4]]
var b = [...a];
a[2][1] = 88
b[1] = 99
console.log(a); // [1,2,[3,88]]
console.log(b); // [1,99,[3,88]]

注意: 可见es6的展开运算符对于一维数据是深拷贝效果,但是对于多维数据任然是浅拷贝效果。

5. 实现 “深” 拷贝

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

JSON.stringfy() 将对象序列化成json对象
JSON.parse() 反序列化——将json对象反序列化成js对象
示例

function deepCopy(obj1){
    
    
    let _obj = JSON.stringify(obj1);
    let obj2 = JSON.parse(_obj);
    return obj2;
}
var a = [1, [1, 2], 3, 4];
var b = deepCopy(a);
b[1][0] = 2;
console.log(a); // 1,1,2,3,4
console.log(b); // 1,2,2,3,4

注意:它会抛弃对象的constructor,深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object类型,这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,
也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON。

2. lodash

示例

import lodash from 'lodash'
var objects = [1,{
    
     'a': 1 }, {
    
     'b': 2 }]; 
var deep = lodash.cloneDeep(objects);
deep[0] = 2;
deep[1].a = 2;
console.log(objects); // [1,{ 'a': 1 }, { 'b': 2 }]
console.log(deep); //[2,{ 'a': 2 }, { 'b': 2 }]

3. 递归拷贝

这也解决了笔试中的一个常见面试题那就是: 手写深拷贝
示例

function deepClone(obj){
    
    
  let objClone =  Array.isArray(obj) ? [] : {
    
    };
  if (obj && typeof obj === 'object') {
    
    
    for(let key in obj){
    
    
      if (obj[key] && typeof obj[key] === 'object'){
    
     // 判断对象的这条属性是否为对象
        objClone[key] = deepClone(obj[key]); // 若是对象进行嵌套调用
      }else{
    
    
        objClone[key] = obj[key]
      }
    }
  }
  return objClone; //返回深度克隆后的对象
}
let arrayA = [{
    
    a: 1, b: 2, aa: [{
    
    ab: 1, ac: [{
    
    ac: 1}]}]}];
let arrayB = deepClone(arrayA);
arrayB[0]['aa'][0].ab = 2;
console.log(arrayA); // [{a: 1, b: 2, aa: [{ab: 1, ac: [{ac: 1}]}]}]
console.log(arrayB); // [{a: 1, b: 2, aa: [{ab: 2, ac: [{ac: 1}]}]}]

注意 : 以上面的方法为例使用递归的方式实现数组、对象的深拷贝,先判断各个字段类型,然后用递归解决嵌套数据。判断要进行拷贝的数据是数组还是对象,是数组的话进行数组拷贝,对象的话进行对象拷贝,进行深拷贝的数据不能为空,并且必须是对象或者是数组。

三. Vue中的深浅拷贝

两个button-counter组件共用同一个jack对象,用同一块地址,当其中一个实例改变时,会影响另一个实例的值(浅拷贝)。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>vue的data选项</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <button-counter></button-counter>
        <button-counter></button-counter>
    </div>

    <script>
        let jack = {
      
      
            counter: 0
        }
        // 子组件
        Vue.component('button-counter', {
      
      
            data() {
      
      
                // 函数类型
                return jack
            },
            template: `<button @click="counter++">click {
       
       {counter}} times</button>`
        })
        let vm = new Vue({
      
      
            el: '#app' // mount到DOM上
        })
    </script>
</body>
</html>

采用深拷贝,重新创建一块内存。这样,vue的button-counter组件中的counter值互不影响。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>vue的data选项</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <button-counter></button-counter>
        <button-counter></button-counter>
    </div>

    <script>
        let jack = {
      
      
            counter: 0
        }
        // 子组件
        Vue.component('button-counter', {
      
      
            data() {
      
      
                // 函数类型
                return JSON.parse(JSON.stringify(jack))
            },
            template: `<button @click="counter++">click {
       
       {counter}} times</button>`
        })
        let vm = new Vue({
      
      
            el: '#app' // mount到DOM上
        })
    </script>
</body>
</html>

猜你喜欢

转载自blog.csdn.net/Siebert_Angers/article/details/128430516