diff --git a/lodash.js b/lodash.js index 55b2bea6c..953ef4a24 100644 --- a/lodash.js +++ b/lodash.js @@ -2236,11 +2236,17 @@ var index = -1, props = keysIn(object), length = props.length, + objLength = length && object.length, result = []; + if (typeof objLength == 'number' && objLength > 0) { + var allowIndexes = isArray(object) || (support.unindexedChars && isString(object)), + maxIndex = objLength - 1; + } while (++index < length) { var key = props[index]; - if (hasOwnProperty.call(object, key)) { + if ((allowIndexes && key > -1 && key <= maxIndex && key % 1 == 0) || + hasOwnProperty.call(object, key)) { result.push(key); } } @@ -6497,8 +6503,8 @@ * // => ['x', 'y'] (property order is not guaranteed across environments) */ var keys = !nativeKeys ? shimKeys : function(object) { - if ((support.enumPrototypes && typeof object == 'function') || - (support.nonEnumArgs && object && object.length && isArguments(object))) { + var length = object ? object.length : 0; + if (typeof length == 'number' && length > 0) { return shimKeys(object); } return isObject(object) ? nativeKeys(object) : []; @@ -6525,19 +6531,30 @@ * // => ['x', 'y', 'z'] (property order is not guaranteed across environments) */ function keysIn(object) { - var result = []; if (!isObject(object)) { - return result; + return []; } - if (support.nonEnumArgs && object.length && isArguments(object)) { - object = slice(object); - } - var skipProto = support.enumPrototypes && typeof object == 'function', - skipErrorProps = support.enumErrorProps && (object === errorProto || object instanceof Error); + var length = object.length; + length = (typeof length == 'number' && length > 0 && + (isArray(object) || (support.unindexedChars && isString(object)) || + (support.nonEnumArgs && isArguments(object))) && length) >>> 0; + var maxIndex = length - 1, + result = Array(length), + skipIndexes = length > 0, + skipErrorProps = support.enumErrorProps && (object === errorProto || object instanceof Error), + skipProto = support.enumPrototypes && typeof object == 'function'; + + if (skipIndexes) { + var index = -1; + while (++index < length) { + result[index] = String(index); + } + } for (var key in object) { if (!(skipProto && key == 'prototype') && - !(skipErrorProps && (key == 'message' || key == 'name'))) { + !(skipErrorProps && (key == 'message' || key == 'name')) && + !(skipIndexes && key > -1 && key <= maxIndex && key % 1 == 0)) { result.push(key); } } @@ -6546,9 +6563,9 @@ // attribute of an existing property and the `constructor` property of a // prototype defaults to non-enumerable. if (support.nonEnumShadows && object !== objectProto) { - var ctor = object.constructor, - index = -1, - length = shadowedProps.length; + var ctor = object.constructor; + index = -1; + length = shadowedProps.length; if (object === (ctor && ctor.prototype)) { var className = object === stringProto ? stringClass : object === errorProto ? errorClass : toString.call(object), diff --git a/test/test.js b/test/test.js index ceab4a6ce..6a10ed987 100644 --- a/test/test.js +++ b/test/test.js @@ -5091,10 +5091,10 @@ deepEqual(_.keys(object), ['a', 'b']); }); - test('should work with sparse arrays', 1, function() { + test('should treat sparse arrays as dense', 1, function() { var array = [1]; array[2] = 3; - deepEqual(_.keys(array), ['0', '2']); + deepEqual(_.keys(array), ['0', '1', '2']); }); test('should work with `arguments` objects (test in IE < 9)', 1, function() { diff --git a/test/underscore.html b/test/underscore.html index 7ceff7a67..867f0ea87 100644 --- a/test/underscore.html +++ b/test/underscore.html @@ -82,6 +82,9 @@ 'isEqual': [ 'Died on test #60', 'Died on test #63' + ], + 'keys': [ + 'is not fooled by sparse arrays; see issue #95' ] }, 'Utility': { @@ -122,6 +125,7 @@ } delete QUnit.config.excused.Chaining; delete QUnit.config.excused.Collections.where; + delete QUnit.config.excused.Objects.keys; delete QUnit.config.excused.Utility['_.escape']; delete QUnit.config.excused.Utility['_.unescape']; }