Come associare gli argomenti della funzione senza associarlo?


115

In Javascript, come posso associare argomenti a una funzione senza associare il thisparametro?

Per esempio:

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

Come posso evitare l'effetto collaterale di dover vincolare anche l'ambito della funzione (ad es. Impostazione this= null)?

Modificare:

Dispiace per la confusione. Voglio associare gli argomenti, quindi essere in grado di chiamare la funzione associata in un secondo momento e farla comportare esattamente come se chiamassi la funzione originale e le passassi gli argomenti vincolati:

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?

Sono ancora nuovo in questo, scusa per la confusione.

Grazie!


1
Sarebbe inadeguato solo rilegare this? var b = c.bind(this, 1,2,3);
kojiro

Perché è problematico avere il primo valore di bind()essere null? Sembra funzionare bene in FF.
Russ

7
Se la funzione ha già questo limite, l'uso di null come primo argomento di bind rovinerà le cose (cioè vincolerà un metodo oggetto all'ambito globale).
Imbue il

1
Non sono davvero chiaro cosa stai cercando di fare. Non puoi avere thiscompletamente unbound in JavaScript. Significa sempre qualcosa . Quindi l'associazione thisalla thisfunzione di inclusione ha molto senso.
kojiro

Sono abbastanza nuovo in JS. Ho modificato la domanda per renderla più chiara. Può essere che quello che voglio non sia possibile. Grazie.
Imbue

Risposte:


32

Puoi farlo, ma è meglio evitare di considerarlo "vincolante" poiché questo è il termine usato per impostare il valore "this". Forse pensarlo come "avvolgere" gli argomenti in una funzione?

Quello che fai è creare una funzione che ha gli argomenti desiderati incorporati tramite chiusure:

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

Spero di aver capito la sintassi proprio lì. Quello che fa è creare una funzione chiamata withWrappedArguments () (per essere pedanti è una funzione anonima assegnata alla variabile) che puoi chiamare in qualsiasi momento in qualsiasi luogo e agirà sempre con actualArg1Value e actualArg2Value e qualsiasi altra cosa tu voglia inserire Là. Puoi anche fare in modo che accetti ulteriori argomenti al momento della chiamata, se lo desideri. Il segreto sono le parentesi dopo l'ultima parentesi graffa di chiusura. Questi fanno sì che la funzione esterna venga eseguita immediatamente, con i valori passati, e generi la funzione interna che può essere chiamata in seguito. I valori passati vengono quindi congelati al momento della generazione della funzione.

Questo è effettivamente ciò che fa bind, ma in questo modo è esplicito che gli argomenti avvolti sono semplicemente chiusure su variabili locali, e non c'è bisogno di cambiare il comportamento di questo.


16
Nota che questo di solito è chiamato "Currying".
M3D

@ M3D L'uso delle maiuscole lo fa sembrare così strano.
Andrew

Ciò sarebbe molto più utile se fosse fornito un esempio di come usarlo.
Andrew

en.wikipedia.org/wiki/Currying per chiunque sia interessato, sebbene la versione TLDR sia "Invece di f(x,y)vogliamo f(x)(y)o g=f(x); g(y). Quindi cambieremo f=(x,y)=>x+yin f=(x)=>(y)=>x+y".
M3D

26

In ES6, ciò può essere fatto facilmente utilizzando i parametri di riposo insieme all'operatore di spargimento .

Quindi possiamo definire una funzione bindArgsche funziona come bind, tranne che solo gli argomenti sono vincolati, ma non il context ( this).

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

Quindi, per una funzione specificata fooe un oggetto obj, l'istruzione

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

è equivalente a

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

dove sono vincolati solo il primo e il secondo argomento bar, mentre objviene utilizzato il contesto specificato nell'invocazione e gli argomenti extra vengono aggiunti dopo gli argomenti associati. Il valore restituito viene semplicemente inoltrato.


