Clone

# Clone

# 浅拷贝

拷贝之前,我们想要了解一下我们到底是要拷贝对象些什么类型的属性,这个需要拷贝是什么类型的数据。

数据分为基本数据类型和引用数据类型。

  • 基本数据类型:String、Number、Boolean、Null、Undefined、Symbol。基本数据类型是直接存储在栈中的数据。
  • 引用数据类型:Array、Object。引用数据类型存储的是该对象在栈中引用,真实的数据存储在内存中。

为了能直观的了解,我们先来看看下面的代码:

// 基本数据类型
let str1 = '123';
str2 = str1;
str2 = '456';
console.log(str1); // '123'
console.log(str2); // '456'

// 引用数据类型
let arr1 = [1, 2, 3];
arr2 = arr1;
arr2.push(4);
console.log(arr1); // [1, 2, 3, 4]
console.log(arr2); // [1, 2, 3, 4]

如上,由于基本数据类型是直接存储的,所以如果我们对基本数据类型进行拷贝,然后修改新数据后,不会影响到原数据。

而当你对引用数据类型进行拷贝,然后修改新数据后,它就会影响到原数据。

初步了解上面的基本数据类型和引用数据类型之后,再来看看赋值,浅拷贝和深拷贝

/**
 * @name 赋值
 */
const dataOne = {
  title: 'study',
  number: ['PonyHuang', 'PonyHuang', 'hsq'],
};
const dataTwo = dataOne;
dataTwo.title = 'play';
dataTwo.number = ['null'];
console.log(dataOne);
// dataOne: { title: 'play', number: ['null'] }
console.log(dataTwo);
// dataTwo: { title: 'play', number: ['null'] }

/**
 * @name 浅拷贝
 */
const dataThree = {
  title: 'study',
  number: ['PonyHuang', 'PonyHuang', 'hsq'],
};
const dataFour = shallowClone(dataThree); // shallowClone 待实现
dataFour.title = 'play';
dataFour.number = ['null'];
console.log(datadataThreeOne);
// dataThree: { title: 'study', number: ['null'] }
console.log(dataFour);
// dataFour: { title: 'play', number: ['null'] }

/**
 * @name 深拷贝
 */
const dataFive = {
  title: 'study',
  number: ['PonyHuang', 'PonyHuang', 'hsq'],
};
const dataSix = deepClone(dataFive); // deepClone 待实现
dataSix.title = 'play';
dataSix.number = ['null'];
console.log(dataFive);
// dataFive: { title: 'study', number: ['PonyHuang', 'PonyHuang', 'hsq'] }
console.log(dataSix);
// dataSix: { title: 'play', number: ['null'] }

如上,我们给出结论:

  • 赋值:引用地址的拷贝。修改赋值后的数据,不管是基本数据类型还是引用数据类型,都会影响到原数据。

  • 浅拷贝:一层拷贝。在浅拷贝中,修改基本数据类型不会影响原有数据的基本数据类型,修改引用数据类型会影响原有的数据类型。

  • 深拷贝:无限层级拷贝。在深拷贝中,修改基本数据类型和引用数据类型都不会影响原有的数据类型。

const arr1 = [1, 2, ['PonyHuang', 'hsq'], 4];

const shallowClone = (arr) => {
  const oReturn = [];
  for (let prop in arr) {
    if (arr.hasOwnProperty(prop)) {
        dst[prop] = arr[prop];
    }
  }
  return oReturn;
}

const arr2 = shallowClone(arr1);
arr2[2].push('CoderArtist');
arr2[3] = 5;

console.log(arr1);
// [ 1, 2, [ 'PonyHuang', 'hsq', 'CoderArtist' ], 4 ]
console.log(arr2);
// [ 1, 2, [ 'PonyHuang', 'hsq', 'CoderArtist' ], 5 ]

# 初步实现深拷贝

