吃透Js深入学习浅拷贝和深拷贝

Posted 静Yu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了吃透Js深入学习浅拷贝和深拷贝相关的知识,希望对你有一定的参考价值。

想要真正搞明白深浅拷贝,你必须要熟练掌握赋值、对象在内存中的存储、数据类型等基础知识。

为了更好地掌握深浅拷贝,我们先来看一下数据类型和在内存中的存储形式。

一、javascript数据类型

原始类型

  • Null:只包含一个值:null
  • Undefined:只包含一个值:undefined
  • Boolean:包含两个值:truefalse
  • Number:整数或浮点数,还有一些特殊值(-Infinity+InfinityNaN
  • String:一串表示文本值的字符序列
  • Symbol:一种实例是唯一且不可改变的数据类型

对象类型

  • Object:自己分一类丝毫不过分,除了常用的Object,Array、Function等都属于特殊的对象

二、原始类型和对象类型的区别

1.原始类型

在JavaScript中,每一个变量都需要一个内存空间来存储。
内存空间被分为两种:堆内存和栈内存。

JavaScript中的原始类型的值被直接存储在栈中,在变量定义时,栈就为其分配好了内存空间。

栈内存特点:

  • 存储的值大小固定
  • 空间较小
  • 可以直接操作其保存的变量,运行效率高
  • 由系统自动分配存储空间

原始类型的特点就是不可变性,即值本身是不可被改变的。

var str = 'JingYu';
str.slice(1);
str.substr(1);
str.trim(1);
str.toLowerCase(1);
str[0] = 1;
console.log(str);  // JingYu

有一个特殊情况:

str += '6'
console.log(str);  // JingYu6

这个情况是不满足不可变性了吗?不是的,在上面的代码中,我们执行了str += ‘6’的操作,实际上是在栈中又开辟了一块内存空间用于存储’JingYu6’,然后将变量str指向这块空间,所以这并不违背不可变性的特点。

2.引用类型

JavaScript中的引用类型(对象类型)的值实际被直接存储在堆内存中,在栈内存中只存储了一个固定长度的地址,这个地址指向堆内存中的值。

堆内存特点:

  • 存储的值大小不定,可动态调整
  • 空间较大,运行效率低
  • 无法直接操作其内部存储,使用引用地址读取
  • 通过代码进行分配空间

引用类型就不具有不可变性的特点了,我们可以轻易改变它,尤其是数组有很多函数可以改变:

  • pop() 删除数组最后一个元素,如果数组为空,则不改变数组,返回undefined,改变原数组,返回被删除的元素
  • push()向数组末尾添加一个或多个元素,改变原数组,返回新数组的长度
  • shift()把数组的第一个元素删除,若空数组,不进行任何操作,返回undefined,改变原数组,返回第一个元素的值
  • unshift()向数组的开头添加一个或多个元素,改变原数组,返回新数组的长度
  • reverse()颠倒数组中元素的顺序,改变原数组,返回该数组
  • sort()对数组元素进行排序,改变原数组,返回该数组
  • splice()从数组中添加/删除项目,改变原数组,返回被删除的元素

3.复制

当我们把一个变量复制到另一个变量的时候,原始类型和引用类型的表现也是不同的。
原始类型:

var name = 'JingYu';
var name2 = name;
name2 = 'JINGYU';
console.log(name); // JingYu;

通过输出结果可以看出,我们修改name2的结果的时候,对name没有任何的影响。那是因为我们将变量name复制给name2的时候是在栈内存空间中创建了一块新的内存空间,这块内存空间存储的是变量name2的值,值与变量name是相同的但是内存空间地址是完全不同的,所以修改name2之后name的值不变。
引用类型:

var obj = name:'JingYu';
var obj2 = obj;
obj2.name = 'JINGYU';
console.log(obj.name); // JINGYU

你会惊奇的发现,显示的结果和原始类型是不同的。这又是为什么呢?
那是因为当我们复制引用类型的变量时,实际上复制的是栈中存储的地址,所以复制出来的obj2实际上和obj指向的堆中同一个对象。因此,我们改变其中任何一个变量的值,另一个变量都会受到影响,这就是为什么会有深拷贝和浅拷贝的原因。

4.比较

先看一下代码,猜猜运行结果…

var name = 'JingYu';
var name2 = 'JingYu';
console.log(name === name2); // true
var obj = name:'JingYu';
var obj2 = name:'JingYu';
console.log(obj === obj2); // false

对于原始类型,比较时会直接比较它们的值,如果值相等,即返回true。
对于引用类型,比较时会比较它们的引用地址,虽然两个变量在堆中存储的对象具有的属性值都是相等的,但是它们被存储在了不同的存储空间,因此比较值为false。

5.值传递

首先给出结论:ECMAScript中所有的函数的参数都是按值传递的。
ECMAScript中是没有引用传递的。

let name = 'JingYu';
function changeValue(name)
  name = 'JINGYU';

changeValue(name);
console.log(name);

很明显,上面的执行结果是’JingYu’,即函数参数仅仅是被传入变量复制给了的一个局部变量,改变这个局部变量不会对外部变量产生影响。

let obj = name:'JingYu';
function changeValue(obj)
  obj.name = 'JINGYU';

changeValue(obj);
console.log(obj.name); //JINGYU

当函数参数是引用类型时,我们同样将参数复制了一个副本到局部变量,只不过复制的这个副本是指向堆内存中的地址而已,我们在函数内部对对象的属性进行操作,实际上和外部变量指向堆内存中的值相同,但是这并不代表着引用传递。

let obj = ;
function changeValue(obj)
  obj.name = 'JingYu';
  obj = name:'JINGYU';

changeValue(obj);
console.log(obj.name); // JingYu

obj = name:'JINGYU';这个只是函数内部的局部对象。

三、浅拷贝

基础知识前面我们已经介绍过了。现在进入我们的正题。

概念

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

实现方法

简单的浅拷贝

function shallowClone(obj) 
    const newObj = ;
    for(let prop in obj) 
        if(obj.hasOwnProperty(prop))
            newObj[prop] = obj[prop];
        
    
    return newObj;

在JavaScript中,存在浅拷贝现象的还有三种:
Object.assign

var obj = 
    age: 18,
    nature: ['smart', 'good'],
    names: 
        name1: 'fx',
        name2: 'xka'
    ,
    love: function () 
        console.log('fx is a great girl')
    

var newObj = Object.assign(, fxObj);

Array.prototype.slice(), Array.prototype.concat()

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

使用拓展运算符实现的复制

const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

四、深拷贝

概念

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式:
_.cloneDeep()

const _ = require('lodash');
const obj1 = 
    a: 1,
    b:  f:  g: 1  ,
    c: [1, 2, 3]
;
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

jQuery.extend()

const $ = require('jquery');
const obj1 = 
    a: 1,
    b:  f:  g: 1  ,
    c: [1, 2, 3]
;
const obj2 = $.extend(true, , obj1);
console.log(obj1.b.f === obj2.b.f); // false

JSON.stringify()

const obj2=JSON.parse(JSON.stringify(obj1));

但是这种方式存在弊端,会忽略undefined、symbol和函数

const obj = 
    name: 'A',
    name1: undefined,
    name3: function() ,
    name4:  Symbol('A')

const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // name: "A"

手写循环递归

function deepClone(obj, hash = new WeakMap()) 
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) 
    if (obj.hasOwnProperty(key)) 
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    
  
  return cloneObj;

