From 9f8cd6c499844451468257140e71f611abb3a040 Mon Sep 17 00:00:00 2001 From: Gianni Chiappetta Date: Tue, 14 Dec 2010 21:53:04 -0500 Subject: [PATCH 1/6] Fixing $.proxy to work like (and use) Function.prototype.bind (ticket #7783) http://bugs.jquery.com/ticket/7783 --- src/core.js | 49 +++++++++++++++++++++++++++++------------------ src/event.js | 44 ++++++++++++++++++++++++++---------------- test/unit/core.js | 11 ++++++++--- 3 files changed, 65 insertions(+), 39 deletions(-) diff --git a/src/core.js b/src/core.js index 346e52d7..74ec4ea0 100644 --- a/src/core.js +++ b/src/core.js @@ -740,31 +740,42 @@ jQuery.extend({ // A global GUID counter for objects guid: 1, - proxy: function( fn, proxy, thisObject ) { - if ( arguments.length === 2 ) { - if ( typeof proxy === "string" ) { - thisObject = fn; - fn = thisObject[ proxy ]; - proxy = undefined; + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var args, proxy; - } else if ( proxy && !jQuery.isFunction( proxy ) ) { - thisObject = proxy; - proxy = undefined; + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( ! jQuery.isFunction( fn ) ) { + return undefined; + } + + if ( jQuery.isFunction( Function.prototype.bind ) ) { + // Native bind + args = slice.call( arguments, 1 ); + proxy = Function.prototype.bind.apply( fn, args ); + } else { + // Simulated bind + args = slice.call( arguments, 2 ); + if ( args.length ) { + proxy = function() { + return arguments.length ? + fn.apply( context, args.concat( slice.call( arguments ) ) ) : + fn.apply( context, args ); + }; + } else { + proxy = function() { + return arguments.length ? + fn.apply( context, arguments ) : + fn.call( context ); + }; } } - if ( !proxy && fn ) { - proxy = function() { - return fn.apply( thisObject || this, arguments ); - }; - } - // Set the guid of unique handler to the same of original handler, so it can be removed - if ( fn ) { - proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - } + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - // So proxy can be declared as an argument return proxy; }, diff --git a/src/event.js b/src/event.js index fd470e71..82e9b52d 100644 --- a/src/event.js +++ b/src/event.js @@ -891,6 +891,8 @@ if ( document.addEventListener ) { jQuery.each(["bind", "one"], function( i, name ) { jQuery.fn[ name ] = function( type, data, fn ) { + var handler; + // Handle object literals if ( typeof type === "object" ) { for ( var key in type ) { @@ -904,10 +906,15 @@ jQuery.each(["bind", "one"], function( i, name ) { data = undefined; } - var handler = name === "one" ? jQuery.proxy( fn, function( event ) { - jQuery( this ).unbind( event, handler ); - return fn.apply( this, arguments ); - }) : fn; + if ( name === "one" ) { + handler = function( event ) { + jQuery( this ).unbind( event, handler ); + return fn.apply( this, arguments ); + }; + handler.guid = fn.guid || jQuery.guid++; + } else { + handler = fn; + } if ( type === "unload" && name !== "one" ) { this.one( type, data, fn ); @@ -971,24 +978,27 @@ jQuery.fn.extend({ toggle: function( fn ) { // Save reference to arguments for access in closure var args = arguments, - i = 1; + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; while ( i < args.length ) { - jQuery.proxy( fn, args[ i++ ] ); + args[ i++ ].guid = guid; } - return this.click( jQuery.proxy( fn, function( event ) { - // Figure out which function to execute - var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; - jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); - - // Make sure that clicks stop - event.preventDefault(); - - // and execute the function - return args[ lastToggle ].apply( this, arguments ) || false; - })); + return this.click( toggler ); }, hover: function( fnOver, fnOut ) { diff --git a/test/unit/core.js b/test/unit/core.js index 70577837..9f9078a4 100644 --- a/test/unit/core.js +++ b/test/unit/core.js @@ -858,7 +858,7 @@ test("jQuery.isEmptyObject", function(){ }); test("jQuery.proxy", function(){ - expect(4); + expect(5); var test = function(){ equals( this, thisObject, "Make sure that scope is set properly." ); }; var thisObject = { foo: "bar", method: test }; @@ -872,8 +872,13 @@ test("jQuery.proxy", function(){ // Make sure it doesn't freak out equals( jQuery.proxy( null, thisObject ), undefined, "Make sure no function was returned." ); - // Use the string shortcut - jQuery.proxy( thisObject, "method" )(); + // Partial application + var test2 = function( a ){ equals( a, "pre-applied", "Ensure arguments can be pre-applied." ); }; + jQuery.proxy( test2, null, "pre-applied" )(); + + // Partial application w/ normal arguments + var test3 = function( a, b ){ equals( b, "normal", "Ensure arguments can be pre-applied and passed as usual." ); }; + jQuery.proxy( test3, null, "pre-applied" )( "normal" ); }); test("jQuery.parseJSON", function(){ From 5b1b57850cfd4c92a4f9231581dff7faac489566 Mon Sep 17 00:00:00 2001 From: Gianni Chiappetta Date: Wed, 15 Dec 2010 18:31:10 -0500 Subject: [PATCH 2/6] Add a quick test to $.support for native bind. As per the suggestion by ajpiano: https://github.com/gf3/jquery/commit/9f8cd6c499844451468257140e71f611abb3a040#commitcomment-218658 --- src/core.js | 2 +- src/support.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core.js b/src/core.js index 74ec4ea0..a6e2a46f 100644 --- a/src/core.js +++ b/src/core.js @@ -751,7 +751,7 @@ jQuery.extend({ return undefined; } - if ( jQuery.isFunction( Function.prototype.bind ) ) { + if ( jQuery.support.nativeBind ) { // Native bind args = slice.call( arguments, 1 ); proxy = Function.prototype.bind.apply( fn, args ); diff --git a/src/support.js b/src/support.js index 67b41c4a..2913d217 100644 --- a/src/support.js +++ b/src/support.js @@ -60,6 +60,9 @@ // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) optSelected: opt.selected, + // Test for native Function#bind. + nativeBind: jQuery.isFunction( Function.prototype.bind ), + // Will be defined later deleteExpando: true, optDisabled: false, From 1ebb5ab3e1c670e04024cd949fa6f341e93c4487 Mon Sep 17 00:00:00 2001 From: Gianni Chiappetta Date: Thu, 16 Dec 2010 16:04:23 -0500 Subject: [PATCH 3/6] Added list of browsers that currently support Function#bind. --- src/support.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/support.js b/src/support.js index 2913d217..1fd1c2a6 100644 --- a/src/support.js +++ b/src/support.js @@ -60,7 +60,8 @@ // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) optSelected: opt.selected, - // Test for native Function#bind. + // Test for presence of native Function#bind. + // Currently in: Chrome 7, FireFox 4 nativeBind: jQuery.isFunction( Function.prototype.bind ), // Will be defined later From 6bc9fc7c10a6dd2c8c7132307e63323a1f59d35f Mon Sep 17 00:00:00 2001 From: Gianni Chiappetta Date: Sat, 18 Dec 2010 19:17:37 -0500 Subject: [PATCH 4/6] Perf. improvement based on fearphage's suggestion (direct vs call vs apply). --- src/core.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core.js b/src/core.js index a6e2a46f..2ffcb911 100644 --- a/src/core.js +++ b/src/core.js @@ -754,7 +754,11 @@ jQuery.extend({ if ( jQuery.support.nativeBind ) { // Native bind args = slice.call( arguments, 1 ); - proxy = Function.prototype.bind.apply( fn, args ); + if ( args.length ) { + proxy = Function.prototype.bind.apply( fn, args ); + } else { + proxy = fn.bind( context ); + } } else { // Simulated bind args = slice.call( arguments, 2 ); From ade531cfaa194eb13fa6307368d1e715f3be8326 Mon Sep 17 00:00:00 2001 From: Gianni Chiappetta Date: Sat, 18 Dec 2010 19:26:36 -0500 Subject: [PATCH 5/6] Noted which browsers don't support Function#bind. --- src/support.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/support.js b/src/support.js index 1fd1c2a6..55c19d80 100644 --- a/src/support.js +++ b/src/support.js @@ -61,7 +61,7 @@ optSelected: opt.selected, // Test for presence of native Function#bind. - // Currently in: Chrome 7, FireFox 4 + // Not in: >= Chrome 6, >= FireFox 3, Safari 5?, IE 9?, Opera 11? nativeBind: jQuery.isFunction( Function.prototype.bind ), // Will be defined later From 574ae3b1be555dbd5242532739cd8e0a34e0569c Mon Sep 17 00:00:00 2001 From: Gianni Chiappetta Date: Fri, 21 Jan 2011 10:33:50 -0500 Subject: [PATCH 6/6] added: Backcompatibility with old proxy syntax. --- src/core.js | 6 ++++++ test/unit/core.js | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/core.js b/src/core.js index 3bdedf53..ea3506a6 100644 --- a/src/core.js +++ b/src/core.js @@ -739,6 +739,12 @@ jQuery.extend({ proxy: function( fn, context ) { var args, proxy; + // XXX BACKCOMPAT: Support old string method. + if ( typeof context === "string" ) { + fn = fn[ context ]; + context = arguments[0]; + } + // Quick check to determine if target is callable, in the spec // this throws a TypeError, but we will just return undefined. if ( ! jQuery.isFunction( fn ) ) { diff --git a/test/unit/core.js b/test/unit/core.js index 7638554a..332fc51e 100644 --- a/test/unit/core.js +++ b/test/unit/core.js @@ -869,7 +869,7 @@ test("jQuery.isEmptyObject", function(){ }); test("jQuery.proxy", function(){ - expect(5); + expect(6); var test = function(){ equals( this, thisObject, "Make sure that scope is set properly." ); }; var thisObject = { foo: "bar", method: test }; @@ -890,6 +890,10 @@ test("jQuery.proxy", function(){ // Partial application w/ normal arguments var test3 = function( a, b ){ equals( b, "normal", "Ensure arguments can be pre-applied and passed as usual." ); }; jQuery.proxy( test3, null, "pre-applied" )( "normal" ); + + // Test old syntax + var test4 = { meth: function( a ){ equals( a, "boom", "Ensure old syntax works." ); } }; + jQuery.proxy( test4, "meth" )( "boom" ); }); test("jQuery.parseJSON", function(){