Cleanup _.isEqual comments and ensure _.isEmpty/_.size detect arguments objects correctly.

Former-commit-id: 75b044d27b990d55393bf27234aad6ce369f6abe
This commit is contained in:
John-David Dalton
2012-07-20 02:06:13 -04:00
parent a192410498
commit befe0fccaf
4 changed files with 85 additions and 69 deletions

View File

@@ -51,6 +51,7 @@
} }
if (isLegacy) { if (isLegacy) {
source = replaceVar(source, 'noArgsClass', 'true');
['isBindFast', 'isKeysFast', 'nativeBind', 'nativeIsArray', 'nativeKeys'].forEach(function(varName) { ['isBindFast', 'isKeysFast', 'nativeBind', 'nativeIsArray', 'nativeKeys'].forEach(function(varName) {
source = replaceVar(source, varName, 'false'); source = replaceVar(source, varName, 'false');
}); });
@@ -401,7 +402,7 @@
* @returns {String} Returns the `isArguments` fallback snippet. * @returns {String} Returns the `isArguments` fallback snippet.
*/ */
function getIsArgumentsFallback(source) { function getIsArgumentsFallback(source) {
return (source.match(/(?:\s*\/\/.*)*\s*if *\(!(?:lodash\.)?isArguments[^)]+\)[\s\S]+?};\s*}/) || [''])[0]; return (source.match(/(?:\s*\/\/.*)*\s*if *\(noArgsClass[\s\S]+?};\s*}/) || [''])[0];
} }
/** /**
@@ -773,15 +774,15 @@
// build replacement code // build replacement code
lodash.forOwn({ lodash.forOwn({
'Arguments': "'[object Arguments]'", 'Arguments': 'argsClass',
'Date': 'dateClass', 'Date': 'dateClass',
'Function': 'funcClass', 'Function': 'funcClass',
'Number': 'numberClass', 'Number': 'numberClass',
'RegExp': 'regexpClass', 'RegExp': 'regexpClass',
'String': 'stringClass' 'String': 'stringClass'
}, function(value, key) { }, function(value, key) {
// skip `isArguments` if a legacy build // skip `isArguments` if not a mobile build
if (isLegacy && key == 'Arguments') { if (!isMobile && key == 'Arguments') {
return; return;
} }
var funcName = 'is' + key, var funcName = 'is' + key,
@@ -818,13 +819,6 @@
' };\n' + ' };\n' +
' });' ' });'
); );
// tweak `isArguments` fallback
snippet = !isLegacy && getIsArgumentsFallback(source);
if (snippet) {
var modified = '\n' + snippet.replace(/isArguments/g, 'lodash.$&');
source = source.replace(snippet, modified);
}
}()); }());
/*--------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/
@@ -861,8 +855,8 @@
// replace `_.isArguments` with fallback // replace `_.isArguments` with fallback
if (!isRemoved(source, 'isArguments')) { if (!isRemoved(source, 'isArguments')) {
source = source.replace( source = source.replace(
matchFunction(source, 'isArguments').replace(/[\s\S]+?var isArguments *=/, ''), matchFunction(source, 'isArguments').replace(/[\s\S]+?function isArguments/, ''),
getIsArgumentsFallback(source).match(/isArguments *=([\s\S]+?) *};/)[1] + ' };\n' getIsArgumentsFallback(source).match(/isArguments *= *function([\s\S]+?) *};/)[1] + ' }\n'
); );
source = removeIsArgumentsFallback(source); source = removeIsArgumentsFallback(source);

View File

@@ -10,10 +10,10 @@
'accumulator', 'accumulator',
'args', 'args',
'arrayClass', 'arrayClass',
'arrayLikeClasses',
'ArrayProto', 'ArrayProto',
'bind', 'bind',
'callback', 'callback',
'className',
'collection', 'collection',
'compareAscending', 'compareAscending',
'concat', 'concat',
@@ -24,6 +24,7 @@
'identity', 'identity',
'index', 'index',
'indexOf', 'indexOf',
'isArguments',
'isFunc', 'isFunc',
'iteratee', 'iteratee',
'iterateeIndex', 'iterateeIndex',
@@ -215,6 +216,10 @@
// remove unrecognized JSDoc tags so Closure Compiler won't complain // remove unrecognized JSDoc tags so Closure Compiler won't complain
source = source.replace(/@(?:alias|category)\b.*/g, ''); source = source.replace(/@(?:alias|category)\b.*/g, '');
// 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 }");
// add brackets to whitelisted properties so Closure Compiler won't mung them // add brackets to whitelisted properties so Closure Compiler won't mung them
// http://code.google.com/closure/compiler/docs/api-tutorial3.html#export // http://code.google.com/closure/compiler/docs/api-tutorial3.html#export
source = source.replace(RegExp('\\.(' + propWhitelist.join('|') + ')\\b', 'g'), "['$1']"); source = source.replace(RegExp('\\.(' + propWhitelist.join('|') + ')\\b', 'g'), "['$1']");
@@ -239,7 +244,7 @@
}); });
}); });
// remove whitespace from `_.template` related regexpes // remove whitespace from `_.template` related regexes
source = source.replace(/(?:reDelimiterCode\w+|reEmptyString\w+|reInsertVariable) *=.+/g, function(match) { source = source.replace(/(?:reDelimiterCode\w+|reEmptyString\w+|reInsertVariable) *=.+/g, function(match) {
return match.replace(/ |\\n/g, ''); return match.replace(/ |\\n/g, '');
}); });
@@ -346,7 +351,7 @@
else { else {
// minify property name strings // minify property name strings
modified = modified.replace(RegExp("'" + property + "'", 'g'), "'" + minNames[index] + "'"); modified = modified.replace(RegExp("'" + property + "'", 'g'), "'" + minNames[index] + "'");
// minify property names in regexps and accessors // minify property names in regexes and accessors
if (isCreateIterator) { if (isCreateIterator) {
modified = modified.replace(RegExp('([\\.|/])' + property + '\\b' , 'g'), '$1' + minNames[index]); modified = modified.replace(RegExp('([\\.|/])' + property + '\\b' , 'g'), '$1' + minNames[index]);
} }

107
lodash.js
View File

@@ -104,7 +104,8 @@
nativeKeys = reNative.test(nativeKeys = Object.keys) && nativeKeys; nativeKeys = reNative.test(nativeKeys = Object.keys) && nativeKeys;
/** `Object#toString` result shortcuts */ /** `Object#toString` result shortcuts */
var arrayClass = '[object Array]', var argsClass = '[object Arguments]',
arrayClass = '[object Array]',
boolClass = '[object Boolean]', boolClass = '[object Boolean]',
dateClass = '[object Date]', dateClass = '[object Date]',
funcClass = '[object Function]', funcClass = '[object Function]',
@@ -123,6 +124,9 @@
*/ */
var hasDontEnumBug = !propertyIsEnumerable.call({ 'valueOf': 0 }, 'valueOf'); var hasDontEnumBug = !propertyIsEnumerable.call({ 'valueOf': 0 }, 'valueOf');
/** Detect if an `arguments` object's [[Class]] is unresolvable (Firefox < 4, IE < 9) */
var noArgsClass = !isArguments(arguments);
/** Detect if `Array#slice` cannot be used to convert strings to arrays (Opera < 10.52) */ /** Detect if `Array#slice` cannot be used to convert strings to arrays (Opera < 10.52) */
var noArraySliceOnStrings = slice.call('x')[0] != 'x'; var noArraySliceOnStrings = slice.call('x')[0] != 'x';
@@ -136,7 +140,7 @@
/* 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));
/* Detect if `Object.keys` exists and is inferred to be fast (V8, Opera, IE) */ /* Detect if `Object.keys` exists and is inferred to be fast (IE, Opera, V8) */
var isKeysFast = nativeKeys && /^.+$|true/.test(nativeKeys + !!window.attachEvent); var isKeysFast = nativeKeys && /^.+$|true/.test(nativeKeys + !!window.attachEvent);
/** Detect if sourceURL syntax is usable without erroring */ /** Detect if sourceURL syntax is usable without erroring */
@@ -151,6 +155,10 @@
var useSourceURL = (Function('//@cc_on!')(), true); var useSourceURL = (Function('//@cc_on!')(), true);
} catch(e){ } } catch(e){ }
/** Used to identify object classifications that are array-like */
var arrayLikeClasses = {};
arrayLikeClasses[argsClass] = arrayLikeClasses[arrayClass] = arrayLikeClasses[stringClass] = true;
/** /**
* Used to escape characters for inclusion in HTML. * Used to escape characters for inclusion in HTML.
* The `>` and `/` characters don't require escaping in HTML and have no * The `>` and `/` characters don't require escaping in HTML and have no
@@ -592,16 +600,16 @@
} }
// create the function factory // create the function factory
var factory = Function( var factory = Function(
'arrayClass, ArrayProto, bind, compareAscending, concat, funcClass, ' + 'arrayClass, arrayLikeClasses, ArrayProto, bind, compareAscending, concat, ' +
'hasOwnProperty, identity, indexOf, iteratorBind, objectTypes, nativeKeys, ' + 'funcClass, hasOwnProperty, identity, indexOf, isArguments, iteratorBind, ' +
'propertyIsEnumerable, slice, stringClass, toString', 'objectTypes, nativeKeys, propertyIsEnumerable, slice, stringClass, toString',
'return function(' + args + ') {\n' + iteratorTemplate(data) + '\n}' 'return function(' + args + ') {\n' + iteratorTemplate(data) + '\n}'
); );
// return the compiled function // return the compiled function
return factory( return factory(
arrayClass, ArrayProto, bind, compareAscending, concat, funcClass, arrayClass, arrayLikeClasses, ArrayProto, bind, compareAscending, concat,
hasOwnProperty, identity, indexOf, iteratorBind, objectTypes, nativeKeys, funcClass, hasOwnProperty, identity, indexOf, isArguments, iteratorBind,
propertyIsEnumerable, slice, stringClass, toString objectTypes, nativeKeys, propertyIsEnumerable, slice, stringClass, toString
); );
} }
@@ -2650,12 +2658,11 @@
* _.isArguments([1, 2, 3]); * _.isArguments([1, 2, 3]);
* // => false * // => false
*/ */
var isArguments = function(value) { function isArguments(value) {
return toString.call(value) == '[object Arguments]'; return toString.call(value) == argsClass;
}; }
// fallback for browser like Firefox < 4 and IE < 9 which detect // fallback for browsers that can't detect `arguments` objects by [[Class]]
// `arguments` as `[object Object]` if (noArgsClass) {
if (!isArguments(arguments)) {
isArguments = function(value) { isArguments = function(value) {
return !!(value && hasOwnProperty.call(value, 'callee')); return !!(value && hasOwnProperty.call(value, 'callee'));
}; };
@@ -2733,8 +2740,9 @@
} }
/** /**
* Checks if `value` is empty. Arrays or strings with a length of `0` and * Checks if `value` is empty. Arrays, strings, or `arguments` objects with a
* objects with no own enumerable properties are considered "empty". * length of `0` and objects with no own enumerable properties are considered
* "empty".
* *
* @static * @static
* @memberOf _ * @memberOf _
@@ -2756,8 +2764,9 @@
'args': 'value', 'args': 'value',
'init': 'true', 'init': 'true',
'top': 'top':
'var className = toString.call(value);\n' + 'if (arrayLikeClasses[toString.call(value)]' +
'if (className == arrayClass || className == stringClass) return !value.length', (noArgsClass ? ' || isArguments(value)' : '') +
') return !value.length',
'inLoop': { 'inLoop': {
'object': 'return false' 'object': 'return false'
} }
@@ -2794,7 +2803,7 @@
// treat `+0` vs. `-0` as not equal // treat `+0` vs. `-0` as not equal
return a !== 0 || (1 / a == 1 / b); return a !== 0 || (1 / a == 1 / b);
} }
// a strict comparison is necessary because `undefined == null` // a strict comparison is necessary because `null == undefined`
if (a == null || b == null) { if (a == null || b == null) {
return a === b; return a === b;
} }
@@ -2818,11 +2827,11 @@
return false; return false;
} }
switch (className) { switch (className) {
// strings, numbers, dates, and booleans are compared by value case boolClass:
case stringClass: case dateClass:
// primitives and their corresponding object instances are equivalent; // coerce dates and booleans to numbers, dates to milliseconds and booleans
// thus, `'5'` is quivalent to `new String('5')` // to `1` or `0`, treating invalid dates coerced to `NaN` as not equal
return a == String(b); return +a == +b;
case numberClass: case numberClass:
// treat `NaN` vs. `NaN` as equal // treat `NaN` vs. `NaN` as equal
@@ -2831,28 +2840,21 @@
// but treat `+0` vs. `-0` as not equal // but treat `+0` vs. `-0` as not equal
: (a == 0 ? (1 / a == 1 / b) : a == +b); : (a == 0 ? (1 / a == 1 / b) : a == +b);
case boolClass:
case dateClass:
// coerce dates and booleans to numeric values, dates to milliseconds and
// booleans to 1 or 0; treat invalid dates coerced to `NaN` as not equal
return +a == +b;
// regexps are compared by their source and flags
case regexpClass: case regexpClass:
return a.source == b.source && case stringClass:
a.global == b.global && // coerce regexes to strings (http://es5.github.com/#x15.10.6.4)
a.multiline == b.multiline && // treat string primitives and their corresponding object instances as equal
a.ignoreCase == b.ignoreCase; return a == b + '';
} }
if (typeof a != 'object' || typeof b != 'object') { if (typeof a != 'object' || typeof b != 'object') {
// for unequal function values
return false; return false;
} }
// Assume equality for cyclic structures. The algorithm for detecting cyclic // assume cyclic structures are equal
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. // 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)
var length = stack.length; var length = stack.length;
while (length--) { while (length--) {
// Linear search. Performance is inversely proportional to the number of
// unique nested structures.
if (stack[length] == a) { if (stack[length] == a) {
return true; return true;
} }
@@ -2865,7 +2867,7 @@
// add the first collection to the stack of traversed objects // add the first collection to the stack of traversed objects
stack.push(a); stack.push(a);
// recursively compare objects and arrays // recursively compare objects and arrays (susceptible to call stack limits)
if (className == arrayClass) { if (className == arrayClass) {
// compare array lengths to determine if a deep comparison is necessary // compare array lengths to determine if a deep comparison is necessary
size = a.length; size = a.length;
@@ -2881,11 +2883,11 @@
} }
} }
else { else {
// objects with different constructors are not equivalent // objects with different constructors are not equal
if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) { if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) {
return false; return false;
} }
// deep compare objects. // deep compare objects
for (var prop in a) { for (var prop in a) {
if (hasOwnProperty.call(a, prop)) { if (hasOwnProperty.call(a, prop)) {
// count the number of properties. // count the number of properties.
@@ -2899,14 +2901,14 @@
// ensure both objects have the same number of properties // ensure both objects have the same number of properties
if (result) { if (result) {
for (prop in b) { for (prop in b) {
// Adobe's JS engine, embedded in applications like InDesign, has a // The JS engine in Adobe products, like InDesign, has a bug that causes
// bug that causes `!size--` to throw an error so it must be wrapped // `!size--` to throw an error so it must be wrapped in parentheses.
// in parentheses.
// https://github.com/documentcloud/underscore/issues/355 // https://github.com/documentcloud/underscore/issues/355
if (hasOwnProperty.call(b, prop) && !(size--)) { if (hasOwnProperty.call(b, prop) && !(size--)) {
break; break;
} }
} }
// `size` will be `-1` if `b` has more properties than `a`
result = !size; result = !size;
} }
// handle JScript [[DontEnum]] bug // handle JScript [[DontEnum]] bug
@@ -2971,7 +2973,7 @@
/** /**
* Checks if `value` is the language type of Object. * Checks if `value` is the language type of Object.
* (e.g. arrays, functions, objects, regexps, `new Number(0)`, and `new String('')`) * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
* *
* @static * @static
* @memberOf _ * @memberOf _
@@ -3167,16 +3169,16 @@
} }
/** /**
* Gets the size of `value` by returning `value.length` if `value` is a string * Gets the size of `value` by returning `value.length` if `value` is an
* or array, or the number of own enumerable properties if `value` is an object. * array, string, or `arguments` object. If `value` is an object, size is
* determined by returning the number of own enumerable properties it has.
* *
* @deprecated * @deprecated
* @static * @static
* @memberOf _ * @memberOf _
* @category Objects * @category Objects
* @param {Array|Object|String} value The value to inspect. * @param {Array|Object|String} value The value to inspect.
* @returns {Number} Returns `value.length` if `value` is a string or array, * @returns {Number} Returns `value.length` or number of own enumerable properties.
* or the number of own enumerable properties if `value` is an object.
* @example * @example
* *
* _.size([1, 2]); * _.size([1, 2]);
@@ -3192,8 +3194,9 @@
if (!value) { if (!value) {
return 0; return 0;
} }
var length = value.length; return arrayLikeClasses[toString.call(value)] || (noArgsClass && isArguments(value))
return length === length >>> 0 ? value.length : keys(value).length; ? value.length
: keys(value).length;
} }
/** /**

View File

@@ -546,6 +546,8 @@
QUnit.module('lodash.isEmpty'); QUnit.module('lodash.isEmpty');
(function() { (function() {
var args = arguments;
test('fixes the JScript [[DontEnum]] bug (test in IE < 9)', function() { test('fixes the JScript [[DontEnum]] bug (test in IE < 9)', function() {
equal(_.isEmpty(shadowed), false); equal(_.isEmpty(shadowed), false);
}); });
@@ -558,7 +560,15 @@
Foo.prototype = { 'a': 1 }; Foo.prototype = { 'a': 1 };
equal(_.isEmpty(Foo), true); equal(_.isEmpty(Foo), true);
}); });
}());
test('should work with an object that has a `length` property', function() {
equal(_.isEmpty({ 'length': 0 }), false);
});
test('should work with `arguments` objects (test in IE < 9)', function() {
equal(_.isEmpty(args), false);
});
}(1, 2, 3));
/*--------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/
@@ -784,6 +794,10 @@
deepEqual(actual, [0, 0, 0, 0, 0]); deepEqual(actual, [0, 0, 0, 0, 0]);
}); });
test('should work with an object that has a `length` property', function() {
equal(_.size({ 'length': 3 }), 1);
});
test('should work with `arguments` objects (test in IE < 9)', function() { test('should work with `arguments` objects (test in IE < 9)', function() {
equal(_.size(args), 3); equal(_.size(args), 3);
}); });