javascript之深浅拷贝

939 阅读5分钟

开笔前的那些事

关于深拷贝和浅拷贝,也是面试中经常被问到的一个高频题目。然而最近在使用redux的过程中,才发现对这一方面的理解不是很到位。

事情来源是这样的:当我在reducer中更新state时,发现initstate也被修改了,被修改的部分是一个数组,基本类型并没有被修改。

举个栗子:

const initstate = {
    id: '',
    list: []
}

当我将接口返回的数据在reducer中进行以下操作时,发现初始值被修改了。

const listState = (state = initstate, data) => {
    const newState = Object.assign({}, state, data);
    return newState;
}

初步猜想是Object.assign({}, obj);是浅拷贝,于是去网上查阅一番后,证实了猜想。 于是整理了一下关于实现深拷贝和浅拷贝的一些方法。

从概念开始吧

首先我们先讨论一个名词:”赋值“,将某一个数值或对象给某个变量的过程,有这两种情况:

  • 基本数据类型:赋值,赋值之后两个变量互不影响
  • 引用数据类型:赋址,两个变量具有相同的地址引用,指向同一个地址,相互之间影响
关于浅拷贝和深拷贝,区别在于引用类型的拷贝。除了基本类型会直接拷贝属性值外,引用类型在拷贝中是存在两种情况,拷贝实例和拷贝引用,前者就是浅拷贝,后者则属于是深拷贝。

var source = {
    num: 123,
    str: '1234',
    arr: ['zhangsan', 'lisi', 'wangwu'],
    obj: { age: 12, name: 'zhuanzhuan'} 
};

var target = source;

target.arr[0] = '张三';
target.obj.name = '转转';

console.log(target === source);
console.log('---target', target);
console.log('---source', source);

代码中直接将源对象赋值给目标对象,并修改目标对象的数组和对象的某一元素和属性值。下面是打印的日志:


从打印的日志中可以看到,赋值的过程只是将目标对象的指针指向源对象的内存地址,所以当修改目标对象的数组的某一元素或者对象的某一属性值时,源对象也会被修改。这就是所谓的浅拷贝。

那么相反的,深拷贝就是不让目标对象的指针指向源对象的内存地址,而是新开一块内存地址,并且将源对象的数据复制一份到目标对象,两个对象的指针指向不同,那么修改一个时将不会影响另一个。

上面的陈述都是一些最基本的概念,同理就可以知道Object.assign({}, obj);也只是修改了一下指针的指向而没有新开一块内存,那么为什么呢?

浅拷贝

Object.assign({}, obj);

根据MDN中的描述:

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

var source = {
    num: 123,
    str: '1234',
    arr: ['zhangsan', 'lisi', 'wangwu'],
    obj: { age: 12, name: 'zhuanzhuan'} 
};
var target = Object.assign({}, source);
target.arr[0] = '张三';
target.obj.name = '转转';

console.log(target === source);
console.log('---source', source);


所以当我们使用Object.assign({}, obj);拷贝一个对象时,遇到引用类型的属性值,也只是拷贝引用指针,而不是开辟一块新内存,返回的是一个新对象。

解构赋值

浅拷贝中除了直接将对象赋值给一个变量,以及上面说明的Object.assign();方法外,ES6中还提供了一种直接操作对象的方式,就是解构赋值

var source = {
    num: 123,
    str: '1234',
    arr: ['zhangsan', 'lisi', 'wangwu'],
    obj: { age: 12, name: 'zhuanzhuan'} 
};
var target = {...source, newNum: '456'};
target.arr[0] = '张三';
target.obj.name = '转转';

console.log(target === source);
console.log('---source', source);
console.log('---target', target);


所以解构赋值也是浅拷贝,如果一个键的值是复合类型的值(数组、对象、函数),那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

深拷贝

JSON.parse(JSON.stringify(obj));

说起深拷贝,首先放第一位应该是深拷贝中的黑科技,首先用下面的代码展示一下这一行代码的威力之处:

var source = {
    num: 123,
    str: '1234',
    arr: ['zhangsan', 'lisi', 'wangwu'],
    obj: { age: 12, name: 'zhuanzhuan' }
}
var target = JSON.parse(JSON.stringify(source));
target.arr[0] = '张三';
target.obj.name = '转转';

console.log(target === source);
console.log('---source', source);


从结果日志中可以看到,当修改目标对象的某属性值时,源对象并未被影响到。源对象和目标对象也不是指向了同一内存地址,实现了基本的深拷贝。为什么JSON.parse()JSON.stringify()可以实现深拷贝呢?

stringify()方法可以将一个JS对象序列化一个JSON字符串,parse()方法可以将JSON字符串反序列化为一个JS对象。JSON字符串转化为JS对象时会建一个新对象。

但是黑科技还是存在一些缺陷,比如:

  • 会忽略undefined
  • 不能序列化函数
  • 会忽略Symbol
  • 对循环引用的对象无效

var source = {
    num: 123,
    str: '1234',
    arr: ['zhangsan', 'lisi', 'wangwu'],
    obj: { age: 12, name: 'zhuanzhuan'},
    fn: function () { return 'fn' },
    name: undefined,
    age: null
};
var target = JSON.parse(JSON.stringify(source));
target.arr[0] = '张三';
target.obj.name = '转转';

console.log(target === source);
console.log('---source', source);
console.log('---target', target);


从打印的日志中可以看到,原对象的方法和undefined类型的键值在拷贝的过程中丢失了,原因是在序列化JS对象的过程中,所有的函数和undefined类型会被有意的忽略。

通常这种方式也用在请求后端接口时需要过滤掉undefined类型值的时候。

jQuery.extend()

jQuery.extend( [deep ], target, object1 [, objectN ] ),其中deepBoolean类型,如果是true,则进行深拷贝。以下面两段代码为例:

var source = {
    num: 123,
    str: '1234',
    arr: ['zhangsan', 'lisi', 'wangwu'],
    obj: { age: 12, name: 'zhuanzhuan'},
    fn: function () { return 'fn' }
};
var target = $.extend(true, source); // 深拷贝
var target = $.extend(source); // 浅拷贝

总结

浅拷贝的方法:

  • 直接赋值
  • 解构赋值
  • Object.assign({}, sourceObj)
  • jQuery.extend(sourceObj)

深拷贝的方法:

  • JSON.prase(JSON.stringify(obj))
  • jQuery.extend(true, sourceObj)
  • or 自己实现一个吧~haha