Comprender el enlace de JavaScript ()
La vinculación de funciones es probablemente su menor preocupación al comenzar con JavaScript, pero cuando se da cuenta de que necesita una solución al problema de cómo mantener el contexto de "esto" dentro de otra función, es posible que no se dé cuenta de que lo que realmente necesita es Función. .prototipo.bind().
La vinculación de funciones es probablemente su menor preocupación al comenzar con JavaScript, pero cuando se da cuenta de que necesita una solución al problema de cómo mantener el contexto dentro de this
otra función, es posible que no se dé cuenta de que lo que realmente necesita es Function.prototype.bind()
.
Lecturas adicionales sobre SmashingMag:
- Lo que necesita saber sobre el alcance de JavaScript
- Una introducción a los eventos DOM
- 7 cosas de JavaScript que desearía saber mucho antes en mi carrera
- Cómo escribir JavaScript rápido y con memoria eficiente
La primera vez que se encuentre con el problema, es posible que se sienta inclinado a establecer this
una variable a la que pueda hacer referencia cuando cambie de contexto. Mucha gente opta por self
, _this
o en ocasiones context
como nombre de variable. Todos son utilizables y no hay nada de malo en hacerlo, pero existe una forma mejor y dedicada.
Jack Archibald tuitea sobre el almacenamiento en caché this
:
Ohhhh, haría cualquier cosa por el alcance, pero no haré eso = esto - Jake Archibald (@jaffathecake) 20 de febrero de 2013
Debería haber sido más evidente para mí cuando Sindre Sorhus lo explicó en detalle :
@benhowdle $this para jQuery, para JS simple no lo hago, uso .bind() — Sindre Sorhus (@sindresorhus) 22 de febrero de 2013
Ignoré este sabio consejo durante muchos meses.
¿Qué problema realmente buscamos resolver?
Aquí hay un código de muestra en el que se podría perdonar a alguien por almacenar en caché el contexto en una variable:
var myObj = { specialFunction: function () { }, anotherSpecialFunction: function () { }, getAsyncData: function (cb) { cb(); }, render: function () { var that = this; this.getAsyncData(function () { that.specialFunction(); that.anotherSpecialFunction(); }); }};myObj.render();
Si hubiéramos dejado nuestras llamadas a funciones como this.specialFunction()
, habríamos recibido el siguiente error:
Uncaught TypeError: Object [object global] has no method 'specialFunction'
Necesitamos mantener el contexto del myObj
objeto al que se hace referencia para cuando se llame a la función de devolución de llamada. Llamar that.specialFunction()
nos permite mantener ese contexto y ejecutar correctamente nuestra función. Sin embargo, esto podría solucionarse un poco usando Function.prototype.bind()
.
Reescribamos nuestro ejemplo:
render: function () { this.getAsyncData(function () { this.specialFunction(); this.anotherSpecialFunction(); }.bind(this));}
¿Qué acabamos de hacer?
Bueno, .bind()
simplemente crea una nueva función que, cuando se llama, tiene su this
palabra clave establecida en el valor proporcionado. Entonces, pasamos nuestro contexto deseado, this
(que es myObj
), a la .bind()
función. Luego, cuando se ejecuta la función de devolución de llamada, this
hace referencia myObj
.
Si está interesado en ver cómo Function.prototype.bind()
se vería y qué hace internamente, aquí tiene un ejemplo muy simple:
Function.prototype.bind = function (scope) { var fn = this; return function () { return fn.apply(scope); };}
Y aquí hay un caso de uso muy simple:
var foo = { x: 3}var bar = function(){ console.log(this.x);}bar(); // undefinedvar boundFunc = bar.bind(foo);boundFunc(); // 3
Hemos creado una nueva función que, cuando se ejecuta, se this
establece en foo
, no en el alcance global, como en el ejemplo donde llamamos bar();
.
Navegador | Soporte de versión |
---|---|
Cromo | 7 |
Firefox (Geco) | 4.0 (2) |
explorador de Internet | 9 |
Ópera | 11.60 |
Safari | 5.1.4 |
Como puede ver, desafortunadamente, Function.prototype.bind
no es compatible con Internet Explorer 8 y versiones anteriores, por lo que tendrá problemas si intenta usarlo sin un respaldo.
Afortunadamente, Mozilla Developer Network, siendo el maravilloso recurso que es, proporciona una alternativa sólida si el navegador no ha implementado el .bind()
método nativo:
if (!Function.prototype.bind) { Function.prototype.bind = function (oThis) { if (typeof this !== "function") { // closest thing possible to the ECMAScript 5 internal IsCallable function throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function () {}, fBound = function () { return fToBind.apply(this instanceof fNOP oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; };}
Patrones de uso
Cuando aprendo algo, me resulta útil no sólo aprender a fondo el concepto, sino también verlo aplicado a lo que estoy trabajando actualmente (o algo parecido). Con suerte, algunos de los ejemplos siguientes se pueden aplicar a su código o a los problemas que enfrenta.
Controladores de clic
Un uso es realizar un seguimiento de los clics (o realizar una acción después de un clic) que podría requerir que almacenemos información en un objeto, así:
var logger = { x: 0, updateCount: function(){ this.x++; console.log(this.x); }}
Podríamos asignar controladores de clic como este y posteriormente llamar a updateCount()
nuestro logger
objeto:
document.querySelector('button').addEventListener('click', function(){ logger.updateCount();});
Pero hemos tenido que crear una función anónima innecesaria para permitir que la this
palabra clave se mantenga correcta en la updateCount()
función.
Esto podría arreglarse, así:
document.querySelector('button').addEventListener('click', logger.updateCount.bind(logger));
Hemos utilizado la .bind()
función sutilmente útil para crear una nueva función y luego establecer el alcance que se vinculará al logger
objeto.
establecer tiempo de espera
Si alguna vez ha trabajado con motores de plantillas (como Manillar) o especialmente con ciertos marcos MV* (solo puedo hablar de Backbone.js por experiencia), entonces es posible que esté al tanto del problema que ocurre cuando renderiza la plantilla, pero desea acceder a los nuevos nodos DOM inmediatamente después de su llamada de renderizado.
Supongamos que intentamos crear una instancia de un complemento jQuery:
var myView = { template: '/* a template string containing our select / */', $el: $('#content'), afterRender: function () { this.$el.find('select').myPlugin(); }, render: function () { this.$el.html(this.template()); this.afterRender(); }}myView.render();
Es posible que descubra que funciona, pero no todo el tiempo. Ahí yace el problema. Es una carrera de ratas: lo que suceda para llegar primero, gana. A veces es el renderizado, a veces es la creación de instancias del complemento.
Ahora, sin que algunos lo sepan, podemos usar un ligero truco con setTimeout()
.
Con una ligera reescritura, podemos crear una instancia segura de nuestro complemento jQuery una vez que los nodos DOM estén presentes:
// afterRender: function () { this.$el.find('select').myPlugin(); }, render: function () { this.$el.html(this.template()); setTimeout(this.afterRender, 0); }//
Sin embargo, recibiremos el mensaje confiable de que .afterRender()
no se puede encontrar la función.
Lo que hacemos, entonces, es añadir nuestro .bind()
a la mezcla:
// afterRender: function () { this.$el.find('select').myPlugin(); }, render: function () { this.$el.html(this.template()); setTimeout(this.afterRender.bind(this), 0); }//
Ahora, nuestra afterRender()
función se ejecutará en el contexto correcto.
Enlace de eventos más ordenado con querySelectorAll
La API DOM mejoró significativamente una vez que incluyó métodos tan útiles como querySelector
y querySelectorAll
la classList
API, por nombrar algunos de los muchos.
Sin embargo, todavía no existe una forma de agregar eventos de forma nativa NodeList
. Entonces, terminamos robando la forEach
función del Array.prototype
bucle to, así:
Array.prototype.forEach.call(document.querySelectorAll('.klasses'), function(el){ el.addEventListener('click', someFunction);});
Sin embargo, podemos hacerlo mejor que eso con nuestro amigo .bind()
:
var unboundForEach = Array.prototype.forEach, forEach = Function.prototype.call.bind(unboundForEach);forEach(document.querySelectorAll('.klasses'), function (el) { el.addEventListener('click', someFunction);});
Ahora tenemos un método ordenado para recorrer nuestros nodos DOM.
Conclusión
Como puede ver, la ()
función de vinculación de JavaScript se puede incluir sutilmente para muchos propósitos diferentes, así como para ordenar el código existente. Esperamos que esta descripción general le haya brindado lo que necesita agregar .bind()
a su propio código (¡si es necesario!) y aprovechar el poder de transformar el valor de this
.
(un poco)Explora más en
- Codificación
- Herramientas
- javascript
- Técnicas
Deja un comentario