Ensure _.isEqual works correctly for objects from another document and add _.clone benchmark.

Former-commit-id: b1ef745ec6c24e8ea0c8fae304ead80c60dfd5aa
This commit is contained in:
John-David Dalton
2012-07-29 00:58:23 -07:00
parent 943004844a
commit 86bd847bf9
5 changed files with 173 additions and 93 deletions

View File

@@ -881,14 +881,17 @@
}); });
// remove JScript [[DontEnum]] fix from `_.isEqual` // 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 // remove IE `shift` and `splice` fix from mutator Array functions mixin
source = source.replace(/(?:\s*\/\/.*)*\n( +)if *\(value.length *=== *0[\s\S]+?\n\1}/, ''); 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, ''); source = source.replace(/ *\|\| *\(noArgsClass *&[^)]+?\)\)/g, '');
// remove `noArgsClass` from `_.isEqual`
source = source.replace(/if *\(noArgsClass[^}]+?}\n/, '');
// remove `noArraySliceOnStrings` from `_.toArray` // remove `noArraySliceOnStrings` from `_.toArray`
source = source.replace(/noArraySliceOnStrings *\?[^:]+: *([^)]+)/g, '$1'); source = source.replace(/noArraySliceOnStrings *\?[^:]+: *([^)]+)/g, '$1');

View File

