From c62b24b024d799586840fd68ed89b6682a50de34 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Thu, 24 May 2012 01:25:08 -0400 Subject: [PATCH] Make `_.bind` follow ES5 spec so it will work with a common Backbone pattern. [closes #11] Former-commit-id: 8d5e399ca9727a32604601f81fffd9134104c8f4 --- lodash.js | 46 ++++++++++++++++++++++++++++++++++++--------- perf/perf.js | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/lodash.js b/lodash.js index d6e248bfd..120ae3b98 100644 --- a/lodash.js +++ b/lodash.js @@ -472,6 +472,15 @@ return '\\' + escapes[match]; } + /** + * A no-operation function. + * + * @private + */ + function noop() { + // no operation performed + } + /** * Used by `template()` to replace "escape" template delimiters with tokens. * @@ -1701,18 +1710,20 @@ } // use if `Function#bind` is faster else if (nativeBind) { - func = nativeBind.call.apply(nativeBind, arguments); - return function() { - return arguments.length ? func.apply(undefined, arguments) : func(); - }; + return nativeBind.call.apply(nativeBind, arguments); + } + // spec'd to throw a TypeError + // http://es5.github.com/#x15.3.4.5 + else if (toString.call(func) != funcClass) { + throw new TypeError; } var partialArgs = slice.call(arguments, 2), partialArgsLength = partialArgs.length; - return function() { - var result, - args = arguments; + function bound() { + var args = arguments, + thisBinding = thisArg; if (!isFunc) { func = thisArg[methodName]; @@ -1724,10 +1735,27 @@ } args = partialArgs; } - result = args.length ? func.apply(thisArg, args) : func.call(thisArg); + var isInstance = this instanceof bound; + if (isInstance) { + // get `func` instance if `bound` is invoked in a `new` expression + noop.prototype = func.prototype; + thisBinding = new noop; + } + + var result = args.length ? func.apply(thisBinding, args) : func.call(thisBinding); partialArgs.length = partialArgsLength; + + if (isInstance) { + // mimic a constructor's `return` behavior + // http://es5.github.com/#x13.2.2 + return objectTypes[typeof result] && result !== null + ? result + : thisBinding + } return result; - }; + } + + return bound; } /** diff --git a/perf/perf.js b/perf/perf.js index 03810e080..7fc1ce267 100644 --- a/perf/perf.js +++ b/perf/perf.js @@ -55,6 +55,17 @@ 'seventeen', 'eighteen', 'nineteen', 'twenty' ]; + var ctor = function() { }, + func = function(greeting) { return greeting + ': ' + this.name; }; + + var lodashBoundNormal = lodash.bind(func, { 'name': 'moe' }), + lodashBoundCtor = lodash.bind(ctor, { 'name': 'moe' }), + lodashBoundPartial = lodash.bind(func, { 'name': 'moe' }, 'hi'); + + var _boundNormal = _.bind(func, { 'name': 'moe' }), + _boundCtor = _.bind(ctor, { 'name': 'moe' }), + _boundPartial = _.bind(func, { 'name': 'moe' }, 'hi'); + for (var index = 0; index < 20; index++) { numbers[index] = index; object['key' + index] = index; @@ -108,6 +119,48 @@ /*--------------------------------------------------------------------------*/ + suites.push( + Benchmark.Suite('bind call') + .add('Lo-Dash', function() { + lodash.bind(func, { 'name': 'moe' }, 'hi'); + }) + .add('Underscore', function() { + _.bind(func, { 'name': 'moe' }, 'hi'); + }) + ); + + suites.push( + Benchmark.Suite('bound normal') + .add('Lo-Dash', function() { + lodashBoundNormal(); + }) + .add('Underscore', function() { + _boundNormal(); + }) + ); + + suites.push( + Benchmark.Suite('bound partial') + .add('Lo-Dash', function() { + lodashBoundPartial(); + }) + .add('Underscore', function() { + _boundPartial(); + }) + ); + + suites.push( + Benchmark.Suite('bound constructor') + .add('Lo-Dash', function() { + new lodashBoundCtor(); + }) + .add('Underscore', function() { + new _boundCtor(); + }) + ); + + /*--------------------------------------------------------------------------*/ + suites.push( Benchmark.Suite('each array') .add('Lo-Dash', function() {