五、浅拷贝、深拷贝和赋值的区别

下面首先借助两张图,可以更加清晰看到浅拷贝与深拷贝的区别


从上图发现,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象

// 浅拷贝
const obj1 = 
    name : 'init',
    arr : [1,[2,3],4],
;
const obj3=shallowClone(obj1) // 一个浅拷贝方法
obj3.name = "update";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存

console.log('obj1',obj1) // obj1  name: 'init',  arr: [ 1, [ 5, 6, 7 ], 4 ] 
console.log('obj3',obj3) // obj3  name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] 

但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

// 深拷贝
const obj1 = 
    name : 'init',
    arr : [1,[2,3],4],
;
const obj4=deepClone(obj1) // 一个深拷贝方法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存

console.log('obj1',obj1) // obj1  name: 'init', arr: [ 1, [ 2, 3 ], 4 ] 
console.log('obj4',obj4) // obj4  name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] 

浅拷贝和赋值

赋值

 var obj1=
        name:'张三',
        age:18,
        class:['一班']
    
    var obj2=obj1//进行了赋值的操作
    obj2.name='李四'
    obj2.class[0]='二班'
    console.log(obj1)
    console.log(obj2)


从例子的可以看出赋值后的对象obj2改变,原对象obj1的值也改变.这是因为赋值后的对象obj2赋值的是原对象obj1的栈内存地址,他们指向的是同一个堆内存数据,所以对赋值后的对象obj2对数据进行操作会改变公共的堆内存中的数据,所以原对象的值也改变了。
浅拷贝

 var obj1=
        name:'张三',
        age:18,
        class:['一班']
    
  function qianCopy(obj)
    var obj2=
    for(var attr in obj)//循环对象的所有属性值
         if(obj.hasOwnProperty(attr))
             obj2[attr]=obj1[attr]
         
    
    return obj2
  
  var obj3=qianCopy(obj1)
  obj3.name='李四'
  obj3.age = 20
  obj3.class[0]='二·班'
  console.log(obj1)
  console.log(obj3)


从结果可以看出obj3改变了基本类型的值name,并没有使原对象obj1的name改变,obj3改变了引用类型的值,导致原对象的值也改变了

六、小结

赋值是完全复制,将一个对象赋值给另一个对象时,只是将一个对象在栈中的内存地址复制给另外一个对象,如果改变其中一个对象的属性值时另外一个对象的属性值也会跟着改变
浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,复制的是对象的内存地址,两个对象指向同一个地址
深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址

以上是关于吃透Js深入学习浅拷贝和深拷贝的主要内容,如果未能解决你的问题,请参考以下文章

js对象浅拷贝和深拷贝详解

JavaScript学习(七十八)—实现对数据的浅拷贝和深拷贝

JS-[浅拷贝和深拷贝]

浅拷贝和深拷贝

Javascript中的浅拷贝和深拷贝

JavaScript27_浅拷贝和深拷贝 + 对象的复制