//myDeepClone
const deepClone = (target) => {
  // if (typeof target !== 'targetect') return new Error('the param must be a object');
  // let Result = {};
  //为了解决属性类型为数组Array,增加类型函数
  const checkType = (target) => {
    return Object.prototype.toString.call(target).slice(8, -1);
  }
  let Result, targetType = checkType(target);
  if (targetType === 'Object') {
    Result = {};
  } else if (targetType === 'Array') {
    Result = [];
  } else {
    return target
  }
  //遍历这个对象
  for (index in target) {
    const value = target[index];
    if (checkType(value) === 'Object' || checkType(value) === 'Array') {
      
      Result[index] = deepClone(target[index])
    } else {
      Result[index] = value;
    }
  }
  return Result;
}

let o1 = {
  name: 'hsq',
  course: {
    major: {
      math: ['xiandai', 'gaoshu', 'gailvlun'],
      computer:['jizu','C','C++','java','python'],
    },
    minor: {
      wenxue:'wenxue'
    }
  }
}
let o2 = deepClone(o1);
console.log(o1);
o2.course.minor.wenxue = 'no'
console.log('o1:', o1.course.minor.wenxue, 'o2:', o2.course.minor.wenxue);
//看数组输出
o2.course.major.math.push('JSxue')
console.log('o1:', o1.course.major.math, 'o2:', o2.course.major.math);

# 遍历对象改用for in

for in可以遍历所有可枚举属性以及原型上的属性

var createObj = function(){
    this.name = "大表哥";
}
var obj1 = new createObj();
createObj.prototype.age = 10;

for(var p in obj1){
    console.log('key:',p);
    console.log('value:',obj1[p]);
}

输出结果

key: name
value: 大表哥
key: age
value: 10

hasOwnProperty()不会从原型上寻找属性

var resName = obj1.hasOwnProperty("name");
console.log("name",resName);

var resAge = obj1.hasOwnProperty("age");
console.log("age",resAge);

输出结果

name true
age false

该深拷贝可以拷贝一般的对象,而且嵌套的层级不能太深

一个特例:循环引用。

let obj = { 
	name:'hsq'
};
obj.link = obj;
console.log(obj); //<ref *1> { val: 100, target: [Circular *1] }
deepClone(obj);//报错: RangeError: Maximum call stack size exceeded

但是这里有一个问题,就是当一个对象有一个属性指向它本身的时候,这个时候深拷贝就会出现爆栈的情况,或者对象里嵌套了很深的对象的时候也会出现爆栈。

想了很多办法,包括利用树来表示对象进行递归,还有就是利用WeakMap进行标记,每一个copy的属性都要加入到map中,当遇到已经在map里的key-value,说明已经copy过一次了,不用copy,直接返回value即可。这样就解决了循环引用导致的重复copy的问题。、

# 改良deepClone

//myDeepClone,为了防止RangeError: Maximum call stack size exceeded;add a WeakMap
const deepClone = (target, map = new WeakMap()) => {
  // console.log(arguments);
  if (!arguments) throw new Error('there is no params');
  //为了解决属性类型为数组Array,增加类型函数
  const checkType = target => Object.prototype.toString.call(target).slice(8, -1);
  
  let Result, targetType = checkType(target);
  if (targetType === 'Object') {
    Result = {};
  } else if (targetType === 'Array') {
    Result = [];
  } else {
    return target;
  }

  //遍历这个对象
  for (index in target) {
    const value = target[index];
    if (map.get(value)) {
      return value
    }
    map.set(target, value);
    if (checkType(value) === 'Object' || checkType(value) === 'Array') {
      
      Result[index] = deepClone(target[index])
    } else {
      Result[index] = value;
    }
  }
  console.log('map:',map);
  return Result;
}
/* 
let o1 = {
  name: 'hsq',
  course: {
    major: {
      math: ['xiandai', 'gaoshu', 'gailvlun'],
      computer:['jizu','C','C++','java','python'],
    },
    minor: {
      wenxue:'wenxue'
    }
  }
}
let o2 = deepClone(o1);
console.log(o1);
o2.course.minor.wenxue = 'no'
console.log('o1:', o1.course.minor.wenxue, 'o2:', o2.course.minor.wenxue);
//看数组输出
o2.course.major.math.push('JSxue')
console.log('o1:', o1.course.major.math, 'o2:', o2.course.major.math); */


