我悟了!一文看懂赋值,浅拷贝与深拷贝

489 阅读6分钟

知识储备-数据类型

在JS中,我们的数据类型分为以下两种:

基本类型: number,string,boolean,null,undifined,symbol

引用类型: object,array,function

这里一定要分清楚这两种不同类型的数据,他们主要的区别是在内存中的存储位置不同。

JavaScript中的堆和栈

首先我们先讲讲为什么存储位置不一样:

  • 基本类型也叫原始数据类型是直接存储在栈中的简单数据,它主要的特点就是占据空间小大小固定频繁被使用
  • 引用类型是存储在堆中的对象,他主要的特点就是占据空间大大小不固定,不将它存储在栈中是因为数据太大会影响到性能。但是引用类型会在栈中开辟空间保存一个引用地址来指向堆中所储存的对象,这里你就可以把栈中储存的引用地址看做一把钥匙,堆中的对象看成一扇,钥匙都是一一匹配的。

堆和栈的区别:

  • 堆比栈空间大,栈比堆运行速度快
  • 堆内存是无序存储,可以根据引用直接获取
  • 基础数据类型比较稳定,而且相对来说占用的内存较小
  • 引用数据类型大小是动态的,无限的

赋值

基本数据类型: 基本数据类型的变量赋值,会在栈中开辟出一个新的空间来存储这个变量,两个变量之间是互相不会影响的,这个时候你修改一个变量另一个变量是不会发生改变的。

var a = 1;
var b = a;
a = 999
console.log(a)   // 999
console.log(b)   // 1

图解: 屏幕快照 2021-10-28 16.34.07.png

引用类型: 引用类型的赋值相当于是给变量赋上了同一个对象的引用地址,这个时候就会出现问题了,由于两个变量作为引用都指向同一个对象,这个时候修改对象的属性是相互影响的。是对象的所有属性,不分数据类型的哦,这里注意一下。

var obj = {
    name: 'liming',
    son: {
        name: 'xiaoming',
        age: 19
    }
}
var objCopy = obj
objCopy.name = 'zhanghong'
objCopy.son.name = 'xiaohong'

console.log(obj)       // {
                                name: 'zhanghong',
                                son: {
                                    name: 'xiaohong',
                                    age: 19
                                }
                            }
                            
console.log(objCopy)   // {
                                name: 'zhanghong',
                                son: {
                                    name: 'xiaohong',
                                    age: 19
                                }
                            }

图解: 屏幕快照 2021-10-28 16.45.45.png

总结:这里大家可以理解为两人拥有了同一个门的钥匙,也就是objCopy拿到了obj门的钥匙去配钥匙的地方配了一把一模一样的。这个时候两者都能打开这个门,门里面的东西不管是谁动了都是会相互影响的。

浅拷贝

下面我们来说一下浅拷贝和深拷贝:这块我们都是针对引用类型来进行拷贝的,务必切记。

我们来手写一个浅拷贝,请看以下例子:

var obj = {
    name: 'liming',
    son: {
        name: 'xiaoming',
        age: 19
    }
}

var objCopy = shallowCopy(obj)     //这块的赋值因为有函数提前,所以不影响赋值

function shallowCopy(object){
        var newObj = {};
        for(var prop in object ){
            if(object.hasOwnProperty(prop)){
                newObj[prop] = src[prop]
            }
        }
        return newObj
}   

// hasOwnProperty()方法返回一个布尔值,判断使用对象自身属性中是否有当前属性

objCopy.name = 'zhanghong'
objCppy.son.name = 'xiaohong'
console.log(obj)                  //{
                                        name: 'liming',
                                        son: {
                                            name: 'xiaohong',
                                            age: 19
                                        }
                                    }
console.log(objCopy)              //{
                                        name: 'zhanghong',
                                        son: {
                                            name: 'xiaohong',
                                            age: 19
                                        }
                                    }

图解:

屏幕快照 2021-10-28 17.38.42.png 相信细心的大家已经发现了一点,浅拷贝之后,基本类型的数据修改并不会相互影响,但是引用类型的值依然会相互影响。这里的原因如下:

  • 浅拷贝是对原对象的精准赋值,如果是基本类型的拷贝,是拷贝了基本类型的值,且修改基本类型的值不会对原对象产生影响。
  • 如果浅拷贝需要拷贝引用类型,则是将原对象引用类型的值的引用地址拷贝了一份,这个时候就相当于针对于引用类型的值,两个对象都拿着同一把钥匙,这个时候修改引用类型的值,是会相互影响的。

总结:浅拷贝只针对对象的第一层数据,基本类型的数据是值的copy,但是引用类型依然是copy了引用地址,相互之间会产生影响。

浅拷贝实现方法:

