¿Cómo vincular argumentos de función sin vincular esto?

115

En Javascript, ¿cómo puedo vincular argumentos a una función sin vincular el thisparámetro?

Por ejemplo:

//Example function.
var c = function(a, b, c, callback) {};

//Bind values 1, 2, and 3 to a, b, and c, leave callback unbound.
var b = c.bind(null, 1, 2, 3); //How can I do this without binding scope?

¿Cómo puedo evitar el efecto secundario de tener que vincular también el alcance de la función (por ejemplo, configuración this= nulo)?

Editar:

Perdón por la confusion. Quiero vincular argumentos, luego poder llamar a la función vinculada más tarde y hacer que se comporte exactamente como si llamara a la función original y le pasara los argumentos vinculados:

var x = 'outside object';

var obj = {
  x: 'inside object',
  c: function(a, b, c, callback) {
    console.log(this.x);
  }
};

var b = obj.c.bind(null, 1, 2, 3);

//These should both have exact same output.
obj.c(1, 2, 3, function(){});
b(function(){});

//The following works, but I was hoping there was a better way:
var b = obj.c.bind(obj, 1, 2, 3); //Anyway to make it work without typing obj twice?

Todavía soy nuevo en esto, lo siento por la confusión.

¡Gracias!

Imbuir
fuente
1
¿Sería inadecuado volver a enlazar this? var b = c.bind(this, 1,2,3);
kojiro
¿Por qué es problemático tener el primer valor de bind()ser null? Parece funcionar bien en FF.
Russ
7
Si la función ya tiene este límite, usar null como primer argumento de bind estropeará las cosas (es decir, vinculará un método de objeto al alcance global).
Imbuir
1
Realmente no tengo claro qué estás tratando de hacer. No se puede tener thiscompletamente desatado en JavaScript. Siempre significa algo . Por lo tanto, la vinculación thisal thisde la función adjunta tiene mucho sentido.
kojiro
Soy bastante nuevo en JS. Edité la pregunta para que quede más clara. Puede ser que lo que quiero no sea posible. Gracias.
Imbuir el

Respuestas:

32

Puede hacer esto, pero es mejor evitar pensar en ello como "vinculante", ya que ese es el término utilizado para establecer el valor "this". ¿Quizás pensar en ello como "envolver" los argumentos en una función?

Lo que debe hacer es crear una función que tenga los argumentos deseados integrados a través de cierres:

var withWrappedArguments = function(arg1, arg2)
    {
        return function() { ... do your stuff with arg1 and arg2 ... };
    }(actualArg1Value, actualArg2Value);

Espero tener la sintaxis ahí. Lo que hace es crear una función llamada withWrappedArguments () (para ser pedante, es una función anónima asignada a la variable) que puede llamar en cualquier momento y en cualquier lugar y siempre actuará con actualArg1Value y actualArg2Value, y cualquier otra cosa que desee poner ahí. También puede hacer que acepte más argumentos en el momento de la llamada si lo desea. El secreto son los paréntesis después de la llave de cierre final. Estos hacen que la función externa se ejecute inmediatamente, con los valores pasados, y genere la función interna que se puede llamar más tarde. Los valores pasados ​​se congelan en el momento en que se genera la función.

Esto es efectivamente lo que hace bind, pero de esta manera es explícito que los argumentos envueltos son simplemente cierres en variables locales, y no hay necesidad de cambiar el comportamiento de esto.

Ian Nartowicz
fuente
16
Tenga en cuenta que esto generalmente se llama "Currying".
M3D
@ M3D Capitalizar eso lo hace parecer tan extraño.
Andrew
Esto sería mucho más útil si se proporcionara un ejemplo de cómo usarlo.
Andrew
en.wikipedia.org/wiki/Currying link para cualquiera que esté interesado, aunque la versión TLDR es "En lugar de f(x,y)que queremos f(x)(y)o g=f(x); g(y). Así que cambiaríamos f=(x,y)=>x+ya f=(x)=>(y)=>x+y".
M3D
26

En ES6, esto se hace fácilmente usando parámetros de descanso junto con el operador de propagación .

Entonces podemos definir una función bindArgsque funcione como bind, excepto que solo los argumentos están vinculados, pero no el contexto ( this).

Function.prototype.bindArgs =
    function (...boundArgs)
    {
        const targetFunction = this;
        return function (...args) { return targetFunction.call(this, ...boundArgs, ...args); };
    };

Luego, para una función fooy un objeto especificados obj, la declaración

return foo.call(obj, 1, 2, 3, 4);

es equivalente a

let bar = foo.bindArgs(1, 2);
return bar.call(obj, 3, 4);

donde solo están vinculados el primer y segundo argumento bar, mientras que objse usa el contexto especificado en la invocación y se añaden argumentos adicionales después de los argumentos vinculados. El valor de retorno simplemente se reenvía.

