diff --git a/underscore.js b/underscore.js index 86e262343..5e1474c3e 100644 --- a/underscore.js +++ b/underscore.js @@ -24,7 +24,8 @@ var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; // Create quick reference variables for speed access to core prototypes. - var slice = ArrayProto.slice, + var concat = ArrayProto.concat, + slice = ArrayProto.slice, unshift = ArrayProto.unshift, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; @@ -72,6 +73,38 @@ // Collection Functions // -------------------- + // list of possible shadowed properties of Object.prototype + var shadowed = [ + 'constructor', 'hasOwnProperty', 'isPrototypeOf', + 'propertyIsEnumerable', 'toLocaleString', + 'toString', 'valueOf' + ]; + + // IE < 9 skips enumerable properties shadowing non-enumerable ones + var forShadowed = !{valueOf:0}.propertyIsEnumerable('valueOf') && + function(obj, iterator) { + // because IE < 9 can't set the [[Enumerable]] attribute of an existing + // property and the `constructor` property of a prototype defaults to + // non-enumerable, we manually skip the `constructor` property when we + // think we are iterating over a `prototype` object. + var ctor = obj.constructor; + var skipCtor = ctor && ctor.prototype && ctor.prototype.constructor == ctor; + for (var key, i = 0; key = shadowed[i]; i++) { + if (!(skipCtor && key == 'constructor') && + hasOwnProperty.call(obj, key) && + iterator(obj[key], key, obj) === breaker) { + break; + } + } + }; + + var simpleEach = function(obj, iterator, index) { + index || (index = 0); + for(var l = obj.length; index < l; index++) { + iterator(obj[index]); + } + }; + // The cornerstone, an `each` implementation, aka `forEach`. // Handles objects with the built-in `forEach`, arrays, and raw objects. // Delegates to **ECMAScript 5**'s native `forEach` if available. @@ -79,16 +112,42 @@ if (obj == null) return; if (nativeForEach && obj.forEach === nativeForEach) { obj.forEach(iterator, context); - } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; - } } else { - for (var key in obj) { - if (hasOwnProperty.call(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) return; - } + var fn = iterator; + var i = -1; + var l = obj.length; + if (context) { + iterator = function() { return fn.call(context, obj[i], i, obj); }; } + if (l === l >>> 0) { + while (++i < l) { + if (i in obj && iterator(obj[i], i, obj) == breaker) return; + } + } else { + forProps(obj, iterator, true); + } + } + }; + + // Iterates over an object's properties, executing the `callback` for each. + var forProps = function(obj, iterator, ownOnly, context) { + var done = !obj; + var skipProto = typeof obj == 'function'; + + for (var key in obj) { + // Opera < 12 and Safari < 5.1 (if the prototype or a property on the prototype has been set) + // incorrectly set a function's `prototype` property [[Enumerable]] value + // to true. Because of this we standardize on skipping the the `prototype` + // property of functions regardless of their [[Enumerable]] value. + if (done = + !(skipProto && key == 'prototype') && + (!ownOnly || ownOnly && hasOwnProperty.call(obj, key)) && + iterator.call(context, obj[key], key, obj) === breaker) { + break; + } + } + if (!done && forShadowed) { + forShadowed(obj, iterator, context); } }; @@ -189,7 +248,7 @@ // Delegates to **ECMAScript 5**'s native `some` if available. // Aliased as `any`. var any = _.some = _.any = function(obj, iterator, context) { - iterator = iterator || _.identity; + iterator || (iterator = _.identity); var result = false; if (obj == null) return result; if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); @@ -228,7 +287,7 @@ _.max = function(obj, iterator, context) { if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); if (!iterator && _.isEmpty(obj)) return -Infinity; - var result = {computed : -Infinity}; + var result = { computed : -Infinity }; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed >= result.computed && (result = {value : value, computed : computed}); @@ -240,7 +299,7 @@ _.min = function(obj, iterator, context) { if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); if (!iterator && _.isEmpty(obj)) return Infinity; - var result = {computed : Infinity}; + var result = { computed : Infinity }; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed < result.computed && (result = {value : value, computed : computed}); @@ -267,11 +326,12 @@ _.sortBy = function(obj, iterator, context) { return _.pluck(_.map(obj, function(value, index, list) { return { - value : value, - criteria : iterator.call(context, value, index, list) + value: value, + criteria: iterator.call(context, value, index, list) }; }).sort(function(left, right) { - var a = left.criteria, b = right.criteria; + var a = left.criteria; + var b = right.criteria; return a < b ? -1 : a > b ? 1 : 0; }), 'value'); }; @@ -292,7 +352,8 @@ // be inserted so as to maintain order. Uses binary search. _.sortedIndex = function(array, obj, iterator) { iterator || (iterator = _.identity); - var low = 0, high = array.length; + var low = 0; + var high = array.length; while (low < high) { var mid = (low + high) >> 1; iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; @@ -302,10 +363,9 @@ // Safely convert anything iterable into a real, live array. _.toArray = function(iterable) { - if (!iterable) return []; - if (iterable.toArray) return iterable.toArray(); - if (_.isArray(iterable)) return slice.call(iterable); - if (_.isArguments(iterable)) return slice.call(iterable); + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + if (_.isArray(iterable) || _.isArguments(iterable)) return slice.call(iterable); return _.values(iterable); }; @@ -420,10 +480,9 @@ // Zip together multiple lists into a single array -- elements that share // an index go together. _.zip = function() { - var args = slice.call(arguments); - var length = _.max(_.pluck(args, 'length')); + var length = _.max(_.pluck(arguments, 'length')); var results = new Array(length); - for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); + for (var i = 0; i < length; i++) results[i] = _.pluck(arguments, i); return results; }; @@ -472,7 +531,6 @@ range[idx++] = start; start += step; } - return range; }; @@ -492,10 +550,10 @@ if (!_.isFunction(func)) throw new TypeError; args = slice.call(arguments, 2); return bound = function() { - if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + if (!(this instanceof bound)) return func.apply(context, concat.apply(args, arguments)); ctor.prototype = func.prototype; var self = new ctor; - var result = func.apply(self, args.concat(slice.call(arguments))); + var result = func.apply(self, concat.apply(args, arguments)); if (Object(result) === result) return result; return self; }; @@ -504,16 +562,20 @@ // Bind all of an object's methods to that object. Useful for ensuring that // all callbacks defined on an object belong to it. _.bindAll = function(obj) { - var funcs = slice.call(arguments, 1); - if (funcs.length == 0) funcs = _.functions(obj); - each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + var i = 1; + var funcs = arguments; + if (funcs.length < 2) { + i = 0; + funcs = _.functions(obj); + } + simpleEach(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }, i); return obj; }; // Memoize an expensive function by storing its results. _.memoize = function(func, hasher) { - var memo = {}; hasher || (hasher = _.identity); + var memo = {}; return function() { var key = hasher.apply(this, arguments); return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); @@ -588,7 +650,7 @@ // conditionally execute the original function. _.wrap = function(func, wrapper) { return function() { - var args = [func].concat(slice.call(arguments)); + var args = concat.apply([func], arguments); return wrapper.apply(this, args); }; }; @@ -596,9 +658,9 @@ // Returns a function that is the composition of a list of functions, each // consuming the return value of the function that follows. _.compose = function() { - var funcs = slice.call(arguments); + var funcs = arguments; return function() { - var args = slice.call(arguments); + var args = arguments; for (var i = funcs.length - 1; i >= 0; i--) { args = [funcs[i].apply(this, args)]; } @@ -622,7 +684,9 @@ _.keys = nativeKeys || function(obj) { if (obj !== Object(obj)) throw new TypeError('Invalid object'); var keys = []; - for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key; + forProps(obj, function(value, key) { + keys[keys.length] = key; + }, true); return keys; }; @@ -635,29 +699,29 @@ // Aliased as `methods` _.functions = _.methods = function(obj) { var names = []; - for (var key in obj) { - if (_.isFunction(obj[key])) names.push(key); - } + forProps(obj, function(value, key) { + if (_.isFunction(value)) names[names.length] = key; + }); return names.sort(); }; // Extend a given object with all the properties in passed-in object(s). _.extend = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (source[prop] !== void 0) obj[prop] = source[prop]; - } - }); + simpleEach(arguments, function(source) { + forProps(source, function(value, key) { + if (value !== void 0) obj[key] = value; + }); + }, 1); return obj; }; // Fill in a given object with default properties. _.defaults = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; - } - }); + simpleEach(arguments, function(source) { + forProps(source, function(value, key) { + if (obj[key] == null) obj[key] = value; + }); + }, 1); return obj; }; @@ -686,8 +750,8 @@ if (a._chain) a = a._wrapped; if (b._chain) b = b._wrapped; // Invoke a custom `isEqual` method if one is provided. - if (_.isFunction(a.isEqual)) return a.isEqual(b); - if (_.isFunction(b.isEqual)) return b.isEqual(a); + if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); + if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); // Compare `[[Class]]` names. var className = toString.call(a); if (className != toString.call(b)) return false; @@ -696,13 +760,11 @@ case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. - return String(a) == String(b); + return a == String(b); case '[object Number]': - a = +a; - b = +b; // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for // other numeric values. - return a != a ? b != b : (a == 0 ? 1 / a == 1 / b : a == b); + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their @@ -742,21 +804,19 @@ } } else { // Objects with different constructors are not equivalent. - if ("constructor" in a != "constructor" in b || a.constructor != b.constructor) return false; + if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; // Deep compare objects. - for (var key in a) { - if (hasOwnProperty.call(a, key)) { - // Count the expected number of properties. - size++; - // Deep compare each member. - if (!(result = hasOwnProperty.call(b, key) && eq(a[key], b[key], stack))) break; - } - } + forProps(a, function(value, key) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = hasOwnProperty.call(b, key) && eq(value, b[key], stack))) return breaker; + }, true); // Ensure that both objects contain the same number of properties. if (result) { - for (key in b) { - if (hasOwnProperty.call(b, key) && !(size--)) break; - } + forProps(b, function() { + return !(size--) && breaker; + }, true); result = !size; } } @@ -773,9 +833,15 @@ // Is a given array, string, or object empty? // An "empty" object has no enumerable own-properties. _.isEmpty = function(obj) { - if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; - for (var key in obj) if (hasOwnProperty.call(obj, key)) return false; - return true; + var result = toString.call(obj); + if (result == '[object Array]' || result == '[object String]') { + return !obj.length; + } + forProps(obj, function() { + result = false; + return breaker; + }, true); + return !!result; }; // Is a given value a DOM element? @@ -795,11 +861,11 @@ }; // Is a given variable an arguments object? - if (toString.call(arguments) == '[object Arguments]') { - _.isArguments = function(obj) { - return toString.call(obj) == '[object Arguments]'; - }; - } else { + _.isArguments = function(obj) { + return toString.call(obj) == '[object Arguments]'; + }; + + if (!_.isArguments(arguments)) { _.isArguments = function(obj) { return !!(obj && hasOwnProperty.call(obj, 'callee')); }; @@ -879,7 +945,7 @@ // Add your own custom functions to the Underscore object, ensuring that // they're correctly added to the OOP wrapper as well. _.mixin = function(obj) { - each(_.functions(obj), function(name){ + simpleEach(_.functions(obj), function(name){ addToWrapper(name, _[name] = obj[name]); }); }; @@ -949,6 +1015,9 @@ // A method to easily add functions to the OOP wrapper. var addToWrapper = function(name, func) { wrapper.prototype[name] = function() { + // avoid applying unshift generically to the arguments object because + // Opera < 9.50 will pave the value at index 0 without incrimenting the + // other index/values var args = slice.call(arguments); unshift.call(args, this._wrapped); return result(func.apply(_, args), this._chain); @@ -959,7 +1028,7 @@ _.mixin(_); // Add all mutator Array functions to the wrapper. - each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + simpleEach(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { var method = ArrayProto[name]; wrapper.prototype[name] = function() { method.apply(this._wrapped, arguments); @@ -968,7 +1037,7 @@ }); // Add all accessor Array functions to the wrapper. - each(['concat', 'join', 'slice'], function(name) { + simpleEach(['concat', 'join', 'slice'], function(name) { var method = ArrayProto[name]; wrapper.prototype[name] = function() { return result(method.apply(this._wrapped, arguments), this._chain);