朋友,jquery的extend了解吗?深复制了解下

372 阅读4分钟

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…