@@ -218,13 +218,18 @@
// manually convert `arrayLikeClasses` property assignments because // manually convert `arrayLikeClasses` property assignments because
// Closure Compiler errors trying to minify them // 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 // manually convert `cloneableClasses` property assignments because
// Closure Compiler errors trying to minify them // Closure Compiler errors trying to minify them
source = source.replace(/(cloneableClasses =)[\s\S]+?= *true/g, source = source.replace(/(cloneableClasses =)[\s\S]+?= *true/g,
"$1{'[object Array]': true, '[object Boolean]': true, '[object Date]': true, " + "$1{'[object Arguments]': false, '[object Array]': true, '[object Boolean]': true, " +
"'[object Number]': true, '[object Object]': true, '[object RegExp]': true, '[object String]': 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 // add brackets to whitelisted properties so Closure Compiler won't mung them

195
lodash.js
View File

@@ -39,7 +39,10 @@
/** Native prototype shortcuts */ /** Native prototype shortcuts */
var ArrayProto = Array.prototype, var ArrayProto = Array.prototype,
ObjectProto = Object.prototype; BoolProto = Boolean.prototype,
ObjectProto = Object.prototype,
NumberProto = Number.prototype,
StringProto = String.prototype;
/** Used to generate unique IDs */ /** Used to generate unique IDs */
var idCounter = 0; var idCounter = 0;
@@ -141,6 +144,9 @@
*/ */
var noCharByIndex = ('x'[0] + Object('x')[0]) != 'xx'; 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) */ /* 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)); var isBindFast = nativeBind && /\n|Opera/.test(nativeBind + toString.call(window.opera));
@@ -161,10 +167,13 @@
/** Used to identify object classifications that are array-like */ /** Used to identify object classifications that are array-like */
var arrayLikeClasses = {}; var arrayLikeClasses = {};
arrayLikeClasses[boolClass] = arrayLikeClasses[dateClass] = arrayLikeClasses[funcClass] =
arrayLikeClasses[numberClass] = arrayLikeClasses[objectClass] = arrayLikeClasses[regexpClass] = false;
arrayLikeClasses[argsClass] = arrayLikeClasses[arrayClass] = arrayLikeClasses[stringClass] = true; arrayLikeClasses[argsClass] = arrayLikeClasses[arrayClass] = arrayLikeClasses[stringClass] = true;
/** Used to identify object classifications that `_.clone` supports */ /** Used to identify object classifications that `_.clone` supports */
var cloneableClasses = {}; var cloneableClasses = {};
cloneableClasses[argsClass] = cloneableClasses[funcClass] = false;
cloneableClasses[arrayClass] = cloneableClasses[boolClass] = cloneableClasses[dateClass] = cloneableClasses[arrayClass] = cloneableClasses[boolClass] = cloneableClasses[dateClass] =
cloneableClasses[numberClass] = cloneableClasses[objectClass] = cloneableClasses[regexpClass] = cloneableClasses[numberClass] = cloneableClasses[objectClass] = cloneableClasses[regexpClass] =
cloneableClasses[stringClass] = true; cloneableClasses[stringClass] = true;
@@ -189,7 +198,8 @@
'object': true, 'object': true,
'number': false, 'number': false,
'string': false, 'string': false,
'undefined': false 'undefined': false,
'unknown': true
}; };
/** Used to escape characters for inclusion in compiled string literals */ /** 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`. * others like `_.map` without using their callback `index` argument for `deep`.
* @param {Array} [stack=[]] Internally used to keep track of traversed objects * @param {Array} [stack=[]] Internally used to keep track of traversed objects
* to avoid circular references. * 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`. * @returns {Mixed} Returns the cloned `value`.
* @example * @example
* *
@@ -2514,8 +2526,8 @@
* shallow[0] === stooges[0]; * shallow[0] === stooges[0];
* // => false * // => false
*/ */
function clone(value, deep, guard, stack) { function clone(value, deep, guard, stack, thorough) {
if (!value) { if (value == null) {
return value; return value;
} }
var isObj = typeof value == 'object'; var isObj = typeof value == 'object';
@@ -2524,34 +2536,44 @@
if (guard) { if (guard) {
deep = false; 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 // 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); return value.clone(deep);
} }
// inspect [[Class]] // inspect [[Class]]
if (isObj) { if (isObj) {
var className = toString.call(value);
// don't clone `arguments` objects, functions, or non-object Objects // don't clone `arguments` objects, functions, or non-object Objects
var className = toString.call(value);
if (!cloneableClasses[className] || (noArgsClass && isArguments(value))) { if (!cloneableClasses[className] || (noArgsClass && isArguments(value))) {
return value; return value;
} }
var ctor = value.constructor, var useCtor,
isArr = className == arrayClass, ctor = value.constructor,
useCtor = toString.call(ctor) == funcClass; isArr = className == arrayClass;
// IE < 9 presents nodes like `Object` objects: if (className == objectClass) {
// IE < 8 are missing the node's constructor property // IE < 9 presents DOM nodes as `Object` objects except they have `toString`
// IE 8 node constructors are typeof "object" // methods that are `typeof` "string" and still can coerce nodes to strings
// check if the constructor is `Object` as `Object instanceof Object` is `true` isObj = !noNodeClass || !(typeof value.toString != 'function' && typeof (value + '') == 'string');
if (className == objectClass &&
(isObj = useCtor && ctor instanceof ctor)) { if (isObj) {
// An object's own properties are iterated before inherited properties. // check that the constructor is `Object` because `Object instanceof Object` is `true`
// If the last iterated key belongs to an object's own property then useCtor = toString.call(ctor) == funcClass;
// there are no inherited enumerable properties. isObj = !useCtor || ctor instanceof ctor;
forIn(value, function(objValue, objKey) { isObj = objKey; }); }
isObj = isObj == true || hasOwnProperty.call(value, isObj); 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 // shallow clone
@@ -2596,11 +2618,11 @@
if (isArr) { if (isArr) {
var index = -1; var index = -1;
while (++index < length) { while (++index < length) {
result[index] = clone(value[index], deep, null, stack); result[index] = clone(value[index], deep, null, stack, thorough);
} }
} else { } else {
forOwn(value, function(objValue, key) { forOwn(value, function(objValue, key) {
result[key] = clone(objValue, deep, null, stack); result[key] = clone(objValue, deep, null, stack, thorough);
}); });
} }
return result; return result;
@@ -2905,6 +2927,8 @@
* @param {Mixed} b The other value to compare. * @param {Mixed} b The other value to compare.
* @param {Array} [stack=[]] Internally used to keep track of traversed objects * @param {Array} [stack=[]] Internally used to keep track of traversed objects
* to avoid circular references. * 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`. * @returns {Boolean} Returns `true` if the values are equvalent, else `false`.
* @example * @example
* *
@@ -2917,26 +2941,33 @@
* _.isEqual(moe, clone); * _.isEqual(moe, clone);
* // => true * // => true
*/ */
function isEqual(a, b, stack) { function isEqual(a, b, stack, thorough) {
stack || (stack = []); stack || (stack = []);
// a strict comparison is necessary because `null == undefined` // a strict comparison is necessary because `null == undefined`
if (a == null || b == null) { if (a == null || b == null) {
return a === b; return a === b;
} }
// unwrap any LoDash wrapped values // avoid slower checks on non-objects
if (a._chain) { if (thorough == null) {
a = a._wrapped; // primitives passed from iframes use the primary document's native prototypes
thorough = !!(BoolProto.isEqual || NumberProto.isEqual || StringProto.isEqual);
} }
if (b._chain) { if (objectTypes[typeof a] || objectTypes[typeof b] || thorough) {
b = b._wrapped; // unwrap any LoDash wrapped values
} if (a._chain) {
// use custom `isEqual` method if available a = a._wrapped;
if (a.isEqual && toString.call(a.isEqual) == funcClass) { }
return a.isEqual(b); if (b._chain) {
} b = b._wrapped;
if (b.isEqual && toString.call(b.isEqual) == funcClass) { }
return b.isEqual(a); // 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 // exit early for identical values
if (a === b) { if (a === b) {
@@ -2968,10 +2999,18 @@
// treat string primitives and their corresponding object instances as equal // treat string primitives and their corresponding object instances as equal
return a == b + ''; return a == b + '';
} }
if (typeof a != 'object' || typeof b != 'object') { // exit early, in older browsers, if `a` is array-like but not `b`
// for unequal function values var isArr = arrayLikeClasses[className];
if (noArgsClass && !isArr && (isArr = isArguments(a)) && !isArguments(b)) {
return false; 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 // assume cyclic structures are equal
// the algorithm for detecting cyclic structures is adapted from ES 5.1 // 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) // section 15.12.3, abstract operation `JO` (http://es5.github.com/#x15.12.3)
@@ -2990,7 +3029,7 @@
stack.push(a); stack.push(a);
// recursively compare objects and arrays (susceptible to call stack limits) // 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 // compare lengths to determine if a deep comparison is necessary
size = a.length; size = a.length;
result = size == b.length; result = size == b.length;
@@ -2998,54 +3037,56 @@
if (result) { if (result) {
// deep compare the contents, ignoring non-numeric properties // deep compare the contents, ignoring non-numeric properties
while (size--) { while (size--) {
if (!(result = isEqual(a[size], b[size], stack))) { if (!(result = isEqual(a[size], b[size], stack, thorough))) {
break; break;
} }
} }
} }
return result;
} }
else {
// objects with different constructors are not equal var ctorA = a.constructor,
if ('constructor' in a != 'constructor' in b || a.constructor != b.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; return false;
} }
// deep compare objects }
for (var prop in a) { // handle JScript [[DontEnum]] bug
if (hasOwnProperty.call(a, prop)) { if (hasDontEnumBug) {
// count the number of properties. while (++index < 7) {
size++; prop = shadowed[index];
// deep compare each property value. if (hasOwnProperty.call(a, prop) &&
if (!(result = hasOwnProperty.call(b, prop) && isEqual(a[prop], b[prop], stack))) { !(hasOwnProperty.call(b, prop) && isEqual(a[prop], b[prop], stack, thorough))) {
break; return false;
}
}
}
// 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;
}
}
} }
} }
} }
return result; return true;
} }
/** /**

View File

@@ -132,7 +132,10 @@
// potentially expensive // potentially expensive
for (index = 0; index < this.count; index++) { 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( suites.push(
Benchmark.Suite('`_.countBy` with `callback` iterating an array') Benchmark.Suite('`_.countBy` with `callback` iterating an array')
.add('Lo-Dash', function() { .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( suites.push(
Benchmark.Suite('`_.size` with an object') Benchmark.Suite('`_.size` with an object')
.add('Lo-Dash', function() { .add('Lo-Dash', function() {

View File

@@ -710,9 +710,35 @@
equal(_.isEqual(object, object), false); 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() { test('fixes the JScript [[DontEnum]] bug (test in IE < 9)', function() {
equal(_.isEqual(shadowed, {}), false); 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("<script>parent._._object = { 'a': 1, 'b': 2, 'c': 3 };<\/script>");
idoc.close();
equal(_.isEqual(object, _._object), true);
body.removeChild(iframe);
delete _._object;
}
else {
skipTest();
}
});
}()); }());
/*--------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/