声明:本方法几乎考虑到了所有类型的对象,比如函数、日期、正则、Set、Map、DOM元素等,如有不足敬请指出。
首先写好一个包含各种类型数据的对象,如下:
var d = Symbol();
var e = Symbol();
var ss = { a: 1 };
var obj = {
a: 1,
b: 2,
c: [1, 2, 3],
zz: new Set([1, 2, ss]),
yy: new Map(),
[d]: "aaa",
z: document.createElement("div"),
d: {
e: new Date(),
f: /a/g,
g: function (s) {
console.log(s);
},
h: {},
},
};
Object.defineProperties(obj.d.h, {
i: {
value: 10,
},
j: {
configurable: true,
writable: true,
value: [1, 2, 3, 4],
},
k: {
writable: true,
value: {
l: {},
m: "abcde",
n: true,
o: [1, 2, 3],
},
},
[e]: {
value: ["a", "b", "c", "e"],
},
});
Object.defineProperties(obj.d.h.k.l, {
p: {
value: function () {
console.log("p");
},
},
q: {
value: {
r: { a: 1 },
j: { b: 2 },
},
},
});
var a_1 = { a: 1 };
var a_2 = { b: 2 };
obj.yy.set("name", "xili");
obj.yy.set(a_1, a_2);
接下来是将对象进行深复制的方法cloneObject,参数中的source指的是源对象(被复制的对象),target指的是目标对象(复制后得到的新对象)。
function cloneObject(source, target) {
if (source === null || source === undefined) return source;
if (source === document) return;
// 如果target不是引用类型,可能是undefined、null、bool、number、string或者为空
if (!Object.prototype.isPrototypeOf(target)) {
// 根据源对象创建一个相同类型的对象赋值给target
if (HTMLElement.prototype.isPrototypeOf(source)) {
// 如果源对象是DOM元素
// 就根据源对象的节点名创建一个新的DOM元素赋值给target
target = document.createElement(source.nodeName);
} else if (source.constructor === RegExp) {
// 如果源对象是正则表达式
// 则将源对象的source属性和flags属性作为参数通过构造函数创建新的正则表达式并赋值给target
target = new RegExp(source.source, source.flags); // 任何正则表达式都有source和flags属性,source是正则内容,flags是正则修饰符,而这两个属性都是只读属性,不能写入,必须通过构造函数创建时带入
} else if (source.constructor === Date) {
// 如果源对象是日期对象
// 将原有的日期对象放入新创建的日期对象,可以让当前日期对象变为原有日期对象的值,但是没有引用关系
target = new Date(source);
} else if (source.constructor === Function) {
// 如果源对象是函数
// 将源函数的参数和函数体内容提取到数组中,然后通过new Function()创建新的函数
var arr = source
.toString()
.replace(/\n|\r/g, "")
.trim()
.match(/\((.*?)\)\s*\{(.*)\}/)
.slice(1);
target = new Function(arr[0].trim(), arr[1]);
} else if (source.constructor === Set) {
// 如果源对象是Set类型
// 因为new Set时可以传入数组,所以我们将原有的Set列表强转为数组,并且将这个强转后的数组深复制以后代入到新的Set中
target = new Set(cloneObject(Array.from(source.values())));
} else if (source.constructor === Map) {
// 如果源对象是Map类型
// 先创建一个新的Map赋值给target
target = new Map();
// 再遍历原来的Map
for (var [key, value] of source.entries()) {
// 如果key是引用类型
if (Object.prototype.isPrototypeOf(key)) {
// 如果value是引用类型
if (Object.prototype.isPrototypeOf(value)) {
// 则将key和value分别做深复制,并将返回的结果放在target中
target.set(cloneObject(key), cloneObject(value));
} else {
// 如果value不是引用类型,则只将key深复制,并且将返回的结果放在target中
target.set(cloneObject(key), value);
}
} else {
if (Object.prototype.isPrototypeOf(value)) {
target.set(key, cloneObject(value));
} else {
target.set(key, value);
}
}
}
} else {
// 否则源对象就只是单纯的对象
// 则直接通过对象类型的反射创建新的同类型对象赋值给target
target = new source.constructor();
}
}
// 获取源对象的所有字符属性名和Symbol属性名,并保存到一个数组中
var names = Object.getOwnPropertyNames(source).concat(
Object.getOwnPropertySymbols(source)
);
// 遍历源对象的所有属性名
for (var i = 0; i < names.length; i++) {
// 如果当前复制的是函数,并且遍历到的当前属性名是prototype,则这个属性不复制,否则会死循环
if (source.constructor === Function && names[i] === "prototype")
continue;
// 获取当前属性名的描述对象
var desc = Object.getOwnPropertyDescriptor(source, names[i]);
// 如果当前描述对象的值是引用类型,则将源对象的描述内容设置给当前目标对象相同属性名的描述内容,并且将源对象的值进行深复制后设置给当前目标对象的值
if (Object.prototype.isPrototypeOf(desc.value)) {
Object.defineProperty(target, names[i], {
configurable: desc.configurable,
enumerable: desc.enumerable,
writable: desc.writable,
value: cloneObject(desc.value),
});
} else {
// 否则直接将描述对象设置给目标对象的这个属性
Object.defineProperty(target, names[i], desc);
}
}
return target;
}
下面简单证明一下目标对象和源对象完全没有引用关系(包括里面所包含的所有引用类型数据)。
var o = cloneObject(obj);
a_1.a = 100;
obj.d.h.j[1] = 1000;
console.log(o, obj);
大家可以实际打印一下,看看打印结果是否相同。