深度复制和浅度复制 是当初初学 c 遇到的第一批问题,似乎使不少人困惑,而类 c 的 javascript 也同样存在这个问题.
第一版:
javascript 中引用类型(Object.prototype.toString.call(object))有 : Array 以及 Object , Date , RegExp ,Number, Function,Boolean .而可以修改自身的包括:
Array : 可修改自身单个元素
Object : 可修改自身单个属性
Date : 可修改自身日期,年份等
RegExp : 可修改 lastIndex
而对于基本类型的包装类型如:new Boolean() ,new Number() 虽然没有方法改变自身值,但是可能在上面附加数据,所以最好还是考虑下。
然后细心点进行深度复制:
function clone(o) { var ret = 0, isPlainObject, isArray; var constructor = o.constructor; // array or plain object if (((isArray = S.isArray(o)) || isPlainObject = S.isPlainObject(o))) { // 先把对象建立起来 if (isArray) { ret = []; } else if (isPlainObject) { ret = {}; } // clone it if (isArray) { for (var i = 0; i < o.length; i++) { ret[i] = S.clone(o[i]); } } else if (isPlainObject) { for (k in o) { if (o.hasOwnProperty(k)) { ret[k] = S.clone(o[k]); } } } } else if (typeof o=="object"&&S.inArray(constructor, [Boolean, String, Number, Date, RegExp])) { ret = new constructor(o.valueOf()); } return ret; }
第二版:
上一版虽然考虑了引用类型,但是对于一种特殊情况却会引起巨大的麻烦:循环引用时的无穷递归。例如以下数据类型:
var son={name:"x"},father:{name:"y"}; father.son=son; son.father=father; var newSon=S.clone(son);
虽然这种情况很少见,甚至不推荐。但是场景确实会存在,比如 dom 树节点就是个很好的例子.
解决:
首先要防止死循环,最常见的做法即是做标记,如果一个源已经被克隆过了,那么只需返回对应的克隆对象即可。
随后就要清除先前的标记了,又是一个问题:怎么清除?从头开始清除?那么真陷入了死循环。为了避免再次死循环就需要在第一步做标记时,把做标记的元素存起来,当最后克隆完毕,再将标记统一清除:
var CLONE_MARKER = '__cloned'; function clone(o) { var marked = {}, ret = cloneInternal(o, marked); S.each(marked, function(v) { // 清理在源对象上做的标记 v = v.o; if (v[CLONE_MARKER]) { try { delete v[CLONE_MARKER]; } catch (e) { S.log(e); v[CLONE_MARKER] = undefined; } } }); marked = undefined; return ret; } function cloneInternal(o, f, marked) { var ret = o, isArray, k, stamp; // 引用类型要先记录 if (o && ((isArray = S.isArray(o)) || S.isPlainObject(o) || S.isDate(o) || S.isRegExp(o) )) { if (o[CLONE_MARKER]) { // 对应的克隆后对象 return marked[o[CLONE_MARKER]].r; } // 做标记 o[CLONE_MARKER] = (stamp = S.guid()); // 先把对象建立起来 if (isArray) { ret = f ? S.filter(o, f) : o.concat(); } else if (S.isDate(o)) { ret = new Date(+o); } else if (S.isRegExp(o)) { ret = new RegExp(o); } else { ret = {}; } // 存储源对象以及克隆后的对象 marked[stamp] = {r:ret,o:o}; } // array or plain object need to be copied recursively if (o && (isArray || S.isPlainObject(o))) { // clone it if (isArray) { for (var i = 0; i < ret.length; i++) { ret[i] = cloneInternal(ret[i], f, marked); } } else { for (k in o) { if (k !== CLONE_MARKER && o.hasOwnProperty(k) && (!f || (f.call(o, o[k], k, o) !== false))) { ret[k] = cloneInternal(o[k], f, marked); } } } } return ret; }
可以找个复杂的例子验证下:
var t7 = [], t8 = {x:1,z:t7}, t9 = {y:1,z:t7}; t7.push(t8, t9);
画个图就是:
那么 clone=S.clone(t7) 的结果应该和 t7 内容一样并且包含关系完全相同即:
不足:
该算法只适用于配置参数等简单数据类型克隆,对于具备复杂原型链的自定义对象尚不能很好支持,或许可以通过
ret=new o.constructor()
来生成对应类型对象,但是由于执行了构造器或存在副作用.
Refer:
原来已经有规范了,不过如果出现 HTMLNode function 就报错的做法不妥?: