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!


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.


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.


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

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

Parece que seria mais correto passar "null" para isso ao invocar apply ()
— eddiewould

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

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/


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

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!


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

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.


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


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.

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

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

-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));
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.