我们日常开发中会用到的浅拷贝方法有不少,这里我们大概列举几种方法:

  • Object.assign() Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
    var obj = {
        name: 'liming',
        son: {
            name: 'xiaoming',
            age: 19
        }
    }
    var objCopy = Object.assign({},obj)
    objCopy.name = 'zhanghong'
    objCopy.son.name = 'xiaohong'
    
    console.log(obj)         //{
                                    name: 'liming',
                                    son: {
                                        name: 'xiaohong',
                                        age: 19
                                    }
                                }
    console.log(objCopy)     //{
                                    name: 'zhanghong',
                                    son: {
                                        name: 'xiaohong',
                                        age: 19
                                    }
                                }
  • 数组slice()方法
        var arr = ['hello', 25, { sex:'man' }];
        let arr1 = arr.slice()
        arr1[2].sex='women'
        arr1[0]='world'
        console.log( arr )     // ['hello', 25, { sex:'women' }];
        console.log( arr1 )    // ['world', 25, { sex:'women' }];
  • 数组concat()方法
        var arr = ['hello', 25, { sex:'man' }];
        let arr1 = arr.concat()
        arr1[2].sex='women'
        arr1[0]='world'
        console.log( arr )     // ['hello', 25, { sex:'women' }];
        console.log( arr1 )    // ['world', 25, { sex:'women' }];
  • ES6解构赋值
let a = {
    age: 18,
    name: 'liming',
    address: {
        city: 'nanjing'
    }
}
 
let b = {...a};
b.age=80
b.address.city = 'beijing';
 
console.log(a)      // {
                            age: 18,
                            name: 'liming',
                            address: {
                                city: 'beijing'
                            }
                        }
console.log(b)     // {
                            age: 80,
                            name: 'liming',
                            address: {
                                city: 'beijing'
                            }
                        }

深拷贝

对比上面的浅拷贝和赋值,大家一定知道了,深拷贝是为了引用类型进行变量赋值的时候,使两个变量完全相互独立,互不影响,避免开发逻辑上的问题。

手动实现深拷贝:

手动实现深拷贝用到的就是递归

var obj = {   
       //原数据,包含字符串、对象、函数、数组等不同的类型
       name:"hello",
       count:{
           a:1,
           b:2
       },
       fn:function(){
           
       },
       list:[1,2,3,[22,33]]
   }
 
   function copy(obj){
    let newobj = null;   //声明一个变量用来储存拷贝之后的内容
        
        //判断数据类型是否是复杂类型,如果是则调用自己,再次循环,如果不是,直接赋值即可,
        //由于null不可以循环但类型又是object,所以这个需要对null进行判断
     
    if(typeof(obj) == 'object' && obj !== null){ 
        
        //根据参数的具体数据类型声明不同的类型来储存
        newobj = obj instanceof Array? [] : {};   
            
        //循环obj 中的每一项,如果里面还有复杂数据类型,则直接利用递归再次调用copy函数
        for(var i in obj){  
              newobj[i] = copy(obj[i])
         }
        }else{
            newobj = obj
        }    
      return newobj;    //函数必须有返回值,否则输出undefined
   }
 
   var objCopy = copy(obj)
   obj2.list[0] = 999
   obj2.name = 'world'
   obj2.count.a = 100
   
   console.log(obj)        //{   
                                   name:"hello",
                                   count:{
                                       a:1,
                                       b:2
                                   },
                                   fn:function(){

                                   },
                                   list:[1,2,3,[22,33]]
                               }
   console.log(obj2)     //{   
                                   name:"world",
                                   count:{
                                       a:100,
                                       b:2
                                   },
                                   fn:function(){

                                   },
                                   list:[999,2,3,[22,33]]
                               }

图解: 屏幕快照 2021-10-28 17.53.11.png

补充深拷贝方法:

  • JSON.parse()配合JSON.stringify()
        var arr = ['hello', 25, { sex:'man' }];
        let arr1 = JSON.parse(JSON.stringify(arr))
        arr1[2].sex='women'
        arr1[0]='world'
        console.log( arr )     // ['hello', 25, { sex:'man' }];
        console.log( arr1 )    // ['world', 25, { sex:'women' }];

这种方法深拷贝有很多弊端,列举几个常见的:

  1. obj里面时间对象将只是字符串的形式。而不是时间对象。
  2. 如果obj里有RegExp、Error对象,结果会只得到空对象。
  3. obj里的函数,undefined会丢失。
  4. obj里的NaN、Infinity和-Infinity会变成null
  • 借助第三方深拷贝库实现深拷贝 这种方法最实用,比较推荐使用,成本低,基本不会出错。常用的第三方库是lodash

那么文章到这里就结束了,总结如下表格所示:

操作是否指向同一堆地址基本数据类型引用数据类型
赋值改变使原数据改变改变使原数据改变
浅拷贝改变不会使原数据改变改变使原数据改变
深拷贝改变不会使原数据改变改变不会使原数据改变

如果文章有错误之处,欢迎大家指正,谢谢观看!