Comparing Custom Deep Extend to jQuery Deep Extend

JavaScript performance comparison

Revision 3 of this test case created by Josh Schell

Info

checking performance including circular referencing

Preparation code

<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js">
</script>
<script src="http://documentcloud.github.com/underscore/underscore.js">
</script>
<script src="https://gist.github.com/raw/1870656/506acfc78bb008943d9883503733a1fc66eab6d1/deepExtend.js">
</script>
<script>
  function getClass(o, extendNumericTypes, toType) {
    var oclass;
    toType = toType || typeof o;
    if (toType === 'undefined') return '[object Undefined]';
    if (o === null) return '[object Null]';
    if (o.toString() === 'NaN') return '[object NaN]';
    oclass = Object.prototype.toString.call(o);
    if (extendNumericTypes && oclass === '[object Number]') {
      if (o === Infinity) return '[object Infinity]';
      if (o % 1 === 0) return '[object Integer]';
      return '[object Float]';
    }
    return oclass;
  }


  function getType(o, extendNumericTypes) {
    return getClass(o, extendNumericTypes).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
  }


  function isType(o, type) {
    return getType(o) === type;
  }


  function isObject(o, plain) {
    var isObj = getType(o) === 'object';
    if (!plain || !isObj) return isObj;
    var hasOwn = {}.hasOwnProperty;
    var key;
    for (key in o) {}
    return key === undefined || hasOwn.call(o, key);
  }


  function isInteger(n) {
    return getType(n, true) === 'integer';
  }


  function isFunction(o) {
    return typeof o === 'function';
  }


  function isDate(o) {
    return getType(o) === 'date';
  }

  function isRegExp(o) {
    return getType(o) === 'regexp';
  }

  function isString(str) {
    return typeof str === 'string';
  }


  function isBoolean(o) {
    return getType(o) === 'boolean';
  }


  function isUndefined(o) {
    return typeof o === 'undefined';
  }


  function isArray(o) {
    if (Array.isArray) return Array.isArray(o);
    return getType(o) === 'array';
  }


  function extend(target, source, options) {
    if (typeof target === 'undefined') target = {};
    if (typeof source !== 'object') return target;

    var settings = {
      defaults: false,
      // if true, will only copy properties that do not exist in target
      preserveTarget: false,
      // if true, do not modify target object
      existingOnly: false,
      // if true (and defaults is false), only copy properties that exist in target
      preserveObjects: true // if true, will not overwrite objects with primitives, if 'error', attempted overwrite throws error
    },
        getPresErr = function(key, targetType, sourceType) {
        return 'protecting ' + key + ' property of type "' + targetType + '" from overwrite with type "' + sourceType + '"';
        },
        targetType, sourceType, o, keys;
    // options must be an object, otherwise ignore it
    settings = (typeof options === 'object') ? extend(settings, options) : settings;
    if (settings.preserveTarget) o = extend({}, target);
    else o = target;
    keys = (settings.existingOnly && !settings.defaults) ? o : source;
    for (var key in keys) {
      if (source[key] !== undefined) {
        targetType = typeof o[key];
        sourceType = typeof source[key];
        if (typeof o[key] === "object") { // property of object being extended is an object
          if (source[key] == o) // avoid infinite loop
          continue;
          else if (typeof source[key] === 'object') { // value to overwrite with is also an object
            if (targetType === sourceType || settings.preserveObjects === false) {
              // either the properties have the same basic object type, or we don't care
              o[key] = extend(o[key], source[key], options);
            } else if (settings.preserveObjects === 'error') {
              throw new Error(getPresErr(key, targetType, sourceType));
            }
          } else if (settings.preserveObjects === false) { // value to overwrite is not also an object, but we don't care
            o[key] = source[key];
          } else if (settings.preserveObjects === 'error') { // value to overwrite is not also an ojbect, and we want to know
            throw new Error(getPresErr(key, targetType, sourceType));
          }
        } else if (source[key] !== undefined && (!settings.defaults || o[key] === undefined)) o[key] = source[key];
      }
    }
    return o;
  }

  function extend2(target, source, options) {

    if (source === undefined) return target;
    if (target === undefined) target = {};
    if (typeof options === 'boolean') options = {
      deep: options
    };
    if (!options) options = {};
    if (typeof options !== 'object') throw new TypeError('invalid options argument');

    var key, sourceType, targetType, sourceValue, targetValue, typesMatch, settings = {
      deep: true,
      ownProperties: true,
      // whether to check for Object.hasOwnProperty when copying properties that are objects
      defaults: false,
      // if true, will only copy properties that do not exist in target
      nulls: false,
      // whether to copy null values from source
      emptyStrings: false,
      // whether to overwrite existing values with empty strings
      existingOnly: false,
      // if true (and defaults is false), only copy properties that exist in target
      preserveTypes: true,
      // if true, will not overwrite properties that are not of the same type
      // if function, if properties are not of same type they will be passed as arguments
      // the function must return true or false to indicate whether to overwrite target with source value
      // if set to the string 'error', the errorHandler option will be called (see below)
      errorHandler: function(key, targetProp, sourceProp) {
        throw new TypeError('cannot merge property ' + key + ', source type ' + getType(sourceProp) + ' does not match target type ' + getType(targetProp));
      }
    };

    for (key in options) {
      if (options.hasOwnProperty(key)) {
        settings[key] = options[key];
      }
    }
    var enumProperties = (settings.existingOnly && !settings.defaults) ? target : source;

    for (key in enumProperties) {

      if (!settings.ownProperties || enumProperties.hasOwnProperty(key) && ((settings.defaults && target[key] === undefined) || !settings.defaults) && source[key] !== undefined && source[key] !== target[key]) {

        targetValue = target[key];
        sourceValue = source[key];
        targetType = getType(targetValue);
        sourceType = getType(sourceValue);
        typesMatch = sourceType === targetType;
        if (!typesMatch && settings.preserveTypes && targetValue !== undefined && targetValue !== null) {

          if (settings.preserveTypes === 'error') {
            settings.errHandler(key, targetValue, sourceValue);
          } else if (typeof settings.preserveTypes === 'function' && settings.preserveTypes(key, targetValue, sourceValue)) {
            target[key] = source[key];
          }
        } else if (typesMatch && settings.deep && (sourceType === 'array' || sourceType === 'object')) {

          target[key] = extend2(target[key], source[key], settings);
        } else if ((sourceValue !== null || settings.nulls) && (sourceValue !== '' || !settings.emptyStrings || targetValue === undefined)) {

          target[key] = sourceValue;
        }

      }
    }
    return target;
  }

  function extend3(target, source, shallow) {
    var key, sourceType, targetType, keys = Object.keys(source);
    for (var i = 0, l = keys.length; i < l; i++) {
      key = keys[i];
      targetType = getType(target[key]);
      sourceType = getType(source[key]);
      if (source[key] !== target[key]) {
        if (!shallow && (sourceType === 'array' || sourceType === 'object') && (sourceType === targetType)) {
          target[key] = extend3(target[key], source[key], true);
        } else if (source[key] !== undefined) {
          target[key] = source[key];
        }
      }
    }
    return target;
  }

  function extend4(target, source, shallow) {
    var sourceType, targetType;
    for (var key in source) {
      if (source.hasOwnProperty(key)) {
        targetType = getType(target[key]);
        sourceType = getType(source[key]);
        if (source[key] !== target[key]) {
          if (!shallow && (sourceType === 'array' || sourceType === 'object') && (sourceType === targetType)) {
            target[key] = extend4(target[key], source[key], true);
          } else if (source[key] !== undefined) {
            target[key] = source[key];
          }
        }
      }
    }
    return target;
  }

  function extend5(target, source, shallow) {
    var array = '[object Array]',
        object = '[object Object]',
        targetMeta, sourceMeta, setMeta = function(value) {
        var jclass;
        if (value === undefined) return 0;
        if (typeof value !== 'object') return false;
        jClass = {}.toString.call(value);
        if (jclass === array) return 1;
        if (jclass === object) return 2;
        };
    for (var key in source) {
      if (source.hasOwnProperty(key)) {
        targetMeta = setMeta(target[key]), sourceMeta = setMeta(source[key])
        if (source[key] !== target[key]) {
          if (!shallow && sourceMeta && targetMeta && targetMeta === sourceMeta) {
            target[key] = extend5(target[key], source[key], true);
          } else if (sourceMeta !== 0) {
            target[key] = source[key];
          }
        }
      } else break; // ownProperties are always first
    }
    return target;
  }
