js的深复制不管是在开发中还是在找工作的笔试面试中,都是频繁出现的。使用jquery的人一定知道jquery有个extend方法,这个方法相当强大,不仅可以实现对jquery插件的扩展,也实现的对对象的扩展(包括了深复制)的功能,开发当中也是使用频率相当高的一个方法。本文将实现一个实用性比较高的深复制方法。
涉及到知识点
1、跨帧判断一个对象是否是数组
2、如何判断一个对象是纯粹对象
3、jquery extend方法是如何实现完美深复制
如何实现深复制
一般我们实现深复制就是利用递归的方式,判断要复制的对象的属性值是一个Object或者是Array的时候,我们需要利用递归进行复制。
function deepClone(target) {
var copy;
// 复制对象,根据被复制对象的类型初始化
if(target instanceof Array) {
copy = [];
} else if(target instanceof Object) {
copy = {};
} else {
return target;
}
for(var key in target) {
// 被复制的属性值
var value = target[key];
if(value instanceof Array || value instanceof Object) {
// 如果是Array/Object类型,则递归复制
copy[key] = deepClone(value)
} else {
copy[key] = target[key];
}
}
return copy;
}
这样就基本实现深复制了,大功告成了。
呵呵呵,少年,too young too simple。这段代码有几个问题。
1、判断对象是否是数组,在多frame的情况下不准。
2、用instanceof判断一个对象是否是Object不合理。我们都知道,instanceof会把DOM对象、BOM对象、Function、RegExp、Date、Array等全部都认为是Object(原型链的最顶部就是Object)。
3、无法对Date和RegExp对象进行复制。
解决方案
问题1
其实问题不是很大,我们很少会遇到跨frame操作对象,不过这里还是讲下如果做到兼容性的判断一个对象是否是数组
function isArray(arr) {
return Object.prototype.toString.call(arr) === "[object Array]"
}
为什么跨frame下判断一个对象是否是数组无效?其实浏览器中,所有的全局对象都是挂载在window对象下,而不同的frame是有不同的window的,因此,一个frame的全局对象,自然不会是另外一个frame的。
问题2
一般我们要复制的对象是对象直面量或者是new方式创建的对象,称为纯粹对象,需要去掉DOM和BOM对象的复制,而对于Date,RegExp一般不考虑,因此我们需要有一种更准确的判断一个对象是否是普通对象,参照jquery的isPlainObject,它可以区分出纯粹对象。
function isPlaginObject = function(obj) {
// 排除不是对象类型/DOM对象/window对象
if(typeof obj !== 'object' || obj.nodeType || obj.self = obj) {
return false;
}
// 如果判断为对象,但对象的__proto__没有isPrototype方法,则说明不是通过new方式创建或者对象字面量,同时RegExp和Date对象也被区分开来
if(obj.constructor && !Object.prototype.hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) {
return false;
}
return true;
}
问题3
instanceof判断Date和RegExp都会是Object类型,所以上面的deepClone复制的时候用走for in遍历复制这两种类型的对象的属性,但是这个时候是没有可遍历的属性的,所以结果会是空对象,明显不是我们要的结果。
针对RegExp对象,我们可以通过新建一个新的RegExp对象,传递source和flags
if(target[key] instanceof RegExp) {
copy[key] = new RegExp(target[key].source, target[key].flags)
}
针对Date对象,同样可以通过新建一个Date对象进行拷贝。
if(target[key] instanceof Date) {
copy[key] = new Date(+target[key]); //+号会将date转为时间戳,原理就是+操作符会调用Date对象的valueOf方法
}
完整代码
/**
** 判断一个对象是否是数组-兼容frame版
**
**/
function isArray(arr) {
return Object.prototype.toString.call(arr) === "[object Array]";
}
/**
** 判断一个对象是否是纯粹对象
**
**/
function isPlainObject(obj) {
// 排除简单类型/DOM对象/window对象
if(typeof obj !== "object" || obj.nodeType || obj.self === self) {
return false;
}
// 排除不是new操作符新建的对象以及RegExp和Array
if(obj.constructor && !Object.prototype.hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) {
return false;
}
return true;
}
/**
** 复制对象
** @target 被复制对象
**
**/
function deepCopy(target) {
var copy = null;
// 目标对象不是纯粹对象或者数组,则不复制
if(!isPlainObject(target) && !isArray(target)) {
return null;
} else if(isPlainObject(target)){
copy = {};
} else {
copy = [];
}
if(isPlainObject(target) || isArray(target)) {
for(var key in target) {
if(target === target[key]) continue // 防止循环复制
// 不复制原形链里面的属性
if(target.hasOwnProperty(key)) {
if(isPlainObject(target[key])) {
// 普通对象,通过递归调用的方式复制
copy[key] = deepCopy(target[key]);
} else if(isArray(target[key])) {
// 数组对象,通过递归调用的方式复制
copy[key] = deepCopy(target[key]);
} else if(target[key] instanceof RegExp) {
copy[key] = new RegExp(target[key].source, target[key].flags);
} else if(target[key] instanceof Date) {
copy[key] = new Date(+target[key]);
}else {
copy[key] = target[key];
}
}
}
} else {
return copy;
}
return copy;
}
git地址: github.com/VikiLee/Uti…