RSS Feed

Modificando el contexto: call, apply y bind

5

enero 25, 2013 by - @pjnovas

Buenas!, hace muy poquito conocí el bind() y me dieron ganas de armar un post mostrando la diferencia entre los 3.

Antes de arrancar, te recomiendo que leas el post Patrones de Invocación de Funciones que habla sobre el bindeo del contexto en el famoso this, así puedo concentrarme en mostrarte la diferencia entre estos métodos.

Bueno, ahora que sabes esto del this y como se bindea su contexto, algo que es extremadamente útil a la hora de tener el control sobre las funciones o métodos que hacemos y utilizamos.

Como viste en el post mas arriba en el caso del apply lo que estamos haciendo es meterle un nuevo contexto a una función, por ejemplo:

var obj = {
  delta: 2,
  test: function(num1, num2){
     return (num1 + num2) * this.delta;
  }
};

var resultado = obj.test(2,2);
console.log(resultado); // (2 + 2) * 2 = 8

En este caso, simplemente tenemos un objeto con una propiedad y una función, al llamarla con 2 números, los sumamos y le aplicamos el delta de su contexto (en el cual está corriendo la función), que para este caso es el objeto en si mismo.

Pero que pasa si ahora queremos cambiarle ese contexto, un nuevo delta, definido por nosotros pero sin modificar el objeto…

apply()

var cambio = {
  delta: 5
};

var resultado = obj.test.apply(cambio, [2,2]);
console.log(resultado); // (2 + 2) * 5 = 20

Le aplicamos un nuevo contexto en el cual corre la función, por lo que ahora el this de obj va a contenter nuestro objeto cambio, por ende, el delta valdrá 5 al ejecutarse.
Y que es el array que sigue a la llamada?, esos son los argumentos que recibe la función:

  [funcion].apply([contexto], [parámetros como array]);

call()

Otra forma de llamar a una función aplicandole un contexto es con call, siguiendo con el ejemplo del apply:

var cambio = {
  delta: 5
};

var resultado = obj.test.call(cambio, 2, 2);
console.log(resultado); // (2 + 2) * 5 = 20

La única diferencia es la forma de pasar los argumentos, ya que en vez de que sea un array, simplemente toma el primero como el contexto y los siguientes son los argumentos en el orden que los esperamos.

  [funcion].call([contexto], [param1], [param2], [paramN]);

bind()

En este caso hay una vuelta de tuerca mas, que nos puede ser bastante útil.
El bind() no llama a la función con un nuevo contexto, sino que nos devuelve una referencia a la función con ese nuevo contexto:

var cambio = {
  delta: 5
};

var funcionConCambio = obj.test.bind(cambio);
var resultado = functionConCambio(2, 2);
console.log(resultado); // (2 + 2) * 5 = 20

Muy loco!, esto aplicado a callbacks puede ser muy interesante.
Usas el self, me o that?

Supongamos tenemos una llamada ajax y definimos un callback para cuando termina, un ejemplo común sería:

// más código donde utilizamos el this
var that = this;
function callback(datos){
  that.magia(datos);
}

ajax(callback);

Ahora con bind nos quedaría asi:

function callback(datos){
  this.magia(datos);
}

ajax(callback.bind(this));

Mucho más limpio!

Algo a tener en cuenta es que el metodo bind() aparece en el ECMAScript 5ta Edición, por lo que puede que los navegadores ancianitos no lo soporten.


  • http://twitter.com/leaocabrera Leandro Cabrera

    muy bueno, se puede adoptar para los navegadores ancianitos el bind / bindAll de Undescore.js

  • pelicanorojo

    underscore.js tiene una implementación de bind, y lo más interesante es que te permite hacer partial application, uno de los pasos necesarios para implementar el patron curry.

  • pelicanorojo

    Hola,

    Muy bueno el post, me dejó re manija, quería aportar un par de cosas extra para tener en cuenta:

    var
      obj1 = {
        prop1: 1,
        prop2: 2,
        metodo: function () {
          return this.prop1 / this.prop2;
        }
      },
      obj2 = {
        prop1: 1
      };
    
    obj1.metodo = obj1.metodo.bind( obj2 );
    
    console.log ( obj1.metodo() );
    

    La moraleja de este ejemplo es:
    – La función que vas a bindear, tenés que bindearla a objetos con una interface mínima compatible con la función,
    en el ejemplo, necesita dos propiedades prop1 y prop2 numéricas.

    Otros cuidados:
    Cuidado con los binds múltiples, si vas a bindear una función que ya está bindeada, fuiste, el segundo bind no surte efecto:
    ( esto pasa con bind nativos y de underscore por ejemplo )

    
    wrapper1  = obj1.metodo1.bind( obj2 );
    obj1.metodo1 = wrapper1;
    
    wrapper2  = obj1.metodo1.bind( obj3 );
    obj1.metodo1 = wrapper2;
    
    obj1.metodo1();
    

    Lo que pasa es lo siguiente se llama wrapper2 con contexto obj1, pero wrapper2 llama a wrapper1 con contexto obj3,

    pero wrapper1, debido al primer bind, llama a metodo1 ¿original? con el contexto obj2 que se le ha bindeado.

    En general un método para bindear debe ser diseñado para eso, porque necesita que el contexto que se le pase tenga una

    determinada interface mínima, porque se debe saber que solo el primer bind tiene “chances” de cambiar el contexto. Y el

    resultado del bindeo es mejor guardarlo en una nueva variable, sin pisar la que guardaba la función que se está bindeando,

    salvo que sepamos lo que hacemos, como acá

    
    obj1.metodo1 = obj1.metodo1.bind ( obj1 );
    
    

    el bindeo a si mismo, hace al método apto para ser usado en callbacks.

  • http://twitter.com/laas29 Alberto Caras

    bien!

  • Pingback: Jamie