Comment lier des arguments de fonction sans lier cela?


115

En Javascript, comment puis-je lier des arguments à une fonction sans lier le thisparamètre?

Par exemple:

//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?

Comment puis-je éviter l'effet secondaire d'avoir à lier également la portée de la fonction (par exemple, setting this= null)?

Éditer:

Désolé pour la confusion. Je veux lier des arguments, puis pouvoir appeler la fonction liée plus tard et la faire se comporter exactement comme si j'appelais la fonction d'origine et lui passais les arguments liés:

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?

Je suis encore nouveau dans ce domaine, désolé pour la confusion.

Merci!


1
Serait-il insuffisant de simplement refaire la liaison this? var b = c.bind(this, 1,2,3);
kojiro

Pourquoi est-il problématique d'avoir la première valeur d' bind()être null? Semble bien fonctionner dans FF.
Russ

7
Si la fonction a déjà cette limite, utiliser null comme premier argument de bind gâchera les choses (c'est-à-dire lier une méthode objet à la portée globale).
Imbue

1
Je ne sais vraiment pas ce que vous essayez de faire. Vous ne pouvez pas avoir thiscomplètement dissocié en JavaScript. Cela signifie toujours quelque chose . La liaison thisà la thisfonction englobante a donc beaucoup de sens.
kojiro

Je suis assez nouveau chez JS. J'ai édité la question pour la rendre plus claire. Il se peut que ce que je veux ne soit pas possible. Merci.
Imbue

Réponses:


32

Vous pouvez le faire, mais il vaut mieux éviter de le considérer comme "contraignant" car c'est le terme utilisé pour définir la valeur "this". Pensez-y peut-être à "envelopper" les arguments dans une fonction?

Ce que vous faites est de créer une fonction qui a les arguments souhaités intégrés via des fermetures:

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

J'espère que j'ai la syntaxe ici. Ce qu'il fait est de créer une fonction appelée withWrappedArguments () (pour être pédant, c'est une fonction anonyme assignée à la variable) que vous pouvez appeler à tout moment n'importe où et agira toujours avec actualArg1Value et actualArg2Value, et tout ce que vous voulez mettre dans Là. Vous pouvez également lui faire accepter d'autres arguments au moment de l'appel si vous le souhaitez. Le secret est les parenthèses après l'accolade de fermeture finale. Celles-ci provoquent l'exécution immédiate de la fonction externe, avec les valeurs passées, et la génération de la fonction interne qui peut être appelée ultérieurement. Les valeurs passées sont ensuite figées au moment où la fonction est générée.

C'est effectivement ce que fait bind, mais de cette façon, il est explicite que les arguments encapsulés sont simplement des fermetures sur des variables locales, et il n'est pas nécessaire de changer le comportement de cela.


16
Notez que cela s'appelle généralement "Currying".
M3D

@ M3D Capitalisation qui le rend si étrange.
Andrew le

Ce serait beaucoup plus utile si un exemple était fourni sur la façon de l'utiliser.
Andrew le

fr.wikipedia.org/wiki/Currying link pour quiconque est intéressé, bien que la version TLDR soit "Au lieu de f(x,y)nous voulons f(x)(y)ou g=f(x); g(y). Donc nous changerions f=(x,y)=>x+yen f=(x)=>(y)=>x+y."
M3D

26

Dans ES6, cela se fait facilement en utilisant les paramètres de repos en conjonction avec l' opérateur d'étalement .

Nous pouvons donc définir une fonction bindArgsqui fonctionne comme bind, sauf que seuls les arguments sont liés, mais pas le contexte ( this).

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

Ensuite, pour une fonction fooet un objet spécifiés obj, l'instruction

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

est équivalent à

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

où seuls les premier et deuxième arguments sont liés bar, tandis que le contexte objspécifié dans l'appel est utilisé et des arguments supplémentaires sont ajoutés après les arguments liés. La valeur de retour est simplement transmise.


11
apporte es6 à la table, utilise toujours var :(
Daniel Kobe

7
@DanielKobe Merci! Les opérateurs de propagation et les paramètres de repos ont été implémentés dans Firefox bien avant letet const, qui n'étaient pas encore disponibles au moment où j'ai publié ceci pour la première fois. Il est mis à jour maintenant.
GOTO 0

C'est génial et fonctionne pour les fonctions de rappel! Peut également être modifié pour ajouter des arguments plutôt que de les ajouter.
DenisM

1
Il convient de noter que cela crée une fermeture chaque fois que bindArgs () est appelé, ce qui signifie que les performances ne sont pas aussi bonnes que l'utilisation de la fonctionnalité native bind ().
Joshua Walsh

17

Dans la bindméthode native , la thisvaleur de la fonction result est perdue. Cependant, vous pouvez facilement recoder le shim commun pour ne pas utiliser d'argument pour le contexte:

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;
};

4
Je n'aime pas le fait que vous deviez jouer avec le prototype de Functionmais cela m'a fait gagner beaucoup de temps et des solutions de contournement horribles. Merci
jackdbernier

