El método bind en Javascript: teoría, ejemplos y usos extremos

27 Jun 2016

Introducción

Function.prototype.bind() es un método del objeto Function creado para manipular el valor contextual de this. Cuando se ejecuta sobre una función dada, creamos una nueva función que nos permite manipular tanto su valor this como los parámetros que espera.

Los usos más extremos de este pseudo constructor, permiten además clonar objetos y sobreescribir tanto sus propiedades como métodos nativos.

¡Vamos a verlo todo!

Teoría rápida y ejemplos básicos

La forma más directa de presentar una función o método es siempre mostrar su definión:

fn.bind( thisValue [, arg1 [, arg2 [, ... ] ] ] );

Y ahora toca explirlo: como podemos ver ahí arriba, bind se aplica sobre una función dada (fn) y soporta varios argumentos: el primero, será el valor que queramos asignar a this, el resto, serán aquellos parámetros que sobreescribirán a los parámetros de la función sobre la que estamos trabajando (fn).

Ejemplo rápido:

var fn = function ( param1, param2 ) {
    console.info( this, param1, param2 );
}
 
var newFn = fn.bind( console, 'param1Fixed' );
 
fn( 'Hello', 'World' ); // Window Hello World
newFn( 'Goodbye', 'Lenin' ); // Object { /* console*/ } param1Fixed Goodbye

Qué ha pasado ahí? Creo que se puede seguir sin problemas, pero por si acaso, lo explicamos:

fn( 'Hello', 'World' ); // Window Hello World

Ahí, hemos llamado a la función original y ésta pinta en la consola el valor de this (en este caso el objeto general Window), y los dos parámetros que le han llegado. Nada especial por ahora.

newFn( 'Goodbye', 'Lenin' ); // Object { /* console*/ } param1Fixed Goodbye

Esta llamada es sobre la nueva función que hemos creado con bind, newFn(). Al definirla, hemos indicado con el primer parámetro que el valor de this será ‘console‘, un objeto que tenemos presente en el navegador. Podríamos haber puesto cualquier otro que tuviéramos definido, o incluso un objeto vació:

var newFn = fn.bind( anotherFn, 'param1Fixed' );
var newFn = fn.bind( {}, 'param1Fixed' );
var newFn = fn.bind( undefined, 'param1Fixed' );

NOTA: Si pasamos como valor ‘undefined‘, éste es ignorado y el valor de this no se altera.

El segundo parámetro que hemos indicado al crear la función, será el que sobreescriba -o fije- el primer argumento de la función original. De ahí que se pinte en la consola.

Obsérvese que el segundo parámetro que hemos usado al invocar a la función (Lenin) no aparece porque directamente no se recoje en la nueva función. El ‘slot’ param1 ya lo habíamos fijado, y el param2 es el primero de nuestra llamada. Como en cualquier otra función, el resto de parámetros con el que llamemos a esa función será ignorado (al margen, claro, del objeto arguments y de los parámetros de arrastre).

NOTA: Una cuestión importante es que el valor de this asignado a la nueva función, no puede ser sobreescrito por la función original. Esa es la clave que nos da control total sobre su contexto.

El problema de la extracción de métodos

Cuando en Javascript utilizamos los métodos de un objeto como funciones, this hace referencia a su contexto:

var counter = {
    count: 0,
 
    // Method
    increment: function () {
        'use strict';
        return ++this.count; // 'this' refers to the 'counter' object
    }
}
 
console.info( counter.increment() ); // 1
console.info( counter.increment() ); // 2
console.info( counter.increment() ); // 3
console.info( counter.increment() ); // 4
console.info( counter.increment() ); // 5

No obstante, si recuperamos el valor de un método, en lugar de ejecutarlo, lo convertimos en una función y perdemos la contextualidad del valor de this.

Una prueba de esto sería por ejemplo el registro de eventos. Podemos simularlo con una función a modo ilustrativo:

var syncCall = function ( fn ) {
    return fn();
}

Si extraemos con esta función el método increment anterior, podremos comprobar cómo se pierde la referencia de su contexto:

syncCall( counter.increment ); // this is undefined

NOTA: Si eliminamos la instrucción ‘use strict‘ de nuestro método ‘increment‘, la función aplicaría el incremento a undefined, dando como resultado NaN:

var counter = {
    count: 0,
 
    increment: function () {
        // No strict mode!
        return ++this.count;
    }
}
 
var syncCall = function ( fn ) {
    return fn();
}
 
console.info( syncCall( counter.increment ) ); // NaN
 
// Testing
console.info( ++undefined ); // NaN

Volviendo al modo estricto, aunque this en nuestro método increment debería estar apuntando al contexto del objeto counter, cuando extraemos esa función la referencia se traslada al objeto global, no a counter.

Para solucionarlo, necesitamos asociar de nuevo el contexto y, para ello, bind nos permite vincular el objeto con esta nueva función que acabamos de crear:

console.info( syncCall( counter.increment.bind( counter ) ) ); // 1
console.info( syncCall( counter.increment.bind( counter ) ) ); // 2
console.info( syncCall( counter.increment.bind( counter ) ) ); // 3
console.info( syncCall( counter.increment.bind( counter ) ) ); // 4
console.info( syncCall( counter.increment.bind( counter ) ) ); // 5

Y de este modo, sí funciona.

Handlers -manejadores-

Un uso interesante de bind nos permite por ejemplo ahorrarnos las funciones anónimas que solemos asociar a eventos, o a cualquier función cuyos parámetros funcionan como un callback.

Por ejemplo, un caso típico:

var ele = $( '#myEle' );
 
ele.on( 'click', function () {
    console.info( this );
} );

Ese tipo de estructuras son muy frecuentes cuando trabajamos con el navegador. Sin embargo, gracias a bind, podemos ir un paso más allá y asociar directamente nuestra función al contexto:

ele.on( 'click', console.info.bind( console ) );

Eso nos permite un manejador de eventos de grano muy fino. Por supuesto, esto funcionaría con cualquier función que hayamos definido previamente, lo que de nuevo nos ejemplifica el escenario que vimos antes:

var logger = {
    clickCounter: 0,
    addClick: function (){
        console.info( ++this.clickCounter );
    }
}
 
var ele = $( '#myEle' );
ele.on( 'click', logger.addClick.bind( logger ) );

Tendríamos ahí la arquitectura básica de un logger que registra y guarda el número de clicks que recibe un elemento concreto del DOM. Efectivamente, aunque podríamos obtener el mismo comportamiento con una función anónima, éste modo es más limpio y elegante…

Aplicaciones parciales

Las aplicaciones parciales son un buen ejemplo para demostrar el uso del resto de argumentos cuando invocamos a bind.

Tomemos nuestra siempre socorrida función de suma, el ejemplo más simple de Función Pura.

function add ( x, y ) {
    return x + y;
}
 
console.info( add( 2, 5 ) ); // 7

Si queremos convertirla en una aplicación parcial fijando el primer parámetro (x), solo tenemos que crear una nueva función y asignar el valor deseado con bind.

var plus1 = add.bind( undefined, 1 );
 
console.info( plus1( 5 ) ); // 6

NOTA: Como en este caso el valor de ‘this’ no nos importa, pasamos ‘undefined’ como primer argumento. Ya hemos comentado que, en este caso, no se altera el valor referencial del contexto.

Rest parameters o argumentos de arrastre

Los parámetros de arrastre también se pueden aplicar a esta construcción haciendo que las aplicaciones parciales sean aún más flexibles.

Veamos el ejemplo de una función que concatena n cadenas utilizando un guión medio como separador:

var concatenate = function ( ...str ) {
    return str.join( '-' );
};
 
console.info( concatenate( 'la', 'donna', 'e', 'mobile' ) );
// la-donna-e-mobile

Usemos ahora el método bind para fijar algunos de estos argumentos gracias al arrastre:

var concatenatePrimaryLog = concatenate.bind( undefined, 'PRIMARY', 'LOGGER' );
 
console.info( concatenatePrimaryLog( 'cual', 'piuma', 'al', 'vento' ) );
// PRIMARY-LOGGER-cual-piuma-al-vento

