Como vincular argumentos de função sem vincular isso?

115

Em Javascript, como posso vincular argumentos a uma função sem vincular o thisparâmetro?

Por exemplo:

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

Como posso evitar o efeito colateral de ter que vincular o escopo da função (por exemplo, configuração this= nulo) também?

Editar:

Desculpe pela confusão. Quero vincular argumentos e, em seguida, ser capaz de chamar a função vinculada mais tarde e fazer com que ela se comporte exatamente como se eu chamasse a função original e passasse os 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?

Ainda sou novo nisso, desculpe a confusão.

Obrigado!

Imbuir
fonte
1
Seria inadequado apenas religar this? var b = c.bind(this, 1,2,3);
kojiro
Por que é problemático ter o primeiro valor de bind()ser null? Parece funcionar bem no FF.
Russ
7
Se a função já tiver esse limite, usar null como o primeiro argumento de bind irá bagunçar as coisas (ou seja, vincular um método de objeto ao escopo global).
Imbuir em
1
Eu realmente não estou certo do que você está tentando fazer. Você não pode ter thistotalmente desvinculado do JavaScript. Sempre significa alguma coisa . Portanto, vincular thisao thisda função envolvente faz muito sentido.
kojiro
Sou muito novo em JS. Editei a pergunta para torná-la mais clara. Pode ser que o que eu quero não seja possível. Obrigado.
Imbuir

Respostas:

32

Você pode fazer isso, mas é melhor evitar pensar nisso como "vinculação", pois esse é o termo usado para definir o valor "this". Talvez pense nisso como "agrupar" os argumentos em uma função?

O que você faz é criar uma função que tenha os argumentos desejados incorporados por meio de encerramentos:

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

Espero ter conseguido a sintaxe certa. O que ele faz é criar uma função chamada withWrappedArguments () (para ser pedante, é uma função anônima atribuída à variável) que você pode chamar a qualquer momento em qualquer lugar e sempre agirá com actualArg1Value e realArg2Value, e qualquer outra coisa que você queira inserir há. Você também pode fazer com que ele aceite outros argumentos no momento da chamada, se desejar. O segredo são os parênteses após a chave de fechamento final. Isso faz com que a função externa seja executada imediatamente, com os valores passados, e gere a função interna que pode ser chamada posteriormente. Os valores passados ​​são congelados no momento em que a função é gerada.

Isso é efetivamente o que o bind faz, mas desta forma é explícito que os argumentos encapsulados são simplesmente encerramentos em variáveis ​​locais, e não há necessidade de alterar o comportamento disso.

Ian Nartowicz
fonte
16
Observe que isso geralmente é chamado de "Currying".
M3D
@ M3D Capitalização que faz com que pareça tão estranho.
Andrew
Isso seria muito mais útil se fosse fornecido um exemplo de como usá-lo.
Andrew
en.wikipedia.org/wiki/Link apressado para quem estiver interessado, embora a versão TLDR seja "Em vez de f(x,y)queremos f(x)(y)ou g=f(x); g(y). Portanto, mudaríamos f=(x,y)=>x+ypara f=(x)=>(y)=>x+y."
M3D
26

No ES6, isso é feito facilmente usando parâmetros de descanso em conjunto com o operador de espalhamento .

Portanto, podemos definir uma função bindArgsque funciona como bind, exceto que apenas os argumentos são vinculados, mas não o contexto ( this).

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

Então, para uma função especificada fooe um objeto obj, a instrução

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

é equivalente a

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

onde apenas o primeiro e o segundo argumentos são vinculados bar, enquanto o contexto objespecificado na invocação é usado e argumentos extras são acrescentados após os argumentos vinculados. O valor de retorno é simplesmente encaminhado.

