[关闭]
@Bios 2018-12-10T08:43:18.000000Z 字数 3546 阅读 1245

js克隆

js


浅克隆

浅克隆之所以被称为浅克隆,是因为对象只会被克隆最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存.

  1. // 浅克隆函数
  2. function shallowClone(o) {
  3. const obj = {};
  4. for ( let i in o) {
  5. obj[i] = o[i];
  6. }
  7. return obj;
  8. }
  9. // 被克隆对象
  10. const oldObj = {
  11. a: 1,
  12. b: [ 'e', 'f', 'g' ],
  13. c: { h: { i: 2 } }
  14. };
  15. const newObj = shallowClone(oldObj);
  16. console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
  17. console.log(oldObj.c.h === newObj.c.h); // true

我们可以看到,很明显虽然oldObj.c.h被克隆了,但是它还与oldObj.c.h相等,这表明他们依然指向同一段堆内存,这就造成了如果对newObj.c.h进行修改,也会影响oldObj.c.h,这就不是一版好的克隆.

  1. newObj.c.h.i = 'change';
  2. console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 'change' }

我们改变了newObj.c.h.i的值,oldObj.c.h.i也被改变了,这就是浅克隆的问题所在.
当然有一个新的api Object.assign()也可以实现浅复制,但是效果跟上面没有差别,所以我们不再细说了.

深克隆

JSON.parse方法

前几年微博上流传着一个传说中最便捷实现深克隆的方法,
JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,这两个方法结合起来就能产生一个便捷的深克隆.

  1. const newObj = JSON.parse(JSON.stringify(oldObj));

我们依然用上一节的例子进行测试

  1. const oldObj = {
  2. a: 1,
  3. b: [ 'e', 'f', 'g' ],
  4. c: { h: { i: 2 } }
  5. };
  6. const newObj = JSON.parse(JSON.stringify(oldObj));
  7. console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
  8. console.log(oldObj.c.h === newObj.c.h); // false
  9. newObj.c.h.i = 'change';
  10. console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 2 }

果然,这是一个实现深克隆的好方法,但是这个解决办法是不是太过简单了.
确实,这个方法虽然可以解决绝大部分是使用场景,但是却有很多坑.

1.他无法实现对函数 、RegExp等特殊对象的克隆

2.会抛弃对象的constructor,所有的构造函数会指向Object

3.对象有循环引用,会报错

主要的坑就是以上几点,我们一一测试下.

  1. // 构造函数
  2. function person(pname) {
  3. this.name = pname;
  4. }
  5. const Messi = new person('Messi');
  6. // 函数
  7. function say() {
  8. console.log('hi');
  9. };
  10. const oldObj = {
  11. a: say,
  12. b: new Array(1),
  13. c: new RegExp('ab+c', 'i'),
  14. d: Messi
  15. };
  16. const newObj = JSON.parse(JSON.stringify(oldObj));
  17. // 无法复制函数
  18. console.log(newObj.a, oldObj.a); // undefined [Function: say]
  19. // 稀疏数组复制错误
  20. console.log(newObj.b[0], oldObj.b[0]); // null undefined
  21. // 无法复制正则对象
  22. console.log(newObj.c, oldObj.c); // {} /ab+c/i
  23. // 构造函数指向错误
  24. console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person]

我们可以看到在对函数、正则对象、稀疏数组等对象克隆时会发生意外,构造函数指向也会发生错误。

  1. const oldObj = {};
  2. oldObj.a = oldObj;
  3. const newObj = JSON.parse(JSON.stringify(oldObj));
  4. console.log(newObj.a, oldObj.a); // TypeError: Converting circular structure to JSON

构造一个深克隆函数

我们知道要想实现一个靠谱的深克隆方法,上一节提到的序列/反序列是不可能了,而通常教程里提到的方法也是不靠谱的,他们存在的问题跟上一届序列反序列操作中凸显的问题是一致的.

由于要面对不同的对象(正则、数组、Date等)要采用不同的处理方式,我们需要实现一个对象类型判断函数。

  1. const isType = (obj, type) => {
  2. if (typeof obj !== 'object') return false;
  3. const typeString = Object.prototype.toString.call(obj);
  4. let flag;
  5. switch (type) {
  6. case 'Array':
  7. flag = typeString === '[object Array]';
  8. break;
  9. case 'Date':
  10. flag = typeString === '[object Date]';
  11. break;
  12. case 'RegExp':
  13. flag = typeString === '[object RegExp]';
  14. break;
  15. default:
  16. flag = false;
  17. }
  18. return flag;
  19. };

这样我们就可以对特殊对象进行类型判断了,从而采用针对性的克隆策略.

  1. const arr = Array.of(3, 4, 5, 2);
  2. console.log(isType(arr, 'Array')); // true

对于正则对象,我们在处理之前要先补充一点新知识.

我们需要通过正则的扩展了解到flags属性等等,因此我们需要实现一个提取flags的函数.

  1. const getRegExp = re => {
  2. var flags = '';
  3. if (re.global) flags += 'g';
  4. if (re.ignoreCase) flags += 'i';
  5. if (re.multiline) flags += 'm';
  6. return flags;
  7. };
  1. /**
  2. * deep clone
  3. * @param {[type]} parent object 需要进行克隆的对象
  4. * @return {[type]} 深克隆后的对象
  5. */
  6. const clone = parent => {
  7. // 维护两个储存循环引用的数组
  8. const parents = [];
  9. const children = [];
  10. const _clone = parent => {
  11. if (parent === null) return null;
  12. if (typeof parent !== 'object') return parent;
  13. let child, proto;
  14. if (isType(parent, 'Array')) {
  15. // 对数组做特殊处理
  16. child = [];
  17. } else if (isType(parent, 'RegExp')) {
  18. // 对正则对象做特殊处理
  19. child = new RegExp(parent.source, getRegExp(parent));
  20. if (parent.lastIndex) child.lastIndex = parent.lastIndex;
  21. } else if (isType(parent, 'Date')) {
  22. // 对Date对象做特殊处理
  23. child = new Date(parent.getTime());
  24. } else {
  25. // 处理对象原型
  26. proto = Object.getPrototypeOf(parent);
  27. // 利用Object.create切断原型链
  28. child = Object.create(proto);
  29. }
  30. // 处理循环引用
  31. const index = parents.indexOf(parent);
  32. if (index != -1) {
  33. // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象
  34. return children[index];
  35. }
  36. parents.push(parent);
  37. children.push(child);
  38. for (let i in parent) {
  39. // 递归
  40. child[i] = _clone(parent[i]);
  41. }
  42. return child;
  43. };
  44. return _clone(parent);
  45. };
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注