11
porta es6 al tavolo, usa ancora var :(
Daniel Kobe

7
@DanielKobe Grazie! Gli operatori di diffusione e i parametri di riposo sono stati implementati in Firefox molto tempo prima lete const, che non erano ancora disponibili quando l'ho pubblicato per la prima volta. Adesso è aggiornato.
GOTO 0

Questo è ottimo e funziona per le funzioni di callback! Può anche essere modificato per aggiungere argomenti anziché anteporli.
DenisM

1
Vale la pena notare che questo crea una chiusura ogni volta che viene chiamato bindArgs (), il che significa che le prestazioni non sono buone come l'utilizzo della funzionalità nativa bind ().
Joshua Walsh

17

Nel bindmetodo nativo il thisvalore nella funzione risultato viene perso. Tuttavia, puoi facilmente ricodificare lo shim comune per non utilizzare un argomento per il contesto:

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
Non mi piace il fatto che devi giocare con il prototipo di, Functionma mi ha fatto risparmiare un sacco di tempo e alcune brutte soluzioni alternative. Grazie
jackdbernier

3
@jackdbernier: non è necessario, ma lo trovo più intuitivo come metodo come bind. Puoi facilmente trasformarlo in unfunction bindArgs(fn){ …, args = slice.call(arguments, 1); …}
Bergi

@ Qantas94Heavy: Questo è quello che hai sempre da fare quando si preoccupano thisdi essere a. Potresti usare bindinvece, ovviamente. Tuttavia, l'OP non si preoccupa esplicitamente del thisvalore e voleva una funzione parziale non vincolata.
Bergi

@Bergi: presumevo che lo scopo di non impostare thisfosse che la funzione utilizza this, ma non volevano vincolarla in quel momento. Inoltre, la parte nella domanda dell'OP //These should both have exact same output.è contraddittoria con altre parti di essa.
Qantas 94 Heavy

1
@KubaWyrostek: Sì, è per far funzionare le funzioni di costruzione parziali, un po 'come la sottoclasse (non che io lo raccomandi, però). Un effettivo bindnon funziona affatto con i costruttori, le funzioni associate non hanno .prototype. No,Function.prototype !== mymethod.prototype
Bergi

7

Un'altra piccola implementazione solo per divertimento:

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

Come usare:

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

Sembra che sarebbe più corretto passare "null" per questo quando si richiama apply ()
eddiewould

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

3
In tutte queste risposte rumorose e ambigue questa è una risposta precisa e utile. Quindi +1. Forse potresti affinare lo stile della risposta per renderla più attraente ad occhio nudo in quel resto rumoroso e stravagante della giungla. const curry = (fn, ...curryArgs) => (...args) => fn(...curryArgs, args)
Joehannes

2

È un po 'difficile dire esattamente cosa vuoi fare alla fine perché l'esempio è un po' arbitrario, ma potresti voler esaminare i parziali (o il 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(){})

Link all'articolo di John Resig: http://ejohn.org/blog/partial-functions-in-javascript/


Questa funzione non funziona, non puoi chiamare una funzione parzialmente applicata più di una volta.
Bergi

Non sono sicuro di seguirlo. Puoi approfondirlo un po 'o pubblicare un esempio jsbin?
Kevin Ennis

Vedi questo esempio . Inoltre, devi partialessere chiamato con undefinedargomenti per funzionare, non quello che ci si aspetterebbe, e rotto su funzioni che accettano un numero arbitrario di parametri.
Bergi

Kevin, grazie per la tua risposta. È vicino a quello che voglio, ma penso che l' thisoggetto sia ancora incasinato. Dai un'occhiata a: jsbin.com/ifoqoj/4/edit
Imbue

Ohhh, gotchya. Questo esempio rende molto più chiaro ciò che stai cercando.
Kevin Ennis

2

Potresti voler associare il riferimento a questo in ultimo ma il tuo codice: -

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

È già stato applicato l'associazione ad esempio questo e successivamente non è possibile modificarlo. Quello che suggerirò di utilizzare il riferimento anche come parametro come questo: -

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

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

Fondamentalmente, la funzione a cui vuoi passare i parametri diventa un proxy che puoi chiamare per passare una variabile, e restituisce la funzione che vuoi effettivamente fare.

Spero che questo ti aiuti!


1

Un utente anonimo ha pubblicato queste informazioni aggiuntive:

Basandosi su ciò che è già stato fornito in questo post, la soluzione più elegante che ho visto è quella di curare i tuoi argomenti e il contesto:

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

Bene, per l'esempio che hai fornito, questo andrà bene

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

Se vuoi garantire l'inclusione dei parametri:

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)

Ma se è così dovresti limitarti alla tua soluzione:

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

È il modo migliore.


1

Semplice così?

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

Soluzione più generale:

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

Usando LoDash puoi usare la _.partialfunzione.

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

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

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

0

Perché non utilizzare un wrapper attorno alla funzione per salvarlo come 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 ha portato esattamente quella caratteristica con la funzione proxy.

A partire da jQuery 1.9, quando il contesto è nullo o non definito, la funzione proxy verrà chiamata con lo stesso oggetto con cui è stato chiamato il proxy. Ciò consente di utilizzare $ .proxy () per applicare parzialmente gli argomenti di una funzione senza modificare il contesto.

Esempio:

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

-5

Caso d'uso di Jquery:

anziché:

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

Usa questo:

for(var i = 0;i<3;i++){
    $('<input>').appendTo('body').click(function(e){
        var i = this;
        $(e.originalEvent.target).val(i);
    }.bind(i));
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.