深克隆-Ramda源码中的实现

1,892 阅读3分钟

知识点总结

写一个深克隆的方法,我们需要掌握的知识点

  1. js的数据类型:原始类型,引用类型。
  2. js对象的理解。Date,ExpRep等内置对象的使用。
  3. 使用typeof、Object.prototype.toString进行类型判断。
  4. 闭包的应用。
  5. for in 语句。

js的数据类型

js的类型分两种

  • 原始类型:Undefined、Null、Boolean、Number、String、Symbol
  • 引用类型:Object 在引用类型中,有内置类型Array、Date、RegExp这样的内置类型,也有用户自己用构造函数创建的类型。

深克隆 vs 浅克隆

因为有了引用类型,才有了深克隆和浅克隆的区别。

浅克隆会对于原始类型,可以完全复制,对于引用类型,只会复制它的引用,对其内部的值,不会复制。

深克隆不论对于原始类型,还是引用类型,都会完全复制。生成一个跟原来的对象,完全没有关联的新对象。

浅克隆的实现

写一个最简单的实现,只考虑简单对象的浅克隆

function clone(value) {
  var cloneValue = {};
  for (var prop in value) {
    cloneValue[prop] = value[prop];
  }
  return cloneValue;
}

深克隆的实现

写一个简单的实现,只考虑简单对象的深克隆

function clone(value) {
  var cloneValue = {};
  for (var key in value) {
    if(typeof value[key] === 'object') {
        cloneValue[key] = clone(value[key]);
      } else {
        cloneValue[key] = value[key];
      }
  }
  return cloneValue;
}

在浅克隆的基础上,判断对象的属性值,是否还是个对象,如果是,调用clone,形成递归,我们的深克隆方法就实现啦。

目前这个方法还不完善,增加我们克隆对象的复杂度,不止是简单对象了,还要能克隆数组。

function type(value) {
  return Object.prototype.toString.call(value).match(/\[object (.*?)\]/)[1];
}

function clone(value) {
  var copyFunc = function(copiedValue){
    for(var key in value) {
      if(typeof value[key] === 'object') {
        copiedValue[key] = clone(value[key]);
      } else {
        copiedValue[key] = value[key];
      }
    }
    return copiedValue;
  }
  
  switch (type(value)) {
    case 'Object': return copyFunc({});
    case 'Array': return copyFunc([]);
    default: return value;
  }
}

这次我们除了clone方法,先写了一个type方法,利用Object.prototype.toString判断入参的类型。在clone方法里,我们将上次的clone方法变成一个闭包函数copyFunc,现在的clone方法,先判断入参是对象还是数组,再传入{}或者[]给copyFunc,利用for in语句既可以遍历对象,又可以遍历数组,完成克隆。

现在,我们考虑更多的可克隆对象,除了对象、数组,还有Date、RegExp、这样的内置对象。

function _cloneRegExp(pattern){
  return new RegExp(pattern.source, (
     (pattern.global ? 'g' : '') +
     (pattern.ignoreCase ? 'i' : '') +
     (pattern.multiline ? 'm' : '') +
     (pattern.sticky ? 'y' : '') +
     (pattern.unicode ? 'u' : '') +
  ));
}

function clone(value) {
  var copyFunc = function(copiedValue){
    for(var key in value) {
      if(typeof value[key] === 'object') {
        copiedValue[key] = clone(value[key]);
      } else {
        copiedValue[key] = value[key];
      }
    }
    return copiedValue;
  }
  
  switch (type(value)) {
    case 'Object': return copyFunc({});
    case 'Array': return copyFunc([]);
    case 'Date': return new Date(value.valueOf());
    case 'RegExp': return _cloneRegExp(value);
    default: return value;
  }
}

只需在swith下添加两个case即可。写到这里,我们的克隆方法基本完毕了,可以说这个实现,是我目前看过最清晰的深克隆实现了。

再考虑一种情况,就是对象的多个属性,引用同一个对象多情况。我们希望保证克隆的对象,同样能保持多个属性引用的是同一个克隆出来的对象。

var childObj = { key: 1 };
var obj = {
    key1:childObj,
    key2:childObj,
}

比如上面这个obj对象,我们要克隆它,可以用下面的深克隆实现。

function clone(value, refFrom = [], refTo = []) {
  var copyFunc = function(copiedValue){
    var len = refFrom.length;
    var idx = 0;
    while (idx < len) {
      if (value === refFrom[idx]) {
        return refTo[idx];
      }
      idx += 1;
    }
    refFrom[idx + 1] = value;
    refTo[idx + 1] = copiedValue;
    
    for(var key in value) {
      if(typeof value[key] === 'object') {
        copiedValue[key] = clone(value[key], refFrom, refTo);
      } else {
        copiedValue[key] = value[key];
      }
    }
    return copiedValue;
  }
  
  switch (type(value)) {
    case 'Object': return copyFunc({});
    case 'Array': return copyFunc([]);
    case 'Date': return new Date(value.valueOf());
    case 'RegExp': return _cloneRegExp(value);
    default: return value;
  }
}

这个版本的深克隆,就是ramda源码中对于克隆的实现,优雅又简洁。