GOTO 0
fonte
11
traz es6 para a mesa, ainda usa var :(
Daniel Kobe
7
@DanielKobe Obrigado! Operadores de propagação e parâmetros de descanso foram implementados no Firefox muito antes lete const, ainda não estavam disponíveis quando eu postei isto pela primeira vez. Ele está atualizado agora.
GOTO 0
Isso é ótimo e funciona para funções de retorno de chamada! Também pode ser alterado para anexar argumentos em vez de anexá-los.
DenisM
1
É importante notar que isso cria um encerramento cada vez que bindArgs () é chamado, o que significa que o desempenho não é tão bom quanto usar a funcionalidade bind () nativa.
Joshua Walsh
17

No bindmétodo nativo , o thisvalor na função de resultado é perdido. No entanto, você pode facilmente recodificar o shim comum para não usar um argumento para o 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
fonte
4
Não gosto do fato de você ter que brincar com o protótipo do, Functionmas me economizou muito tempo e algumas soluções alternativas. Obrigado
jackdbernier
3
@jackdbernier: Você não precisa, mas acho que é mais intuitivo como método bind. Você pode facilmente transformá-lo em umfunction bindArgs(fn){ …, args = slice.call(arguments, 1); …}
Bergi
@ Qantas94Heavy: Isso é o que você sempre tem que fazer quando quer thisser a. Você pode usar em bindvez disso, é claro. No entanto, o OP explicitamente não se preocupa com othis valor e deseja uma função parcial não vinculada.
Bergi
@Bergi: Eu estava presumindo que o propósito de não definir this é que a função usa this, mas eles não queriam vinculá-la naquele momento. Além disso, a parte na pergunta do OP //These should both have exact same output.é contraditória com outras partes dele.
Qantas 94 Heavy de
1
@KubaWyrostek: Sim, é para fazer as funções construtoras parciais funcionarem, meio como subclasses (embora eu não recomende isso). Um real bindnão funciona com construtores, as funções associadas não têm .prototype. Não,Function.prototype !== mymethod.prototype
Bergi
7

Mais uma pequena implementação apenas por diversão:

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

Como usar:

function onWriteEnd(evt) {}
var myPersonalWriteEnd = bindWithoutThis(onWriteEnd, "some", "data");
Dmitrii Sorin
fonte
Parece que seria mais correto passar "null" para isso ao invocar apply ()
eddiewould
6
var b = function() {
    return c(1,2,3);
};
Mike
fonte
3
Em todas essas respostas barulhentas e ambíguas, esta é uma resposta precisa e útil. Portanto, +1. Talvez você pudesse refinar o estilo da resposta para torná-la mais atraente a olho nu naquele resto chique e barulhento da selva. const curry = (fn, ...curryArgs) => (...args) => fn(...curryArgs, args)
Joehannes de
2

É um pouco difícil dizer exatamente o que você quer fazer porque o exemplo é meio arbitrário, mas você pode querer dar uma olhada em parciais (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(){})

Link para o artigo de John Resig: http://ejohn.org/blog/partial-functions-in-javascript/

Kevin Ennis
fonte
Esta função não funciona, você não pode chamar uma função parcialmente aplicada mais de uma vez.
Bergi
Não tenho certeza se estou entendendo. Você pode elaborar um pouco sobre isso ou postar um exemplo de jsbin?
Kevin Ennis
Veja este exemplo . Além disso, você partialprecisa ser chamado com undefinedargumentos para funcionar - não o que se esperaria, e interrompido em funções que usam um número arbitrário de parâmetros.
Bergi
Kevin, obrigado pela sua resposta. Está perto do que eu quero, mas acho que o thisobjeto ainda fica bagunçado. Dê uma olhada em: jsbin.com/ifoqoj/4/edit
Imbue
Ohhh, entendi. Esse exemplo deixa muito mais claro o que você está procurando.
Kevin Ennis
2

Pode ser que você queira vincular a referência deste no último, mas seu código: -

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

Já aplicou vinculação por exemplo esta e posteriormente você não pode alterá-la. O que vou sugerir é que use a referência também como um 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
fonte
1

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

Basicamente, a função para a qual você deseja passar parâmetros se torna um proxy que você pode chamar para passar uma variável, e retorna a função que você realmente deseja fazer.

Espero que isto ajude!

Cody
fonte
1

Um usuário anônimo postou esta informação adicional:

Com base no que já foi fornecido neste post - a solução mais elegante que vi é curry seus argumentos e 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
fonte
1

Bem, para o exemplo que você deu, isso servirá

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

Se você deseja garantir o fechamento dos 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)

Mas se for assim, você deve apenas se ater à sua solução:

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

É a melhor maneira.

Joseph Garrone
fonte
1

Simples assim?

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

Solução mais geral:

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
fonte
1

Usando o LoDash, você pode usar a _.partialfunção.

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
fonte
0

Por que não usar um invólucro em torno da função para salvá-la como 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);
Olivier Archer
fonte
-1

O jQuery 1.9 trouxe exatamente esse recurso com a função proxy.

A partir do jQuery 1.9, quando o contexto é nulo ou indefinido, a função proxy será chamada com o mesmo objeto com que o proxy foi chamado. Isso permite que $ .proxy () seja usado para aplicar parcialmente os argumentos de uma função sem alterar o contexto.

Exemplo:

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

Caso de uso do Jquery:

em vez de:

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 isto:

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

fonte