JavaScript对象的克隆

1,209 阅读2分钟

1. JavaScript数据类型

我们将 JavaScript中的数据类型分为两大类:原始类型和对象类型。

1.1 原始类型包括:

数值、字符串、布尔值、null、undefined(我们需要克隆的主要是前面三个)

1.2 对象类型包括:

对象(Object),函数(Function)、数组(Array)。在克隆过程中对这两类数据类型的处理方式是不一样的

今天我们着重对象类型的克隆:

2.对象类型克隆

2.1数组的克隆

var x = [1,2,3];
var y = x;
console.log(y);  //[1,2,3]
y.push(4);
console.log(y);  //[1,2,3,4]
console.log(x);  //[1,2,3,4]

对象类型存储的是对象的引用地址,而把对象的实际内容单独存放,因为对象类型通常比较庞大,这是数据开销和内存开销优化的手段。因此我们不能像原始数据类型一样简单的赋值,而应该遍历数据中的每一个元素,将其中的每一个原始数据类型复制过去,做法如下:

var x = [1,2,3];
var y = [];
for (var i = 0; i < x.length; i++) {
    y[i]=x[i];
}
console.log(y);  //[1,2,3]
y.push(4);
console.log(y);  //[1,2,3,4]
console.log(x);  //[1,2,3]

2.2 对象的克隆

参照数组的克隆,我们采用同样的思想进行对象的克隆:

var x = {a:1,b:2};
var y = {};
for(var i in x){
    y[i] = x[i];
}
console.log(y);  //Object {a: 1, b: 2}
y.c = 3;
console.log(y);  //Object {a: 1, b: 2, c: 3}
console.log(x);  //Object {a: 1, b: 2}

2.3函数的克隆

var x = function(){console.log(1);};
var y = x;
y = function(){console.log(2);};
x();  //1
y();  //2

由于函数对象克隆之后的对象会单独复制一次并存储实际数据,因此并不会影响克隆之前的对象。所以采用简单的复制“=”即可完成克隆。

3. JavaScript浅克隆和深度克隆

3.1 浅克隆

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

// 浅克隆函数
function shallowClone(o) {
  const obj = {};
  for ( let i in o) {
    obj[i] = o[i];
  }
  return obj;
}
// 被克隆对象
const oldObj = {
  a: 1,
  b: [ 'e', 'f', 'g' ],
  c: { h: { i: 2 } }
};
​
const newObj = shallowClone(oldObj);
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // true

我们可以很明显地看到,虽然oldObj.c.h被克隆了,但是它还与oldObj.c.h相等,这表明他们依然指向同一段堆内存,我们上面讨论过了引用类型的特点,这就造成了如果对newObj.c.h进行修改,也会影响oldObj.c.h。这本身不是我们想要的,因此就不算是一版好的克隆。

newObj.c.h.i = '我们两个都变了';
console.log(newObj.c.h, oldObj.c.h); // { i: '我们两个都变了' } { i: '我们两个都变了' }

我们改变了newObj.c.h.i的值,oldObj.c.h.i也被改变了,这就是浅克隆的问题所在.

3.2 深克隆

3.2.1 JSON.parse方法

JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,这两个方法结合起来就能产生一个便捷的深克隆.

* deep clone
* @param  {[type]} parent object 需要进行克隆的对象
* @return {[type]}        深克隆后的对象
*/
const clone = parent => {
  // 维护两个储存循环引用的数组
  const parents = [];
  const children = [];

  const _clone = parent => {
    if (parent === null) return null;
    if (typeof parent !== 'object') return parent;

    let child, proto;

    if (isType(parent, 'Array')) {
      // 对数组做特殊处理
      child = [];
    } else if (isType(parent, 'RegExp')) {
      // 对正则对象做特殊处理
      child = new RegExp(parent.source, getRegExp(parent));
      if (parent.lastIndex) child.lastIndex = parent.lastIndex;
    } else if (isType(parent, 'Date')) {
      // 对Date对象做特殊处理
      child = new Date(parent.getTime());
    } else {
      // 处理对象原型
      proto = Object.getPrototypeOf(parent);
      // 利用Object.create切断原型链
      child = Object.create(proto);
    }

    // 处理循环引用
    const index = parents.indexOf(parent);

    if (index != -1) {
      // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象
      return children[index];
    }
    parents.push(parent);
    children.push(child);

    for (let i in parent) {
      // 递归
      child[i] = _clone(parent[i]);
    }

    return child;
  };
  return _clone(parent);
};

浅克隆深克隆参考地址:JavaScript中如何实现深度克隆 - 简书 (jianshu.com)(完整注解)