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`
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');

View File

@@ -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

195
lodash.js
View File

@@ -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;
}
/**

View File

@@ -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() {

View File

@@ -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("<script>parent._._object = { 'a': 1, 'b': 2, 'c': 3 };<\/script>");
idoc.close();
equal(_.isEqual(object, _._object), true);
body.removeChild(iframe);
delete _._object;
}
else {
skipTest();
}
});
}());
/*--------------------------------------------------------------------------*/