3
@jackdbernier: Vous n'êtes pas obligé, mais je trouve cela plus intuitif en tant que méthode comme bind. Vous pouvez facilement le transformer en unfunction bindArgs(fn){ …, args = slice.call(arguments, 1); …}
Bergi

@ Qantas94Heavy: C'est ce que vous devez toujours faire lorsque vous vous souciez thisde l'être a. Vous pouvez utiliser à la bindplace, bien sûr. Pourtant, l'OP ne se soucie explicitement pas de la thisvaleur et voulait une fonction partielle non liée.
Bergi

@Bergi: Je supposais que le but de ne pas définir thisest que la fonction utilise this, mais ils ne voulaient pas la lier à ce moment-là. De plus, la partie de la question du PO //These should both have exact same output.est contradictoire avec d'autres parties de celle-ci.
Qantas 94 Heavy le

1
@KubaWyrostek: Oui, c'est pour faire fonctionner les fonctions de constructeur partielles, un peu comme le sous-classement (pas que je le recommande, cependant). Un réel bindne fonctionne pas du tout avec les constructeurs, les fonctions liées n'en ont pas .prototype. Non,Function.prototype !== mymethod.prototype
Bergi

7

Encore une petite implémentation juste pour le plaisir:

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);
    };
}

Comment utiliser:

function onWriteEnd(evt) {}
var myPersonalWriteEnd = bindWithoutThis(onWriteEnd, "some", "data");

On dirait qu'il serait plus correct de passer "null" pour cela lors de l'appel de apply ()
eddiewould

6
var b = function() {
    return c(1,2,3);
};

3
Dans toutes ces réponses bruyantes et ambiguës, c'est une réponse précise et utile. À cet effet +1. Peut-être pourriez-vous affiner le style de la réponse pour la rendre plus attrayante à l'œil nu dans ce reste bruyant et fantaisiste de la jungle. const curry = (fn, ...curryArgs) => (...args) => fn(...curryArgs, args)
Joehannes

2

Il est un peu difficile de dire exactement ce que vous voulez faire en fin de compte car l'exemple est en quelque sorte arbitraire, mais vous voudrez peut-être vous pencher sur les partiels (ou 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(){})

Lien vers l'article de John Resig: http://ejohn.org/blog/partial-functions-in-javascript/


Cette fonction ne fonctionne pas, vous ne pouvez pas appeler une fonction partiellement appliquée plus d'une fois.
Bergi

Je ne suis pas sûr de suivre. Pouvez-vous élaborer un peu là-dessus ou publier un exemple jsbin?
Kevin Ennis

Voyez cet exemple . En outre, votre partialbesoin d'être appelé avec des undefinedarguments pour fonctionner - pas ce à quoi on pourrait s'attendre, et interrompu sur des fonctions qui prennent un nombre arbitraire de paramètres.
Bergi

Kevin, merci pour votre réponse. C'est proche de ce que je veux, mais je pense que l' thisobjet est toujours en désordre. Jetez un oeil à: jsbin.com/ifoqoj/4/edit
Imbue

Ohhh, gotchya. Cet exemple montre bien ce que vous recherchez.
Kevin Ennis

2

Peut-être voulez-vous lier la référence de ceci en dernier mais votre code: -

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

Déjà appliqué la liaison par exemple ceci et plus tard, vous ne pouvez pas le changer. Ce que je suggérerai d'utiliser la référence également comme paramètre comme celui-ci: -

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);

1

Utilisez 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);
}

Fondamentalement, la fonction à laquelle vous voulez passer des paramètres devient un proxy que vous pouvez appeler pour passer une variable, et il renvoie la fonction que vous voulez réellement faire.

J'espère que cela t'aides!


1

Un utilisateur anonyme a publié ces informations supplémentaires:

S'appuyant sur ce qui a déjà été fourni dans cet article - la solution la plus élégante que j'ai vue est de Curry vos arguments et votre contexte:

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']);

1

Eh bien pour l'exemple que vous avez donné, cela fera l'affaire

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

Si vous souhaitez garantir la clôture des paramètres:

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)

Mais si c'est le cas, vous devez simplement vous en tenir à votre solution:

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

C'est la meilleure façon.


1

Aussi simple que ça?

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

Solution plus générale:

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


1

En utilisant LoDash, vous pouvez utiliser la _.partialfonction.

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

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

pf(function () {})                // callback.

0

Pourquoi ne pas utiliser un wrapper autour de la fonction pour enregistrer ceci en tant que mythis?

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);

-1

jQuery 1.9 a apporté exactement cette fonctionnalité avec la fonction proxy.

À partir de jQuery 1.9, lorsque le contexte est nul ou non défini, la fonction mandatée sera appelée avec le même objet que celui avec lequel le proxy a été appelé. Cela permet d'utiliser $ .proxy () pour appliquer partiellement les arguments d'une fonction sans changer le contexte.

Exemple:

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

-5

Cas d'utilisation de Jquery:

au lieu:

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

utilisez ceci:

for(var i = 0;i<3;i++){
    $('<input>').appendTo('body').click(function(e){
        var i = this;
        $(e.originalEvent.target).val(i);
    }.bind(i));
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.