{JS} Clone 框架层面的深浅拷贝问题Mon Sep 11 2017

对象的Clone在Javascript中的应用场景还是不少的,尤其是在编写框架或者库的时候,多多少少都会接触到. 刚写到这里,可能有人会问我,既然Object.create 和 Object.assign 都可以“拷贝” 一个对象,那么为什么不用原生的方法,还需要自己重新实现一个. 看官不着急,我们先来看看以下分析(更多测试请自行JSPerf)


  • 原生拷贝(Object.assign, Object.create, Object.clone) 性能低下,请见测试(越长越好):

  • 兼容性方面存在各种问题,Object.assign在IE系列浏览器全军覆没

  • Object.create() 对于已经执行了Object.freeze 的对象拷贝无效

  • Object.clone() Chrome完全不支持,浏览器兼容差异巨大

  • 从框架层面上来讲,高效的拷贝操作是性能的关键点


诸多的问题,浏览器的实现不一 ,再加之原生方法效率极其低下,尤其是ES2015的Object.assign方法, 目前的浏览器优化简直愚蠢.

test




基本知识

首先,第一你需要知道,什么东西需要被拷贝,而什么不需要被拷贝

Javascript 的数据类型,官方给出的结论是:

JavaScript 数据类型和数据结构

Undefined, 
Null, 
Number,
String, 
Boolean, 
Object, 
Symbol //(ES2015)

在拷贝前,我们对类型进行整理(动态类型脚本语言,回归问题本质,40多年计算机语言发展经验来看,依然是强静态编译型语言效率最高)

不需要拷贝的数据类型,在拷贝过程中直接进行赋值操作, 我们称其为原始值(Primitive) :

字符串,数字(包含NaN),函数(指针),布尔值, undefined,null, DOM节点(Element, 或者Node);


需要拷贝的数据类型, 我们称之为引用值

对象(Object) ,数组(Array),类数组(ArrayLike) 其实本质都是Object.




浅拷贝

浅拷贝的意思是指,遇到需要拷贝的数据类型(见上), 对其做浅层的拷贝, 而其内部的数据不执行拷贝,例如:

// 举个例子 

var a = { a:1 };

var arr1 = [a];

// 浅拷贝
arr2 = clone(arr1);


// 一定是true
// 也就是说,a在内存中的地址是不变的(指针)
arr2[0] === arr1[0]   // -> true

// 但是,两者不再相等(指针不再指向同一个地址,arr2重新分配了空间)
arr2 === arr1   // -> false

浅拷贝无论何时都最好只用于纯数据类型的对象,或者数组(也就是不存在需要拷贝的数据类型-见上)

最偷懒,也是浏览器层面优化比较好的方式,就是使用JSON的解析和序列化API, 既简单,效率也非常高(v8层面又C++实现的解析引擎):

var arr2 = JSON.parse(JSON.stringify(arr1));

但是,这种浅拷贝的方法存在以下问题:

  • 一旦对象中存在函数类型(Function),此解析器会自动忽略掉Function
  • 字符串化之后必须是标准类型的JSON格式
  • 原型链丢失

所以,我认为这种拷贝方式,最适合纯数据类型的对象或者数组, 那么接下来,我们需要重点看看,框架级别层面的深度拷贝:




*深拷贝

首先我们需要来看,深拷贝需要达到的目标是什么:

var a   = { a:1 }
var b   = { b:2 , a:a } // 引用a
var arr = [ b , { c : 3 } ]; // 拷贝目标

var arr2 = deepclone(arr);

// 所有的指针引用,都不相等(也就是说,子元素同样被拷贝了一份)
arr2[0] === arr[0]       // b === b ?  -> false

arr2[1] === arr[1]       // c === c ?  -> false

arr2[0].a === arr[0].a   // a === a ?  -> false

如何实现? 在Google上我没有找到很漂亮的实现,多数例子中的代码臃肿,厚重,编写者直观思维,这里我放一下Struct中实现的深度拷贝, 只有不到15行:

// Deeping Clone [ fast , complicated ]

function depclone(l){
  if(isArray(l)){
     // detect if is Array, clone it
     // map isNotPrimitive convert recursion
     return slice(l).map(citd(depclone,negate(isPrimitive)));

  }else if(!isPrimitive(l)){
    var res = {};

    // clone object ^ with copy prototype
    if(l.constructor.prototype !== Object.prototype){
      var _ = function(){};
      _.prototype = l.constructor.prototype;
      res = new _();
    }

    // copy properties
    each(l, function(val,key){
      this[key] = isPrimitive(val) ? val : depclone(val);
    },res);

    return res;
  }

  return l;
}

总体思路是使用深度递归的方式,检测数据是否需要拷贝.

如果不是需要被拷贝的类型,则可以直接return回去(例如最后一行 return l)

数组类型的元素,先使用 [].slice.call(arr) 的方式将其执行浅拷贝,然后使用map的方式进行深度递归

对象元素,首先,检测一下它是不是原生对象(与原始的Object原型链一致), 如果是,就不需要拷贝原型链,而如果不是,首先我们需要先将它的原型链拷贝过来. 然后再复制它的属性. 复制属性时进行递归.

*注意,函数(Function)不需要拷贝, 直接当作Primitive处理就可以了