GOTO 0
fuente
11
trae es6 a la mesa, todavía usa var :(
Daniel Kobe
7
@DanielKobe ¡Gracias! Los operadores de propagación y los parámetros de descanso se implementaron en Firefox mucho antes lety constaún no estaban disponibles cuando publiqué esto por primera vez. Está actualizado ahora.
GOTO 0
¡Esto es genial y funciona para funciones de devolución de llamada! También se puede cambiar para agregar argumentos en lugar de anteponerlos.
DenisM
1
Vale la pena señalar que esto crea un cierre cada vez que se llama a bindArgs (), lo que significa que el rendimiento no es tan bueno como el uso de la funcionalidad nativa bind ().
Joshua Walsh
17

En el bindmétodo nativo ,this se pierde el valor de la función de resultado. Sin embargo, puede recodificar fácilmente la corrección común para no usar un argumento para el contexto:

Function.prototype.arg = function() {
    if (typeof this !== "function")
        throw new TypeError("Function.prototype.arg needs to be called on a function");
    var slice = Array.prototype.slice,
        args = slice.call(arguments), 
        fn = this, 
        partial = function() {
            return fn.apply(this, args.concat(slice.call(arguments)));
//                          ^^^^
        };
    partial.prototype = Object.create(this.prototype);
    return partial;
};
Bergi
fuente
4
No me gusta el hecho de que tienes que jugar con el prototipo, Functionpero me ahorró mucho tiempo y algunas soluciones desagradables. Gracias
jackdbernier
3
@jackdbernier: No tienes que hacerlo, pero me parece más intuitivo como un método como bind. Puede transformarlo fácilmente en unfunction bindArgs(fn){ …, args = slice.call(arguments, 1); …}
Bergi
@ Qantas94Heavy: Eso es lo que siempre tienes que hacer cuando quieres thisserlo a. Puede usar binden su lugar, por supuesto. Sin embargo, al OP no le importa explícitamente el thisvalor y quería una función parcial no vinculada.
Bergi
@Bergi: Supuse que el propósito de no configurar thises que la función se usa this, pero no querían vincularla en ese momento. Además, la parte en la pregunta del OP //These should both have exact same output.es contradictoria con otras partes.
Qantas 94 Heavy
1
@KubaWyrostek: Sí, es para hacer que las funciones de constructor parciales funcionen, como una subclase (aunque no es que recomiendo esto). Un real bindno funciona en absoluto con constructores, las funciones enlazadas no tienen .prototype. No,Function.prototype !== mymethod.prototype
Bergi
7

Una pequeña implementación más solo por diversión:

function bindWithoutThis(cb) {
    var bindArgs = Array.prototype.slice.call(arguments, 1);

    return function () {
        var internalArgs = Array.prototype.slice.call(arguments, 0);
        var args = Array.prototype.concat(bindArgs, internalArgs);
        return cb.apply(this, args);
    };
}

Cómo utilizar:

function onWriteEnd(evt) {}
var myPersonalWriteEnd = bindWithoutThis(onWriteEnd, "some", "data");
Dmitrii Sorin
fuente
Parece que sería más correcto pasar "nulo" para esto al invocar apply ()
eddiewould
6
var b = function() {
    return c(1,2,3);
};
Miguel
fuente
3
En todas estas respuestas ruidosas y ambiguas, esta es una respuesta precisa y útil. Por lo tanto +1. Tal vez podría refinar el estilo de la respuesta para hacerla más atractiva a simple vista en ese elegante y ruidoso resto de la jungla. const curry = (fn, ...curryArgs) => (...args) => fn(...curryArgs, args)
Joehannes
2

Es un poco difícil decir exactamente lo que quiere hacer en última instancia porque el ejemplo es un poco arbitrario, pero es posible que desee buscar en parciales (o currying): http://jsbin.com/ifoqoj/1/edit

Function.prototype.partial = function(){
  var fn = this, args = Array.prototype.slice.call(arguments);
  return function(){
    var arg = 0;
    for ( var i = 0; i < args.length && arg < arguments.length; i++ )
      if ( args[i] === undefined )
        args[i] = arguments[arg++];
    return fn.apply(this, args);
  };
};

var c = function(a, b, c, callback) {
  console.log( a, b, c, callback )
};

var b = c.partial(1, 2, 3, undefined);

b(function(){})

Enlace al artículo de John Resig: http://ejohn.org/blog/partial-functions-in-javascript/

Kevin Ennis
fuente
Esta función no funciona, no puede llamar a una función aplicada parcialmente más de una vez.
Bergi
No estoy seguro de seguir. ¿Puede explicarlo un poco o publicar un ejemplo de jsbin?
Kevin Ennis
Vea este ejemplo . Además, partialnecesita ser llamado con undefinedargumentos para que funcione, no lo que uno esperaría, y dividido en funciones que toman un número arbitrario de parámetros.
Bergi
Kevin, gracias por tu respuesta. Está cerca de lo que quiero, pero creo que el thisobjeto aún se estropea. Eche un vistazo a: jsbin.com/ifoqoj/4/edit
Imbue el
Ohhh, gotchya. Ese ejemplo deja mucho más claro lo que estás buscando.
Kevin Ennis
2

Puede que desee vincular la referencia de esto en último lugar, pero su código: -

var c = function(a, b, c, callback) {};
var b = c.bind(null, 1, 2, 3); 

Ya se aplicó el enlace, por ejemplo, este y más tarde no se puede cambiar. Lo que sugeriré es que use la referencia también como un parámetro como este: -

var c = function(a, b, c, callback, ref) {  
    var self = this ? this : ref; 
    // Now you can use self just like this in your code 
};
var b = c.bind(null, 1, 2, 3),
    newRef = this, // or ref whatever you want to apply inside function c()
    d = c.bind(callback, newRef);
Deepak Dixit
fuente
1

Utilice un protagonist!

var geoOpts = {...};

function geoSuccess(user){  // protagonizes for 'user'
  return function Success(pos){
    if(!pos || !pos.coords || !pos.coords.latitude || !pos.coords.longitude){ throw new Error('Geolocation Error: insufficient data.'); }

    var data = {pos.coords: pos.coords, ...};

    // now we have a callback we can turn into an object. implementation can use 'this' inside callback
    if(user){
      user.prototype = data;
      user.prototype.watch = watchUser;
      thus.User = (new user(data));
      console.log('thus.User', thus, thus.User);
    }
  }
}

function geoError(errorCallback){  // protagonizes for 'errorCallback'
  return function(err){
    console.log('@DECLINED', err);
    errorCallback && errorCallback(err);
  }
}

function getUserPos(user, error, opts){
  nav.geo.getPos(geoSuccess(user), geoError(error), opts || geoOpts);
}

Básicamente, la función a la que desea pasar parámetros se convierte en un proxy al que puede llamar para pasar una variable y devuelve la función que realmente desea hacer.

¡Espero que esto ayude!

Cody
fuente
1

Un usuario anónimo publicó esta información adicional:

Sobre la base de lo que ya se ha proporcionado en esta publicación, la solución más elegante que he visto es Curry sus argumentos y contexto:

function Class(a, b, c, d){
    console.log('@Class #this', this, a, b, c, d);
}

function Context(name){
    console.log('@Context', this, name);
    this.name = name;
}

var context1 = new Context('One');
var context2 = new Context('Two');

function curryArguments(fn) {
    var args = Array.prototype.slice.call(arguments, 1);
    return function bindContext() {
      var additional = Array.prototype.slice.call(arguments, 0);
      return fn.apply(this, args.concat(additional));
    };
}

var bindContext = curryArguments(Class, 'A', 'B');

bindContext.apply(context1, ['C', 'D']);
bindContext.apply(context2, ['Y', 'Z']);
Barett
fuente
1

Bueno, para el ejemplo que diste, esto servirá

var b= function(callback){
        return obj.c(1,2,3, callback);
};

Si desea garantizar el cierre de los parámetros:

var b= (function(p1,p2,p3, obj){
        var c=obj.c;
        return function(callback){
                return c.call(obj,p1,p2,p3, callback);
        }
})(1,2,3,obj)

Pero si es así, debes ceñirte a tu solución:

var b = obj.c.bind(obj, 1, 2, 3);

Es la mejor forma.

Joseph Garrone
fuente
1

¿Tan simple como eso?

var b = (cb) => obj.c(1,2,3, cb)
b(function(){}) // insidde object

Solución más general:

function original(a, b, c) { console.log(a, b, c) }
let tied = (...args) => original(1, 2, ...args)

original(1,2,3) // 1 2 3
tied(5,6,7) // 1 2 5

QWERTY
fuente
1

Usando LoDash puedes usar la _.partialfunción.

const f  = function (a, b, c, callback) {}

const pf = _.partial(f, 1, 2, 3)  // f has first 3 arguments bound.

pf(function () {})                // callback.
Daniele Orlando
fuente
0

¿Por qué no utilizar una envoltura alrededor de la función para guardar esto como mitos?

function mythis() {
  this.name = "mythis";
  mythis = this;
  function c(a, b) {
    this.name = "original";
    alert('a=' + a + ' b =' + b + 'this = ' + this.name + ' mythis = ' + mythis.name);
    return "ok";
  }    
  return {
    c: c
  }
};

var retval = mythis().c(0, 1);
Olivier Archer
fuente
-1

jQuery 1.9 trajo exactamente esa característica con la función de proxy.

A partir de jQuery 1.9, cuando el contexto es nulo o indefinido, la función proxy se llamará con el mismo objeto con el que se llamó al proxy. Esto permite usar $ .proxy () para aplicar parcialmente los argumentos de una función sin cambiar el contexto.

Ejemplo:

$.proxy(this.myFunction, 
        undefined /* leaving the context empty */, 
        [precededArg1, precededArg2]);
gosua
fuente
-5

Caso de uso de Jquery:

en lugar:

for(var i = 0;i<3;i++){
    $('<input>').appendTo('body').click(function(i){
        $(this).val(i); // wont work, because 'this' becomes 'i'
    }.bind(i));
}

utilizar este:

for(var i = 0;i<3;i++){
    $('<input>').appendTo('body').click(function(e){
        var i = this;
        $(e.originalEvent.target).val(i);
    }.bind(i));
}

fuente