</script>
<script>
Benchmark.prototype.setup = function() {
    var source = {
      name: "Jane Johnson",
      addresses: [{
        street: "34 Nameless.",
        city: "Houston",
        state: "TX"
      }, {
        street: "4567 Somewhere Rd.",
        city: "Denver",
        state: "CO"
      }],
      dob: new Date("1/12/1988")
    };
   
    var target = {
      name: "John Doe",
      kids: ["Will", "Amy", "Janie"],
      addresses: [{
        street: "1234 Anywhere St.",
        city: "Los Angeles",
        state: "CA"
      }, {
        street: "4567 Somewhere Rd.",
        city: "Denver",
        state: "CO"
      }, {
        street: "9876 Nowhere Ave.",
        city: "Atlanta",
        state: "GA"
      }],
      dob: new Date("1/12/1981")
    };
   
    source.associate = target;
};

Benchmark.prototype.teardown = function() {
    source = null;
    target = null;
};
</script>

Preparation code output

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in unknown unknown
Test Ops/sec
jQuery Deep Extend
$.extend(true, target, source);
$.extend(true, {}, source);
pending…
Custom Underscore Deep Extend
_.deepExtend(target, source);
_.deepExtend({}, source);
pending…
mashdraggin ntools DeepExtend variant1
extend(target, source);
extend({}, source);
pending…
mashdraggin ntools DeepExtend variant2
extend2(target, source);
extend2({}, source);
pending…
mashdraggin ntools DeepExtend variant3
extend3(target, source);
extend3({}, source);
pending…
mashdraggin ntools DeepExtend variant4
extend4(target, source);
extend4({}, source);
pending…
mashdraggin ntools DeepExtend variant5
extend5(target, source);
extend5({}, source);
pending…
Regular Underscore extend
_.extend(target, source);
_.extend({}, source);
pending…

Compare results of other browsers

Revisions

You can edit these tests or add even more tests to this page by appending /edit to the URL. Here’s a list of current revisions for this page:

0 comments

Add a comment