Skip to content

Commit

Permalink
Add deep clone support via the deep argument to _.clone.
Browse files Browse the repository at this point in the history
  • Loading branch information
jdalton committed Jul 24, 2012
1 parent 632ace7 commit 8aa8658
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 17 deletions.
15 changes: 7 additions & 8 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
'bind': [],
'bindAll': ['bind', 'functions'],
'chain': ['mixin'],
'clone': ['extend', 'isArray'],
'clone': ['extend', 'forOwn', 'isArguments'],
'compact': [],
'compose': [],
'contains': [],
Expand Down Expand Up @@ -774,17 +774,12 @@

// build replacement code
lodash.forOwn({
'Arguments': 'argsClass',
'Date': 'dateClass',
'Function': 'funcClass',
'Number': 'numberClass',
'RegExp': 'regexpClass',
'String': 'stringClass'
}, function(value, key) {
// skip `isArguments` if not a mobile build
if (!isMobile && key == 'Arguments') {
return;
}
var funcName = 'is' + key,
funcCode = matchFunction(source, funcName);

Expand Down Expand Up @@ -887,15 +882,19 @@
// remove IE `shift` and `splice` fix from mutator Array functions mixin
source = source.replace(/(?:\s*\/\/.*)*\n( +)if *\(value.length *=== *0[\s\S]+?\n\1}/, '');

// remove `noCharByIndex` from `_.reduceRight`
source = source.replace(/noCharByIndex *&&[^:]+: *([^;]+)/g, '$1');
// remove `noArgsClass` from `_.clone` and `_.size`
source = source.replace(/ *\|\| *\(noArgsClass *&[^)]+?\)\)/g, '');

// remove `noArraySliceOnStrings` from `_.toArray`
source = source.replace(/noArraySliceOnStrings *\?[^:]+: *([^)]+)/g, '$1');

// remove `noCharByIndex` from `_.reduceRight`
source = source.replace(/noCharByIndex *&&[^:]+: *([^;]+)/g, '$1');

source = removeVar(source, 'extendIteratorOptions');
source = removeVar(source, 'hasDontEnumBug');
source = removeVar(source, 'iteratorTemplate');
source = removeVar(source, 'noArgsClass');
source = removeVar(source, 'noArraySliceOnStrings');
source = removeVar(source, 'noCharByIndex');
source = removeIsArgumentsFallback(source);
Expand Down
7 changes: 7 additions & 0 deletions build/pre-compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@
// Closure Compiler errors trying to minify them
source = source.replace(/(arrayLikeClasses =)[\s\S]+?= *true/g, "$1{'[object Arguments]': true, '[object Array]': true, '[object String]': true }");

// manually convert `cloneableClasses` property assignments because
// Closure Compiler errors trying to minify them
source = source.replace(/(cloneableClasses =)[\s\S]+?= *true/g,
"$1{'[object Array]': true, '[object Boolean]': true, '[object Date]': true, " +
"'[object Number]': true, '[object Object]': true, '[object RegExp]': true, '[object String]': true }"
);

// add brackets to whitelisted properties so Closure Compiler won't mung them
// http://code.google.com/closure/compiler/docs/api-tutorial3.html#export
source = source.replace(RegExp('\\.(' + propWhitelist.join('|') + ')\\b', 'g'), "['$1']");
Expand Down
134 changes: 125 additions & 9 deletions lodash.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
reEmptyStringMiddle = /\b(__p \+=) '' \+/g,
reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g;

/** Used to match regexp flags from their coerced string values */
var reFlags = /\w*$/;

