diff --git a/build.js b/build.js index c34e840ac..de0312d2d 100755 --- a/build.js +++ b/build.js @@ -881,14 +881,17 @@ }); // remove JScript [[DontEnum]] fix from `_.isEqual` - source = source.replace(/(?:\s*\/\/.*)*\n( +)if *\(result *&& *hasDontEnumBug[\s\S]+?\n\1}/, ''); + source = source.replace(/(?:\s*\/\/.*)*\n( +)if *\(hasDontEnumBug[\s\S]+?\n\1}/, ''); // 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 `noArgsClass` from `_.clone` and `_.size` + // remove `noArgsClass` from `_.clone`, `_.isEqual`, and `_.size` source = source.replace(/ *\|\| *\(noArgsClass *&[^)]+?\)\)/g, ''); + // remove `noArgsClass` from `_.isEqual` + source = source.replace(/if *\(noArgsClass[^}]+?}\n/, ''); + // remove `noArraySliceOnStrings` from `_.toArray` source = source.replace(/noArraySliceOnStrings *\?[^:]+: *([^)]+)/g, '$1'); diff --git a/build/pre-compile.js b/build/pre-compile.js index 724563a2d..c35d3b892 100644 --- a/build/pre-compile.js +++ b/build/pre-compile.js @@ -218,13 +218,18 @@ // manually convert `arrayLikeClasses` property assignments because // 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 }"); + source = source.replace(/(arrayLikeClasses =)[\s\S]+?= *true/g, + "$1{'[object Arguments]': true, '[object Array]': true, '[object Boolean]': false, " + + "'[object Date]': false, '[object Function]': false, '[object Number]': false, " + + "'[object Object]': false, '[object RegExp]': false, '[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 }" + "$1{'[object Arguments]': false, '[object Array]': true, '[object Boolean]': true, " + + "'[object Date]': true, '[object Function]': false, '[object Number]': true, " + + "'[object Object]': true, '[object RegExp]': true, '[object String]': true }" ); // add brackets to whitelisted properties so Closure Compiler won't mung them diff --git a/lodash.js b/lodash.js index a5c684c76..a7ddd5577 100644 --- a/lodash.js +++ b/lodash.js @@ -39,7 +39,10 @@ /** Native prototype shortcuts */ var ArrayProto = Array.prototype, - ObjectProto = Object.prototype; + BoolProto = Boolean.prototype, + ObjectProto = Object.prototype, + NumberProto = Number.prototype, + StringProto = String.prototype; /** Used to generate unique IDs */ var idCounter = 0; @@ -141,6 +144,9 @@ */ var noCharByIndex = ('x'[0] + Object('x')[0]) != 'xx'; + /** Detect if a node's [[Class]] is unresolvable (IE < 9) */ + var noNodeClass = toString.call(window.document || {}) == objectClass; + /* Detect if `Function#bind` exists and is inferred to be fast (all but V8) */ var isBindFast = nativeBind && /\n|Opera/.test(nativeBind + toString.call(window.opera)); @@ -161,10 +167,13 @@ /** Used to identify object classifications that are array-like */ var arrayLikeClasses = {}; + arrayLikeClasses[boolClass] = arrayLikeClasses[dateClass] = arrayLikeClasses[funcClass] = + arrayLikeClasses[numberClass] = arrayLikeClasses[objectClass] = arrayLikeClasses[regexpClass] = false; arrayLikeClasses[argsClass] = arrayLikeClasses[arrayClass] = arrayLikeClasses[stringClass] = true; /** Used to identify object classifications that `_.clone` supports */ var cloneableClasses = {}; + cloneableClasses[argsClass] = cloneableClasses[funcClass] = false; cloneableClasses[arrayClass] = cloneableClasses[boolClass] = cloneableClasses[dateClass] = cloneableClasses[numberClass] = cloneableClasses[objectClass] = cloneableClasses[regexpClass] = cloneableClasses[stringClass] = true; @@ -189,7 +198,8 @@ 'object': true, 'number': false, 'string': false, - 'undefined': false + 'undefined': false, + 'unknown': true }; /** Used to escape characters for inclusion in compiled string literals */ @@ -2494,6 +2504,8 @@ * 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. + * @param {Boolean} thorough Internally used to indicate whether or not to perform + * a more thorough clone of non-object values. * @returns {Mixed} Returns the cloned `value`. * @example * @@ -2514,8 +2526,8 @@ * shallow[0] === stooges[0]; * // => false */ - function clone(value, deep, guard, stack) { - if (!value) { + function clone(value, deep, guard, stack, thorough) { + if (value == null) { return value; } var isObj = typeof value == 'object'; @@ -2524,34 +2536,44 @@ if (guard) { deep = false; } + // avoid slower checks on non-objects + if (thorough == null) { + // primitives passed from iframes use the primary document's native prototypes + thorough = !!(BoolProto.clone || NumberProto.clone || StringProto.clone); + } // use custom `clone` method if available - if (value.clone && toString.call(value.clone) == funcClass) { + if ((isObj || thorough) && 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 + var className = toString.call(value); if (!cloneableClasses[className] || (noArgsClass && isArguments(value))) { return value; } - var ctor = value.constructor, - isArr = className == arrayClass, - useCtor = toString.call(ctor) == funcClass; + var useCtor, + ctor = value.constructor, + isArr = className == arrayClass; - // 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); + if (className == objectClass) { + // IE < 9 presents DOM nodes as `Object` objects except they have `toString` + // methods that are `typeof` "string" and still can coerce nodes to strings + isObj = !noNodeClass || !(typeof value.toString != 'function' && typeof (value + '') == 'string'); + + if (isObj) { + // check that the constructor is `Object` because `Object instanceof Object` is `true` + useCtor = toString.call(ctor) == funcClass; + isObj = !useCtor || ctor instanceof ctor; + } + if (isObj) { + // 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 @@ -2596,11 +2618,11 @@ if (isArr) { var index = -1; while (++index < length) { - result[index] = clone(value[index], deep, null, stack); + result[index] = clone(value[index], deep, null, stack, thorough); } } else { forOwn(value, function(objValue, key) { - result[key] = clone(objValue, deep, null, stack); + result[key] = clone(objValue, deep, null, stack, thorough); }); } return result; @@ -2905,6 +2927,8 @@ * @param {Mixed} b The other value to compare. * @param {Array} [stack=[]] Internally used to keep track of traversed objects * to avoid circular references. + * @param {Boolean} thorough Internally used to indicate whether or not to perform + * a more thorough comparison of non-object values. * @returns {Boolean} Returns `true` if the values are equvalent, else `false`. * @example * @@ -2917,26 +2941,33 @@ * _.isEqual(moe, clone); * // => true */ - function isEqual(a, b, stack) { + function isEqual(a, b, stack, thorough) { stack || (stack = []); // a strict comparison is necessary because `null == undefined` if (a == null || b == null) { return a === b; } - // unwrap any LoDash wrapped values - if (a._chain) { - a = a._wrapped; + // avoid slower checks on non-objects + if (thorough == null) { + // primitives passed from iframes use the primary document's native prototypes + thorough = !!(BoolProto.isEqual || NumberProto.isEqual || StringProto.isEqual); } - if (b._chain) { - b = b._wrapped; - } - // use custom `isEqual` method if available - if (a.isEqual && toString.call(a.isEqual) == funcClass) { - return a.isEqual(b); - } - if (b.isEqual && toString.call(b.isEqual) == funcClass) { - return b.isEqual(a); + if (objectTypes[typeof a] || objectTypes[typeof b] || thorough) { + // unwrap any LoDash wrapped values + if (a._chain) { + a = a._wrapped; + } + if (b._chain) { + b = b._wrapped; + } + // use custom `isEqual` method if available + if (a.isEqual && toString.call(a.isEqual) == funcClass) { + return a.isEqual(b); + } + if (b.isEqual && toString.call(b.isEqual) == funcClass) { + return b.isEqual(a); + } } // exit early for identical values if (a === b) { @@ -2968,10 +2999,18 @@ // treat string primitives and their corresponding object instances as equal return a == b + ''; } - if (typeof a != 'object' || typeof b != 'object') { - // for unequal function values + // exit early, in older browsers, if `a` is array-like but not `b` + var isArr = arrayLikeClasses[className]; + if (noArgsClass && !isArr && (isArr = isArguments(a)) && !isArguments(b)) { return false; } + // exit for functions and DOM nodes + if (!isArr && (className != objectClass || (noNodeClass && ( + (typeof a.toString != 'function' && typeof (a + '') == 'string') || + (typeof b.toString != 'function' && typeof (b + '') == 'string'))))) { + return false; + } + // assume cyclic structures are equal // the algorithm for detecting cyclic structures is adapted from ES 5.1 // section 15.12.3, abstract operation `JO` (http://es5.github.com/#x15.12.3) @@ -2990,7 +3029,7 @@ stack.push(a); // recursively compare objects and arrays (susceptible to call stack limits) - if (arrayLikeClasses[className] || (noArgsClass && isArguments(a))) { + if (isArr) { // compare lengths to determine if a deep comparison is necessary size = a.length; result = size == b.length; @@ -2998,54 +3037,56 @@ if (result) { // deep compare the contents, ignoring non-numeric properties while (size--) { - if (!(result = isEqual(a[size], b[size], stack))) { + if (!(result = isEqual(a[size], b[size], stack, thorough))) { break; } } } + return result; } - else { - // objects with different constructors are not equal - if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) { + + var ctorA = a.constructor, + ctorB = b.constructor; + + // non `Object` object instances with different constructors are not equal + if (ctorA != ctorB && !( + toString.call(ctorA) == funcClass && ctorA instanceof ctorA && + toString.call(ctorB) == funcClass && ctorB instanceof ctorB + )) { + return false; + } + // deep compare objects + for (var prop in a) { + if (hasOwnProperty.call(a, prop)) { + // count the number of properties. + size++; + // deep compare each property value. + if (!(hasOwnProperty.call(b, prop) && isEqual(a[prop], b[prop], stack, thorough))) { + return false; + } + } + } + // ensure both objects have the same number of properties + for (prop in b) { + // The JS engine in Adobe products, like InDesign, has a bug that causes + // `!size--` to throw an error so it must be wrapped in parentheses. + // https://github.com/documentcloud/underscore/issues/355 + if (hasOwnProperty.call(b, prop) && !(size--)) { + // `size` will be `-1` if `b` has more properties than `a` return false; } - // deep compare objects - for (var prop in a) { - if (hasOwnProperty.call(a, prop)) { - // count the number of properties. - size++; - // deep compare each property value. - if (!(result = hasOwnProperty.call(b, prop) && isEqual(a[prop], b[prop], stack))) { - break; - } - } - } - // ensure both objects have the same number of properties - if (result) { - for (prop in b) { - // The JS engine in Adobe products, like InDesign, has a bug that causes - // `!size--` to throw an error so it must be wrapped in parentheses. - // https://github.com/documentcloud/underscore/issues/355 - if (hasOwnProperty.call(b, prop) && !(size--)) { - break; - } - } - // `size` will be `-1` if `b` has more properties than `a` - result = !size; - } - // handle JScript [[DontEnum]] bug - if (result && hasDontEnumBug) { - while (++index < 7) { - prop = shadowed[index]; - if (hasOwnProperty.call(a, prop)) { - if (!(result = hasOwnProperty.call(b, prop) && isEqual(a[prop], b[prop], stack))) { - break; - } - } + } + // handle JScript [[DontEnum]] bug + if (hasDontEnumBug) { + while (++index < 7) { + prop = shadowed[index]; + if (hasOwnProperty.call(a, prop) && + !(hasOwnProperty.call(b, prop) && isEqual(a[prop], b[prop], stack, thorough))) { + return false; } } } - return result; + return true; } /** diff --git a/perf/perf.js b/perf/perf.js index 1001d5e31..b30f776e8 100644 --- a/perf/perf.js +++ b/perf/perf.js @@ -132,7 +132,10 @@ // potentially expensive for (index = 0; index < this.count; index++) { - bindAllObjects[index] = belt.clone(lodash); + bindAllObjects[index] = belt.reduce(funcNames, function(object, funcName) { + object[funcName] = lodash[funcName]; + return object; + }, {}); } } @@ -515,6 +518,18 @@ /*--------------------------------------------------------------------------*/ + suites.push( + Benchmark.Suite('`_.clone` with an object') + .add('Lo-Dash', function() { + lodash.clone(object); + }) + .add('Underscore', function() { + _.clone(object); + }) + ); + + /*--------------------------------------------------------------------------*/ + suites.push( Benchmark.Suite('`_.countBy` with `callback` iterating an array') .add('Lo-Dash', function() { @@ -1145,16 +1160,6 @@ /*--------------------------------------------------------------------------*/ - suites.push( - Benchmark.Suite('`_.size` with an array') - .add('Lo-Dash', function() { - lodash.size(numbers); - }) - .add('Underscore', function() { - _.size(numbers); - }) - ); - suites.push( Benchmark.Suite('`_.size` with an object') .add('Lo-Dash', function() { diff --git a/test/test.js b/test/test.js index 249156dff..5e78a2655 100644 --- a/test/test.js +++ b/test/test.js @@ -710,9 +710,35 @@ equal(_.isEqual(object, object), false); }); + test('should use custom `isEqual` method on primitives', function() { + Boolean.prototype.isEqual = function() { return true; }; + equal(_.isEqual(true, false), true); + delete Boolean.prototype.isEqual; + }); + test('fixes the JScript [[DontEnum]] bug (test in IE < 9)', function() { equal(_.isEqual(shadowed, {}), false); }); + + test('should return `true` for like-objects from different documents', function() { + if (window.document) { + var body = document.body, + iframe = document.createElement('iframe'), + object = { 'a': 1, 'b': 2, 'c': 3 }; + + body.appendChild(iframe); + var idoc = (idoc = iframe.contentDocument || iframe.contentWindow).document || idoc; + idoc.write("