JavaScript中的浅拷贝和深拷贝

620 阅读6分钟

一、前言

我第一次接触浅拷贝深拷贝的概念是在学CC++的时候,虽然现在已经差不多忘得干干净净了,但我记得一点的是它们和内存是有关系的。之后再看到这个概念在面试中提到,我天真的以为,浅拷贝就是这样子的:

var a = { name: '老曹' };
var b = a;

后来大致搞懂后,才知道我是多么的无知...

在讲解之前我相信大家对的概念已经有所了解了,至少应该知道:

栈中存储基本类型的值,是线性结构。 堆中存储引用类型的值,这个值是地址,同时在栈中存放了指向这个地址的指针,是非线性结构。

有了这个基础后,下面就用通俗易懂的话来解释浅拷贝深拷贝吧。

二、基本概念

在说之前要明白一点:不管是浅拷贝还是深拷贝,它们都是针对引用类型。而对于基本类型的值应该被称之为赋值,被赋值的变量和新变量之间不会影响。

2.1 浅拷贝

某个对象的属性被挨个拷贝到另外一个新对象中。如果被拷贝的属性的值是存储的值是引用类型,那么新对象和被拷贝的指针指向同一个地址。

我自己画了一张图,可能不是很准确,但可以表达我个人意思:

在这里插入图片描述

从这看出来,如果被拷贝的属性是基本类型,那么双方不受影响,但是如果是引用类型的话,双方是共享一块地址的,只要某个对象变了就会影响到另外一方。

2.2 深拷贝

深拷贝在浅拷贝的基础上,将值为引用类型的属性递归拷贝到一个新的内存中。

也就是说被拷贝的对象和新对象毫无关联。

在这里插入图片描述

虽然我画的图在一定程度上不准确(我计算机组成原理没学太好,不能用更专业的知识给大家解答了),但是我相信大家对浅拷贝深拷贝已经大致明白是怎么一回事了。那么,接下来就好办了,我们来看看代码是怎么体现的。

三、实现浅拷贝

ECMAScript中已经帮我们实现了一些浅拷贝的api,我来介绍几个:

3.1 Object.assign(target,...source)

该方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。

看下概念也不是很难理解,但是要注意的是这个方法复制的是可枚举属性,对于哪些不可枚举的属性是无法复制的。

说一下可枚举属性:能被for ... inObject.keys遍历出来的属性,更深层来说,该属性的描述符[[enumerable]]true。参数说明如下:

-target:目标对象 -source:被拷贝的对象,可以有多个source对象,它们都会被合并到tarset对象中

demo.js

var source1 = {
  name: '老曹',
  age: 22
};
var source2 = {
  nation: 'China',
  isHandesome: true
};
var source3 = {
  name: '老张',
  friends: ['小刘']
};

var newObj = Object.assign({}, source1, source2, source3);

console.log(newObj);

打印出来的对象如下:

在这里插入图片描述

如果有同名属性将会被覆盖,这个大家需要注意一下。然后我给新对象的属性friends添加一个新元素后:

newObj.friends.push('小罗');
console.log(newObj);
console.log(source3);

我们惊喜地发现source3对象中的friends属性也被添加了新元素。

在这里插入图片描述

细心的你一定能想到,这个功能和展开运算符...是很像的,我们把上面的newObj改为以下代码:

var newObj = { ...source1, ...source2, ...source3 };

效果是一样的,不信你们试试。对了哦,上面两种方法都是ES6以上版本,注意兼容性啊。

3.2 Array.from(arrayLike[, mapFn[, thisArg]])

-arrayLike: 想要转换成数组的伪数组对象或可迭代对象。 -mapFn: 对数组每个元素执行的回调。 -thisArg: 为回调函数指定this

我们来看一段简单的代码:

var a = [1, [2, 3, 4]];
var b = Array.from(a);

console.log("new Array of b:", b);

a[1].push(5);
b[0] = 0;

console.log(a,b);

在控制台打印输出:

在这里插入图片描述