Sobreescrituras en modo hardcore con bind()

Personalmente, una de las posibilidades con más implicaciones de bind es la de clonar un objeto dado sobreescribiendo en tiempo de ejecución sus propiedades. Esto es algo similar (o extensible) del apartado anterior con las aplicaciones parciales, pero con un mayor grado de flexibilidad.

Reescribamos por ejemplo la función anterior ‘concatenate’ con un formato de objeto:

var strHelper = {
    separator: '-',
 
    concatenate: function ( ...str ) {
        return str.join( this.separator );
    }
};
 
console.info( strHelper.concatenate( 'la', 'donna', 'e', 'mobile' ) );
// la-donna-e-mobile

La cuestión aquí es que, por la arquitectura del objeto, el caracter que utilizamos para concatenar está escrito a fuego como valor de la propiedad ‘separator’. Si necesitáramos cambiarlo por otro caracter, por ejemplo un guión bajo, podríamos crear una nueva función y utilizar bind para sobreescribir el valor necesario en la original:

var newSeparator = {
    separator: '_'
};
 
var concatenateLowDash = strHelper.concatenate.bind( newSeparator );
 
console.info( concatenateLowDash( 'la', 'donna', 'e', 'mobile' ) );
// la_donna_e_mobile

Esto, que es solo un ejemplo simple, da una pista de que las modificaciones pueden ser más complejas:

var newSeparator = {
    separator: ( function () {
        /* Do exotic things here... */
 
        return '*';
    } ) ()
};

Poderoso; si el objeto inicial está diseñado para soportar este tipo de comportamiento y sacarle partido, se pueden hacer malabares en tiempo de ejecución, definiendo nuevas funciones en caliente que sobreescriben las propiedades originales según sea necesario.

Creando atajos sintácticos

Con el método bind, es sencillo crear nuevas funciones en Javascript que representen atajos sintácticos de otras nativas (alias). Por ejemplo, si queremos un alias de la función Math.max() que nos devuelve el mayor de los valores dados, podemos crearla del siguiente modo:

var max = Function.prototype.call.bind( Math.max );
 
console.info( max( 10, 20, 15, 25, 5 ) ); // 25

Conclusión

En este artículo hemos visto como el método bind permite a partir de funciones dadas, crear otras nuevas donde podemos asignar libremente el valor contextual de this. Del mismo modo, podemos también fijar el valor de los parámetros que la función original espera, abriendo así el camino a las aplicaciones parciales y a la programación más funcional.

Cuando forzamos un poco su definición, conseguimos clonar objetos y sobreescribir tanto sus propiedades y métodos, además de facilitar implementar handlers de un modo más declarativo y elegante.

Con el nuevo estándar, ES6 (aka ECMAScript2015), las Funciones Flecha se han presentado como unas sustitutas perfectas de estas estructuras. Pero ese es otro tema y lo veremos en un próximo artículo.

Más:

{3} Comentarios.

  1. Corrección De Errores

    Sea…

    var ele = $( ‘#myEle’ );

    Si haces…

    ele.on( ‘click’, function () { console.info( this ); } );

    …obtendrás…

    >

    Pero si haces…

    ele.on( ‘click’, console.info.bind( console ) );

    …obtendrás…

    > Object { originalEvent: click, type: «click», isDefaultPrevented: qa(), timeStamp: 792946.2422257826, jQuery1124041810278367746867: true, toElement: undefined, screenY: 454, screenX: 362, pageY: 8942, pageX: 242, 27 more… }

  2. Ever

    hola, revisando en developer tools, veo que la nota que pusiste

    » Si pasamos como valor ‘undefined‘, éste es ignorado y el valor de this no se altera. »

    Pero al darle valor ‘undefined‘ el ‘this‘ se vuelve window.

    • Carlos Benítez

      Tengo que comprobarlo, pero es muy posible que haya cambiado la implementación en los navegadores… en un lenguaje tan vivo como lo es Javascript, no es raro que pasen estas cosas 😀

      Gracias por el aviso!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *