mirror of
https://github.com/whoisclebs/lodash.git
synced 2026-01-29 06:27:49 +00:00
Ensure _.isEqual works correctly for objects from another document and add _.clone benchmark.
Former-commit-id: b1ef745ec6c24e8ea0c8fae304ead80c60dfd5aa
This commit is contained in:
7
build.js
7
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');
|
||||
|
||||
|
||||
@@ -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
195
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
27
perf/perf.js
27
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() {
|
||||
|
||||
26
test/test.js
26
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("<script>parent._._object = { 'a': 1, 'b': 2, 'c': 3 };<\/script>");
|
||||
idoc.close();
|
||||
|
||||
equal(_.isEqual(object, _._object), true);
|
||||
body.removeChild(iframe);
|
||||
delete _._object;
|
||||
}
|
||||
else {
|
||||
skipTest();
|
||||
}
|
||||
});
|
||||
}());
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
Reference in New Issue
Block a user