// Maximum call stack size exceeded
let obj = {
  val: 100,
  fn: (a, b)=>{
    console.log(a + b);
    return a + b;
  }
};
obj.obj = obj;
console.log(obj);
let out = deepClone(obj);
console.log('out:',out);
out.out = out;
console.log(out);
// 输出结果
// <ref *1> { val: 100, fn: [Function: fn], obj: [Circular *1] }
//out: <ref *1> { val: 100, fn: [Function: fn], obj: [Circular *1] }
//<ref *1> {
//  val: 100,
//  fn: [Function: fn],
//  obj: [Circular *1],
//  out: [Circular *1]
//}

下面看看还有没有其他的边界条件没有解决

上面的代码能解决引用自身的问题,因为做了标记,对于深度=1000的也可以拷贝,但是当深度=10000,马上就出现了爆栈。思考思考,其实我们可以将这个对象看成是一棵树,然后去遍历这棵树。

话不多说,敲代码:

# 树表示法

function deepClone(x) {
  const root = {};

  // 栈
  const loopList = [
    {
      parent: root,
      key: undefined,
      data: x,
    }
  ];

  while (loopList.length) {
    // 深度优先
    const node = loopList.pop();
    const parent = node.parent;
    const key = node.key;
    const data = node.data;

    // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
    let res = parent;
    if (typeof key !== 'undefined') {
      res = parent[key] = {};
    }

    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        if (typeof data[k] === 'object') {
          // 下一次循环
          loopList.push({
            parent: res,
            key: k,
            data: data[k],
          });
        } else {
          res[k] = data[k];
        }
      }
    }
  }

  return root;
}

function createData(deep, breadth) {
  var data = {};
  var temp = data;

  for (var i = 0;i < deep;i++) {
    temp = temp['data'] = {};
    for (var j = 0;j < breadth;j++) {
      temp[j] = j;
    }
  }

  return data;
}
let obj = createData(1000, 1000);
let out = deepClone(obj);
console.log(out);

然后想了想,应该把引用自身和树的遍历结合起来

# 树加Map

// 保持引用关系
function cloneForce(x) {
  // =============
  const uniqueList = []; // 用来去重
  // =============
  let root = {};
  
  // 循环数组
  const loopList = [
    {
      parent: root,
      key: undefined,
      data: x,
    }
  ];
  while (loopList.length) {
    // 深度优先
    const node = loopList.pop();
    const parent = node.parent;
    const key = node.key;
    const data = node.data;

    // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
    let res = parent;
    if (typeof key !== 'undefined') {
      res = parent[key] = {};
    }

    // =============
    // 数据已经存在
    let uniqueData = find(uniqueList, data);
    if (uniqueData) {
      parent[key] = uniqueData.target;
      break; // 中断本次循环
    }

    // 数据不存在
    // 保存源数据,在拷贝数据中对应的引用
    uniqueList.push({
      source: data,
      target: res,
    });
    // =============

    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        if (typeof data[k] === 'object') {
          // 下一次循环
          loopList.push({
            parent: res,
            key: k,
            data: data[k],
          });
        } else {
          res[k] = data[k];
        }
      }
    }
  }

  return root;
}

function find(arr, item) {
  for (let i = 0;i < arr.length;i++) {
    if (arr[i].source === item) {
      return arr[i];
    }
  }

  return null;
}

// test Data

function createData(deep, breadth) {
  var data = {};
  var temp = data;
  for (var i = 0;i < deep;i++) {
    temp = temp['data'] = {};
    for (var j = 0;j < breadth;j++) {
      temp[j] = j;
    }
  }
  return data;
}
let obj = createData(10000, 10000);
let out = cloneForce(obj);
console.log(out);