我们可以很清晰地看到b是一个从a浅拷贝过来的数组,数组中的元素如果是基本类型的值就互不影响,如果是引用类型就会受到影响。

这个方法的功能不只是浅拷贝那么简单的,还有很多其他强大的功能,限于篇幅有兴趣的同学可以从下面链接点击。

3.3 Array.prototype.slice([begin[, end]])

-begin:开始索引 -end:结束索引,但不包括该索引

有了上面的铺垫,我相信这个大家肯定会,不多说:

var a = [1, [2, 3, 4]];
var b = a.slice();

console.log(a, b)

b[1].push(0);

console.log(a, b);

在这里插入图片描述

3.4 自己搞一个(只针对普通对象和数组)

分两步走:

  • 判断类型
  • 遍历
function shallowCopy (source) {
  if (typeof source !== "object" || source === null) {
    throw new TypeError(`${source} is not an plain object or array`);
    return;
  }

  let target = source instanceof Array ? [] : {};
  for (let key in source) {
    target[key] = source[key];
  }

  return target;
}

let source = {
  a: 1,
  b: [1, 2, 3]
};

let newObj = shallowCopy(source);

newObj.a = 2;
newObj.b.push(4);

console.log(newObj);
console.log(source);

很多不完善的地方,但是也可以说明浅拷贝。(待我精通JS之时...)

四、实现深拷贝

4.1 JSON.parse(JSON.stringinfy(...))

let source = {
  a: 1,
  b: [1, 2, 3]
};

let target = JSON.parse(JSON.stringify(source));

target.a = 2;
target.b.push(4);

console.log(target); // { a: 2, b: [ 1, 2, 3, 4 ] }
console.log(source); // { a: 1, b: [ 1, 2, 3 ] }

如果不考虑其他复杂的场景,这个方法完全够用了,而且很简单使用API就行了。对于要处理函数这种特殊对象就无能为力了。

4.2 递归

先说下思想,我不敢保证每个人都能记住他曾经写过的代码,但是记住思想一定可以走到最后。以下思想仅代表个人。

  • 对传入的参数进行类型检测。
  • 根据不同对象类型创建一个新实例。
  • 使用for ...in ,判断属性类型,如果是引用就递归。
function deepCopy (source) {
  // 定义目标对象
  let target;
  // 定义判断对象函数
  let isObj = source => ((typeof source === 'object' || typeof source === 'function') && source !== null);

  // 判断是否是对象
  if(!isObj(source)) {
    throw new TypeError(`${source} is not an object or function`);
    return;
  }

  // 根据对象的类型来实例化目标对象,这段代码是核心
  target = new source.constructor();
  // 递归遍历
  for (let key in source) {
    target[key] = isObj(source[key]) ? deepCopy(source[key]) : source[key];
  }

  return target;
}


let source = {
  a: 1,
  b: [1, 2, 3],
  d: /\w/,
  c() {
    console.log('I am a function')
  }
};

let target = deepCopy(source);

target.a = 2;
target.b.push(4);
target.d = /\s/;
target.c = function() { console.log('I am a changed function') };

console.log(target);
console.log(source);

为了测试方便,我把代码放到浏览器控制台执行:

在这里插入图片描述

我们看到了,深拷贝很好的被克隆了,然后给这个函数传递基本值类型,看报的错:

在这里插入图片描述

这个函数实现的我还比较满意,可能会有一些漏洞,如果大家能够指正将感激不尽!

五、总结

总体来说,浅拷贝和深拷贝都是针对引用类型的。在浅拷贝中,如果属性是引用类型,那么两个对象会相互影响。而深拷贝完全就和原来的对象断开关联了,就是在内存中申请了一块的内存存储相同的属性而已。

本人前端小白一枚,正在成长中,如果我的文章对你有帮助,将是我最大的鼓励。最后,谢谢你能看到这。

六、参考

【1】MDN Object.assign()

【2】MDN Array.from()

【3】MDN Array.prototype.slice()

PS:文章首发于CSDN,因为掘(da)金(lao)的颜(tai)值(duo),我决定慢慢把文章搬过来,嘿嘿。