diff --git a/build.js b/build.js index 156fd14a3..0f8c7e419 100755 --- a/build.js +++ b/build.js @@ -150,7 +150,7 @@ 'bind': [], 'bindAll': ['bind', 'functions'], 'chain': ['mixin'], - 'clone': ['extend', 'isArray'], + 'clone': ['extend', 'forOwn', 'isArguments'], 'compact': [], 'compose': [], 'contains': [], @@ -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); @@ -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); diff --git a/build/pre-compile.js b/build/pre-compile.js index d488c0dfe..fe6ee0f96 100644 --- a/build/pre-compile.js +++ b/build/pre-compile.js @@ -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']"); diff --git a/lodash.js b/lodash.js index 104b2fa1a..9f05ed000 100644 --- a/lodash.js +++ b/lodash.js @@ -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; @@ -110,6 +113,7 @@ dateClass = '[object Date]', funcClass = '[object Function]', numberClass = '[object Number]', + objectClass = '[object Object]', regexpClass = '[object RegExp]', stringClass = '[object String]'; @@ -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 @@ -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; } /** @@ -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 * diff --git a/test/test.js b/test/test.js index 6a143ddd7..0d8d6b6d0 100644 --- a/test/test.js +++ b/test/test.js @@ -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() {