/** Used to insert the data object variable into compiled template source */
var reInsertVariable = /(?:__e|__t = )\(\s*(?![\d\s"']|this\.)/g;

Expand Down Expand Up @@ -110,6 +113,7 @@
dateClass = '[object Date]',
funcClass = '[object Function]',
numberClass = '[object Number]',
objectClass = '[object Object]',
regexpClass = '[object RegExp]',
stringClass = '[object String]';

Expand Down Expand Up @@ -159,6 +163,12 @@
var arrayLikeClasses = {};
arrayLikeClasses[argsClass] = arrayLikeClasses[arrayClass] = arrayLikeClasses[stringClass] = true;

/** Used to identify object classifications that `_.clone` supports */
var cloneableClasses = {};
cloneableClasses[arrayClass] = cloneableClasses[boolClass] = cloneableClasses[dateClass] =
cloneableClasses[numberClass] = cloneableClasses[objectClass] = cloneableClasses[regexpClass] =
cloneableClasses[stringClass] = true;

/**
* Used to escape characters for inclusion in HTML.
* The `>` and `/` characters don't require escaping in HTML and have no
Expand Down Expand Up @@ -2464,23 +2474,129 @@
/*--------------------------------------------------------------------------*/

/**
* Create a shallow clone of the `value`. Any nested objects or arrays will be
* assigned by reference and not cloned.
* Create a clone of `value`. If `deep` is `true`, all nested objects, excluding
* functons and `arguments` objects, will be cloned otherwise they will be
* assigned by reference.
*
* @static
* @memberOf _
* @category Objects
* @param {Mixed} value The value to clone.
* @param {Boolean} deep A flag to indicate a deep clone.
* @param {Object} [guard] Internally used to allow this method to work with
* others like `_.map` without using their callback `index` argument for `deep`.
* @param {Array} [stack=[]] Internally used to keep track of traversed objects
* to avoid circular references.
* @returns {Mixed} Returns the cloned `value`.
* @example
*
* var stooges = [
* { 'name': 'moe', 'age': 40 },
* { 'name': 'larry', 'age': 50 },
* { 'name': 'curly', 'age': 60 }
* ];
*
* _.clone({ 'name': 'moe' });
* // => { 'name': 'moe' };
* // => { 'name': 'moe' }
*
* var shallow = _.clone(stooges);
* shallow[0] === stooges[0];
* // => true
*
* var deep = _.clone(stooges, true);
* shallow[0] === stooges[0];
* // => false
*/
function clone(value) {
return value && objectTypes[typeof value]
? (isArray(value) ? value.slice() : extend({}, value))
: value;
function clone(value, deep, guard, stack) {
if (!value) {
return value;
}
var isObj = typeof value == 'object';
stack || (stack = []);

if (guard) {
deep = false;
}
// use custom `clone` method if available
if (value.clone && toString.call(value.clone) == funcClass) {
return value.clone(deep);
}
// inspect [[Class]]
if (isObj) {
var className = toString.call(value);

// don't clone `arguments` objects, functions, or non-object Objects
if (!cloneableClasses[className] || (noArgsClass && isArguments(value))) {
return value;
}

var ctor = value.constructor,
isArr = className == arrayClass,
useCtor = toString.call(ctor) == funcClass;

// IE < 9 presents nodes like `Object` objects:
// IE < 8 are missing the node's constructor property
// IE 8 node constructors are typeof "object"
// check if the constructor is `Object` as `Object instanceof Object` is `true`
if (className == objectClass &&
(isObj = useCtor && ctor instanceof ctor)) {
// An object's own properties are iterated before inherited properties.
// If the last iterated key belongs to an object's own property then
// there are no inherited enumerable properties.
forIn(value, function(objValue, objKey) { isObj = objKey; });
isObj = isObj == true || hasOwnProperty.call(value, isObj);
}
}
// shallow clone
if (!isObj || !deep) {
// don't clone functions
return isObj
? (isArr ? slice.call(value) : extend({}, value))
: value;
}

switch (className) {
case boolClass:
return new ctor(value == true);

case dateClass:
return new ctor(+value);

case numberClass:
case stringClass:
return new ctor(value);

case regexpClass:
return ctor(value.source, reFlags.exec(value));
}

// check for circular references and return corresponding clone
var length = stack.length;
while (length--) {
if (stack[length].value == value) {
return stack[length].clone;
}
}

// init cloned object
length = value.length;
var result = isArr ? ctor(length) : (useCtor ? new ctor : {});

// add current clone and original value to the stack of traversed objects
stack.push({ 'clone': result, 'value': value });

// recursively populate clone (susceptible to call stack limits)
if (isArr) {
var index = -1;
while (++index < length) {
result[index] = clone(value[index], deep, null, stack);
}
} else {
forOwn(value, function(objValue, key) {
result[key] = clone(objValue, deep, null, stack);
});
}
return result;
}

/**
Expand Down Expand Up @@ -2780,8 +2896,8 @@
* @category Objects
* @param {Mixed} a The value to compare.
* @param {Mixed} b The other value to compare.
* @param {Array} [stack] Internally used to keep track of "seen" objects to
* avoid circular references.
* @param {Array} [stack=[]] Internally used to keep track of traversed objects
* to avoid circular references.
* @returns {Boolean} Returns `true` if the values are equvalent, else `false`.
* @example
*
Expand Down
96 changes: 96 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,102 @@

/*--------------------------------------------------------------------------*/

QUnit.module('lodash.clone');

(function() {
function Klass() { }
Klass.prototype = { 'a': 1 };

var nonCloneable = {
'an arguments object': arguments,
'an element': window.document && document.body,
'a function': Klass,
'a Klass instance': new Klass
};

var objects = {
'an array': ['a', 'b', 'c', ''],
'an array-like-object': { '0': 'a', '1': 'b', '2': 'c', '3': '', 'length': 5 },
'boolean': false,
'boolean object': Object(false),
'an object': { 'a': 0, 'b': 1, 'c': 3 },
'an object with object values': { 'a': /a/, 'b': ['B'], 'c': { 'C': 1 } },
'null': null,
'a number': 3,
'a number object': Object(3),
'a regexp': /a/gim,
'a string': 'a',
'a string object': Object('a'),
'undefined': undefined
};

objects['an array'].length = 5;

_.forOwn(objects, function(object, key) {
test('should deep clone ' + key + ' correctly', function() {
var clone = _.clone(object, true);

if (object == null) {
equal(clone, object);
} else {
deepEqual(clone.valueOf(), object.valueOf());
}
if (_.isObject(object)) {
ok(clone !== object);
} else {
skipTest();
}
});
});

_.forOwn(nonCloneable, function(object, key) {
test('should not clone ' + key, function() {
ok(_.clone(object) === object);
ok(_.clone(object, true) === object);
});
});

test('should shallow clone when used as `callback` for `_.map`', function() {
var expected = [{ 'a': [0] }, { 'b': [1] }],
actual = _.map(expected, _.clone);

ok(actual != expected && actual.a == expected.a && actual.b == expected.b);
});

test('should deep clone objects with circular references', function() {
var object = {
'foo': { 'b': { 'foo': { 'c': { } } } },
'bar': { }
};

object.foo.b.foo.c.foo = object;
object.bar.b = object.foo.b;

var clone = _.clone(object, true);
ok(clone.bar.b === clone.foo.b && clone === clone.foo.b.foo.c.foo && clone !== object);
});

test('should clone using Klass#clone', function() {
var object = new Klass;
Klass.prototype.clone = function() { return new Klass; };

var clone = _.clone(object);
ok(clone !== object && clone instanceof Klass);

clone = _.clone(object, true);
ok(clone !== object && clone instanceof Klass);

delete Klass.prototype.clone;
});

test('should clone problem JScript properties (test in IE < 9)', function() {
deepEqual(_.clone(shadowed), shadowed);
deepEqual(_.clone(shadowed, true), shadowed);
});
}(1, 2, 3));

/*--------------------------------------------------------------------------*/

QUnit.module('lodash.contains');

(function() {
Expand Down

0 comments on commit 8aa8658

Please sign in to comment.