JavaScript中对象的复制、浅拷贝、深拷贝

4,226 阅读8分钟

之前在开发中有遇到拷贝的问题,我也看到刚入门同学在用vue开发时所写的赋值相关问题,在使用时,概念使得不清楚使赋值数据模糊不清。为了帮助他人或者帮助自己更能深刻记住,在查阅了相关资料之后在此记录下自己的小小见解。在弄清楚拷贝之前我们先弄清楚数据类型和堆栈的关系

数据类型与堆栈的关系

JS数据类型:JS 的数据类型有几种?

8种,Number、String、Boolean、Null、undefined、object、symbol、bigInt
Symbol

Symbol 本质上是一种唯一标识符,可用作对象的唯一属性名,这样其他人就不会改写或覆盖你设置的属性值

Symbol 数据类型的特点是唯一性,即使是用同一个变量生成的值也不相等

let id1 = Symbol('id'); 
let id2 = Symbol('id'); 
console.log(id1 == id2); //false

Symbol 数据类型的另一特点是隐藏性,for···in,object.keys() 不能访问

let id = Symbol("id"); 
let obj = { [id]:'symbol' }; 
for(let option in obj){ 
    console.log(obj[option]); //空 
}

但是也有能够访问的方法:Object.getOwnPropertySymbols,Object.getOwnPropertySymbols 方法会返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值

JS数据类型:Object 中包含了哪几种类型?

其中包含了 Object, Array, Date, Function, RegExp等

JS数据类型:JS的基本类型有哪些呢?

基本类型(单类型):String、Number、boolean、null、undefined
BigInt

谷歌67版本中出现了一种bigInt,是指安全存储、操作大整数。(但是很多人不把这个做为一个类型) BigInt数据类型的目的是比Number数据类型支持的范围更大的整数值。在对大整数执行数学运算时,以任意精度表示整数的能力尤为重要。使用BigInt,整数溢出将不再是问题。具体请看JavaScript 标准内置对象, JS最新基本数据类型:BigInt

存储方式

基本类型:基本类型值在内存中占据固定大小,保存在`栈内存`中(不包含`闭包`中的变量)
    
引用类型:引用类型的值是对象,保存在`堆内存`中。而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址(引用),引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

这里用一张图表明它们之前储存的关系

闭包与堆内存

闭包中的变量并不保存中栈内存中,而是保存在堆内存中。 这也就解释了函数调用之后之后为什么闭包还能引用到函数内的变量。 我们先来看什么是闭包:

function A() {
  let a = '羊先生'
  function B() {
      console.log(a)
  }
  return B
}

函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。 函数 A 调用后,函数 A 中的变量这时候是存储在堆上的,所以函数B依旧能引用到函数A中的变量

赋值

基本数据类型复制

let a ='羊先生';
let b = a;
b='www.vipbic.com';
console.log(a); // 羊先生

结论:在栈内存中的数据发生数据变化的时候,系统会自动为新的变量分配一个新的之值在栈内存中,两个变量相互独立,互不影响的。

引用数据类型复制

let a = {x:'羊先生', y:'羊先生1'}
let b = a;
b.x = 'www.vipbic.com';
console.log(a.x); // www.vipbic.com

结论 引用类型的复制,同样为新的变量b分配一个新的值,保存在栈内存中,不同的是这个变量对应的具体值不在栈中,栈中只是一个地址指针。两个变量地址指针相同,指向堆内存中的对象,因此b.x发生改变的时候,a.x也发生了改变

浅拷贝

Array的slice和concat方法

Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。之所以把它放在浅拷贝里,是因为它看起来像是深拷贝。而实际上它是浅拷贝。原数组的元素会按照下述规则拷贝:

var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[0] = 2;
console.log(a); // [ 1, 3, 5, { x: 1 } ];
console.log(b); // [ 2, 3, 5, { x: 1 } ];

从输出结果可以看出,浅拷贝后,数组a[0]并不会随着b[0]改变而改变,说明a和b在栈内存中引用地址并不相同。

var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[3].x = 2;
console.log(a); // [ 1, 3, 5, { x: 2 } ];
console.log(b); // [ 1, 3, 5, { x: 2 } ];

从输出结果可以看出,浅拷贝后,数组中对象的属性会根据修改而改变,说明浅拷贝的时候拷贝的已存在对象的对象的属性引用

...扩展运算符

语法: var cloneObj = { ...obj };
let obj = {a:1,b:{c:1}}
let obj2 = {...obj};
obj.a=2;
console.log(obj); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}

obj.b.c = 2;
console.log(obj); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}

扩展运算符也是浅拷贝,对于值是对象的属性无法完全拷贝成2个不同对象

实现简单的引用复制

