diff --git a/build.js b/build.js index 0c0186aff..2a6caeddb 100755 --- a/build.js +++ b/build.js @@ -381,21 +381,26 @@ ].join('\n' + indent); }); - // add `__chain__` checks to `_.mixin` - source = source.replace(matchFunction(source, 'mixin'), function(match) { - return match.replace(/^( *)return new lodash.+/m, function() { - var indent = arguments[1]; - return indent + [ - '', - 'var result = func.apply(lodash, args);', - 'if (this.__chain__) {', - ' result = new lodash(result);', - ' result.__chain__ = true;', - '}', - 'return result;' - ].join('\n' + indent); - }); - }); + // replace `_.mixin` + source = replaceFunction(source, 'mixin', [ + 'function mixin(object) {', + ' forEach(functions(object), function(methodName) {', + ' var func = lodash[methodName] = object[methodName];', + '', + ' lodash.prototype[methodName] = function() {', + ' var args = [this.__wrapped__];', + ' push.apply(args, arguments);', + '', + ' var result = func.apply(lodash, args);', + ' if (this.__chain__) {', + ' result = createWrapper(result);', + ' result.__chain__ = true;', + ' }', + ' return result;', + ' };', + ' });', + '}' + ].join('\n')); // replace wrapper `Array` method assignments source = source.replace(/^(?: *\/\/.*\n)*( *)each\(\['[\s\S]+?\n\1}$/m, function(match, indent) { @@ -1174,7 +1179,7 @@ // remove `noCharByIndex` from `_.toArray` source = source.replace(matchFunction(source, 'toArray'), function(match) { - return match.replace(/noCharByIndex[^:]+:/, ''); + return match.replace(/(return\b).+?noCharByIndex[^:]+:\s*/, '$1 '); }); // `noCharByIndex` from `iteratorTemplate` @@ -1753,7 +1758,7 @@ if (isModern) { // remove `_.isPlainObject` fallback source = source.replace(matchFunction(source, 'isPlainObject'), function(match) { - return match.replace(/!getPrototypeOf.+?: */, ''); + return match.replace(/!getPrototypeOf[^:]+:\s*/, ''); }); if (!isMobile) { @@ -1927,7 +1932,7 @@ ' }', ' var isArr = className == arrayClass;', ' if (!isArr) {', - ' if (a.__wrapped__ || b.__wrapped__) {', + ' if (a instanceof lodash || b instanceof lodash) {', ' return isEqual(a.__wrapped__ || a, b.__wrapped__ || b, stackA, stackB);', ' }', ' if (className != objectClass) {', @@ -1989,6 +1994,19 @@ '}' ].join('\n')); + // replace `lodash` + source = replaceFunction(source, 'lodash', [ + 'function lodash(value) {', + ' if (value instanceof lodash) {', + ' return value;', + ' }', + ' if (!(this instanceof lodash)) {', + ' return new lodash(value);', + ' }', + ' this.__wrapped__ = value;', + '}' + ].join('\n')); + // replace `_.omit` source = replaceFunction(source, 'omit', [ 'function omit(object) {', @@ -2189,7 +2207,7 @@ // remove conditional `charCodeCallback` use from `_.max` and `_.min` _.each(['max', 'min'], function(methodName) { source = source.replace(matchFunction(source, methodName), function(match) { - return match.replace(/!callback *&& *isString\(collection\)[\s\S]+?: */g, ''); + return match.replace(/(return\b).+?callback *&& *isString[^:]+:\s*/g, '$1 '); }); }); @@ -2469,9 +2487,14 @@ ' lodash[methodName] = func;', '', ' lodash.prototype[methodName] = function() {', - ' var args = [this.__wrapped__];', + ' var value = this.__wrapped__,', + ' args = [value];', + '', ' push.apply(args, arguments);', - ' return new lodash(func.apply(lodash, args));', + ' var result = func.apply(lodash, args);', + " return (value && typeof value == 'object' && value == result)", + ' ? this', + ' : createWrapper(result);', ' };', '});' ].join('\n' + indent); diff --git a/lodash.js b/lodash.js index ca8c88cc9..a4770b5c4 100644 --- a/lodash.js +++ b/lodash.js @@ -174,6 +174,7 @@ /* Native method shortcuts for methods with the same name as other `lodash` methods */ var nativeBind = reNative.test(nativeBind = slice.bind) && nativeBind, + nativeCreate = reNative.test(nativeCreate = Object.create) && nativeCreate, nativeIsArray = reNative.test(nativeIsArray = Array.isArray) && nativeIsArray, nativeIsFinite = context.isFinite, nativeIsNaN = context.isNaN, @@ -319,7 +320,7 @@ */ function lodash(value) { // exit early if already wrapped, even if wrapped by a different `lodash` constructor - if (value && typeof value == 'object' && value.__wrapped__) { + if (value && typeof value == 'object' && hasOwnProperty.call(value, '__wrapped__')) { return value; } // allow invoking `lodash` without the `new` operator @@ -644,9 +645,7 @@ } if (this instanceof bound) { // ensure `new bound` is an instance of `func` - noop.prototype = func.prototype; - thisBinding = new noop; - noop.prototype = null; + thisBinding = createObject(func.prototype); // mimic the constructor's `return` behavior // http://es5.github.com/#x13.2.2 @@ -769,6 +768,33 @@ ); } + /** + * Creates a new object that inherits from the given `prototype` object. + * + * @private + * @param {Object} prototype The prototype object. + * @returns {Object} Returns the new object. + */ + var createObject = nativeCreate || function(prototype) { + noop.prototype = prototype; + var result = new noop; + noop.prototype = null; + return result; + }; + + /** + * A fast path for creating `lodash` wrapper objects. + * + * @private + * @param {Mixed} value The value to wrap in a `lodash` instance. + * @returns {Object} Returns a `lodash` instance. + */ + function createWrapper(value) { + var result = createObject(lodash.prototype); + result.__wrapped__ = value; + return result; + } + /** * A function compiled to iterate `arguments` objects, arrays, objects, and * strings consistenly across environments, executing the `callback` for each @@ -1562,7 +1588,7 @@ case numberClass: // treat `NaN` vs. `NaN` as equal - return a != +a + return (a != +a) ? b != +b // but treat `+0` vs. `-0` as not equal : (a == 0 ? (1 / a == 1 / b) : a == +b); @@ -1576,7 +1602,7 @@ var isArr = className == arrayClass; if (!isArr) { // unwrap any `lodash` wrapped values - if (a.__wrapped__ || b.__wrapped__) { + if (hasOwnProperty.call(a, '__wrapped__ ') || hasOwnProperty.call(b, '__wrapped__')) { return isEqual(a.__wrapped__ || a, b.__wrapped__ || b, callback, thisArg, stackA, stackB); } // exit for functions and DOM nodes @@ -2748,7 +2774,7 @@ } } } else { - callback = !callback && isString(collection) + callback = (!callback && isString(collection)) ? charAtCallback : createCallback(callback, thisArg); @@ -2817,7 +2843,7 @@ } } } else { - callback = !callback && isString(collection) + callback = (!callback && isString(collection)) ? charAtCallback : createCallback(callback, thisArg); @@ -3181,7 +3207,7 @@ */ function toArray(collection) { if (collection && typeof collection.length == 'number') { - return noCharByIndex && isString(collection) + return (noCharByIndex && isString(collection)) ? collection.split('') : slice(collection); } @@ -3848,7 +3874,7 @@ while (low < high) { var mid = (low + high) >>> 1; - callback(array[mid]) < value + (callback(array[mid]) < value) ? low = mid + 1 : high = mid; } @@ -4573,9 +4599,14 @@ var func = lodash[methodName] = object[methodName]; lodash.prototype[methodName] = function() { - var args = [this.__wrapped__]; + var value = this.__wrapped__, + args = [value]; + push.apply(args, arguments); - return new lodash(func.apply(lodash, args)); + var result = func.apply(lodash, args); + return (value && typeof value == 'object' && value == result) + ? this + : createWrapper(result); }; }); } @@ -5130,7 +5161,7 @@ var result = func(this.__wrapped__, callback, thisArg); return callback == null || (thisArg && typeof callback != 'function') ? result - : new lodash(result); + : createWrapper(result); }; } }); @@ -5172,7 +5203,7 @@ each(['concat', 'slice', 'splice'], function(methodName) { var func = arrayRef[methodName]; lodash.prototype[methodName] = function() { - return new lodash(func.apply(this.__wrapped__, arguments)); + return createWrapper(func.apply(this.__wrapped__, arguments)); }; }); @@ -5190,7 +5221,7 @@ if (value.length === 0) { delete value[0]; } - return isSplice ? new lodash(result) : result; + return isSplice ? createWrapper(result) : result; }; }); } diff --git a/test/test.js b/test/test.js index 2df29a69f..126754dba 100644 --- a/test/test.js +++ b/test/test.js @@ -179,8 +179,8 @@ ok(_() instanceof _); }); - test('should return passed LoDash instances', function() { - var wrapped = _([]); + test('should return passed `lodash` instances', function() { + var wrapped = _(false); equal(_(wrapped), wrapped); }); }()); @@ -810,11 +810,16 @@ QUnit.module('lodash.forEach'); (function() { - test('returns the collection', function() { + test('should return the collection', function() { var collection = [1, 2, 3]; equal(_.forEach(collection, Boolean), collection); }); + test('should return the existing wrapper when chaining', function() { + var wrapper = _([1, 2, 3]); + equal(wrapper.forEach(Boolean), wrapper); + }); + _.each({ 'literal': 'abc', 'object': Object('abc') @@ -901,6 +906,12 @@ _.each(['assign', 'defaults', 'merge'], function(methodName) { var func = _[methodName]; + test('should return the existing wrapper when chaining', function() { + var wrapper = _({ 'a': 1 }); + equal(wrapper[methodName]({ 'b': 2 }), wrapper); + }); + + test('lodash.' + methodName + ' should assign problem JScript properties (test in IE < 9)', function() { var object = { 'constructor': '0',