function shallowClone(copyObj) {
  var obj = {};
  for ( var i in copyObj) {
    obj[i] = copyObj[i];
  }
  return obj;
}
var x = {
  a: 1,
  b: { f: { g: 1 } },
  c: [ 1, 2, 3 ]
};
var y = shallowClone(x);
console.log(y.b.f === x.b.f);     // true

Object.assign()

object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

var obj1 = {
    title: '测试',
    subset: {
        name: '子对象',
        subpoll: {
            des: '子子对象'
        },
        sex: function() {
            console.log('sex')
        }
    },
    age: function() {
        console.log('age')
    }
}

//只从表面上来看,似乎Object.assign()的目标对象是{},是一个新的对象(开辟了一块新的内存空间),是深拷贝
var ojb2 = Object.assign({}, obj1)
ojb2.title = '1111' // obj1 不受影响 
ojb2.subset.name = '2222' // obj1 受影响 
ojb2.subset.subpoll.des = '3333' // obj1 受影响 

// 打印看结果
console.log('obj1:', obj1)
console.log('ojb2:', ojb2)

从打印结果可以看出,Object.assign是一个浅拷贝,它只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址。 Object.assign注意事项 1.只拷贝源对象的自身属性(不拷贝继承属性) 2.它不会拷贝对象不可枚举的属性 3.undefinednull无法转成对象,它们不能作为Object.assign参数,但是可以作为源对象

Object.assign(undefined) // 报错
Object.assign(null) // 报错

let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true

4.属性名为Symbol值的属性,可以被Object.assign拷贝

深拷贝

JSON.parse(JSON.stringify())

JSON.stringify()是前端开发过程中比较常用的深拷贝方式。原理是把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用JSON.parse()反序列化将JSON字符串变成一个新的对象

let arr = [1, 3, {
    username: '羊先生'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'www.vipbic.com'; 
console.log(arr4);// [ 1, 3, { username: 'www.vipbic.com' } ]
console.log(arr);// [ 1, 3, { username: ' 羊先生' } ]

实现了深拷贝,当改变数组中对象的值时候,原数组中的内容并没有发生改变。JSON.stringify()虽然可以实现深拷贝,但是还有一些弊端比如不能处理函数等。

JSON.stringify()实现深拷贝注意点

1.拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失
2.无法拷贝不可枚举的属性,无法拷贝对象的原型链
3.拷贝Date引用类型会变成字符串
4.拷贝RegExp引用类型会变成空对象
5.对象中含有NaN、Infinity和-Infinity,则序列化的结果会变成null
6.无法拷贝对象的循环应用(即obj[key] = obj)
总结: 它会抛弃对象的constructor,深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object;这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON;

手动实现深拷贝

这是看github一个栗子,github.com/wengjq/Blog…

//类型判断
(function($) {
    "use strict";
    var types = "Array,Object,String,Date,RegExp,Function,Boolean,Number,Null,Undefined".split(",");
    for (let i = types.length; i--; ) {
        $["is" + types[i]] = str => Object.prototype.toString.call(str).slice(8, -1) === types[i];
    }
    return $;
})(window.$ || (window.$ = {})); 

function copy(obj, deep = false, hash = new WeakMap()) {
    if (hash.has(obj)) {
        return hash.get(obj);
    }
    if ($.isFunction(obj)) {
        return new Function("return " + obj.toString())();
    } else if (obj === null || typeof obj !== "object") {
        return obj;
    } else {
        var name,
        target = $.isArray(obj) ? [] : {},
        value;
        hash.set(obj, target);
        for (name in obj) {
            value = obj[name];
            if (deep) {
                if ($.isArray(value) || $.isObject(value)) {
                    target[name] = copy(value, deep, hash);
                } else if ($.isFunction(value)) {
                    target[name] = new Function("return " + value.toString())();
                } else {
                    target[name] = value;
                }
            } else {
                target[name] = value;
            }
        }
      return target;
    }
}

//使用
var x = {};
var y = {};
x.i = y;
y.i = x;
var z = copy(x, true);
console.log(x, z);

var a = {};
a.a = a;
var b = copy(a, true);
console.log(a, b);

第三方深拷贝库

lodash

该函数库也有提供_.cloneDeep用来做 Deep Copy(lodash是一个不错的第三方开源库,有好多不错的函数,也可以看具体的实现源码)

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

jquery 提供一个$.extend可以用来做深拷贝
语法:$.extend([deep], target, object1[, objectN] )
**deep:**表示是否深拷贝 默认为false 为true为深拷贝,为false,则为浅拷贝
target: Object类型 目标对象,其他对象的成员属性将被附加到该对象上。
object1  objectN: 可选 Object类型 第一个以及第N个被合并的对象。

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

总结

参考文章

JavaScript中的 Object 类型
javaScript中浅拷贝和深拷贝的实现
深拷贝与浅拷贝的实现

关于我