Cómo entender la palabra clave esto y el contexto en JavaScript

Como mencioné en uno de mis artículos anteriores, dominar JavaScript por completo puede ser un viaje largo. Es posible que se haya encontrado thisen su viaje como desarrollador de JavaScript. Cuando comencé, lo vi por primera vez al usar eventListenersy con jQuery. Más tarde, tuve que usarlo a menudo con React y estoy seguro de que tú también lo hiciste. Eso no significa que realmente entendiera qué es y cómo tomar el control por completo.

Sin embargo, es muy útil dominar el concepto que hay detrás, y cuando se lo aborda con la mente clara, tampoco es muy difícil.

Profundizando en esto

Explicar thispuede generar mucha confusión, simplemente nombrando la palabra clave.

thisestá estrechamente acoplado al contexto en el que se encuentra, en su programa. Empecemos desde arriba. En nuestro navegador, si escribe thisen la consola, obtendrá el window-object, el contexto más externo para su JavaScript. En Node.js, si lo hacemos:

console.log(this)

terminamos con {}un objeto vacío. Esto es un poco extraño, pero parece que Node.js se comporta de esa manera. Si lo haces

(function() { console.log(this); })();

sin embargo, recibirá el globalobjeto, el contexto más externo. En ese contexto setTimeout, setIntervalse almacenan. Siéntete libre de jugar un poco con él para ver qué puedes hacer con él. A partir de aquí, casi no hay diferencia entre Node.js y el navegador. Estaré usando window. Solo recuerde que en Node.js será el globalobjeto, pero realmente no hace una diferencia.

Recuerde: el contexto solo tiene sentido dentro de las funciones

Imagina que escribes un programa sin anidar nada en funciones. Simplemente escribiría una línea tras otra, sin bajar estructuras específicas. Eso significa que no tiene que hacer un seguimiento de dónde se encuentra. Siempre estás al mismo nivel.

Cuando empiece a tener funciones, es posible que tenga diferentes niveles de su programa y thisrepresente dónde está, qué objeto llamó a la función.

Seguimiento del objeto de la llamada

Echemos un vistazo al siguiente ejemplo y veamos cómo thiscambia según el contexto:

const coffee = { strong: true, info: function() { console.log(`The coffee is ${this.strong ? '' : 'not '}strong`) }, } coffee.info() // The coffee is strong

Dado que llamamos a una función que está declarada dentro del coffeeobjeto, nuestro contexto cambia exactamente a ese objeto. Ahora podemos acceder a todas las propiedades de ese objeto a través de this. En nuestro ejemplo anterior, también podríamos hacer referencia a él directamente haciendo coffee.strong. Se vuelve más interesante, cuando no sabemos en qué contexto, en qué objeto estamos o cuando las cosas simplemente se vuelven un poco más complejas. Eche un vistazo al siguiente ejemplo:

const drinks = [ { name: 'Coffee', addictive: true, info: function() { console.log(`${this.name} is ${this.addictive ? '' : 'not '} addictive.`) }, }, { name: 'Celery Juice', addictive: false, info: function() { console.log(`${this.name} is ${this.addictive ? '' : 'not '} addictive.`) }, }, ] function pickRandom(arr) { return arr[Math.floor(Math.random() * arr.length)] } pickRandom(drinks).info()

Clases e instancias

Las clases se pueden utilizar para abstraer su código y compartir comportamientos. infoNo es bueno repetir siempre la declaración de función en el último ejemplo. Dado que las clases y sus instancias son de hecho objetos, se comportan de la misma manera. Sin embargo, una cosa a tener en cuenta es que declarar thisen el constructor en realidad es una predicción para el futuro, cuando habrá una instancia.

Vamos a ver:

class Coffee { constructor(strong) { this.strong = !!strong } info() { console.log(`This coffee is ${this.strong ? '' : 'not '}strong`) } } const strongCoffee = new Coffee(true) const normalCoffee = new Coffee(false) strongCoffee.info() // This coffee is strong normalCoffee.info() // This coffee is not strong

Dificultad: llamadas a funciones anidadas sin problemas

A veces, terminamos en un contexto que realmente no esperábamos. Esto puede suceder cuando, sin saberlo, llamamos a la función dentro de otro contexto de objeto. Un ejemplo muy común es cuando se usa setTimeouto setInterval:

// BAD EXAMPLE const coffee = { strong: true, amount: 120, drink: function() { setTimeout(function() { if (this.amount) this.amount -= 10 }, 10) }, } coffee.drink()

¿Qué crees que coffee.amountes?

...

..

.

Todavía lo es 120. Primero, estábamos dentro del coffeeobjeto, ya que el drinkmétodo está declarado dentro de él. Simplemente lo hicimos setTimeouty nada más. Eso es exactamente.

Como expliqué anteriormente, el setTimeoutmétodo se declara realmente en el windowobjeto. Al llamarlo, en realidad cambiamos el contexto a la de windownuevo. Eso significa que nuestras instrucciones en realidad intentaron cambiar window.amount, pero terminaron sin hacer nada debido a la ifdeclaración-. Para encargarnos de eso, tenemos bindnuestras funciones (ver más abajo).

Reaccionar

Usando React, esto será algo del pasado pronto, gracias a Hooks. Por el momento, todavía tenemos que hacer bindtodo (más sobre eso más adelante) de una forma u otra. Cuando comencé, no tenía idea de por qué lo estaba haciendo, pero en este punto, ya deberías saber por qué es necesario.

Echemos un vistazo a dos componentes simples de la clase React:

// BAD EXAMPLE import React from 'react' class Child extends React.Component { render() { return  Get some Coffee!  } } class Parent extends React.Component { constructor(props) { super(props) this.state = { coffeeCount: 0, } // change to turn into good example – normally we would do: // this._getCoffee = this._getCoffee.bind(this) } render() { return (    ) } _getCoffee() { this.setState({ coffeeCount: this.state.coffeeCount + 1, }) } }

Cuando ahora hagamos clic en el botón representado por Child, recibiremos un error. ¿Por qué? Porque React cambió nuestro contexto al llamar al _getCoffeemétodo.

Supongo que React llama al método de renderizado de nuestros Componentes en otro contexto, a través de clases auxiliares o similares (aunque tendría que profundizar más para saberlo con certeza). Por lo tanto, this.stateno está definido y estamos intentando acceder this.state.coffeeCount. Deberías recibir algo como Cannot read property coffeeCount of undefined.

Para resolver el problema, debe bind(llegaremos allí) los métodos en nuestras clases, tan pronto como los pasemos fuera del componente donde están definidos.

Echemos un vistazo a un ejemplo más genérico:

// BAD EXAMPLE class Viking { constructor(name) { this.name = name } prepareForBattle(increaseCount) { console.log(`I am ${this.name}! Let's go fighting!`) increaseCount() } } class Battle { constructor(vikings) { this.vikings = vikings this.preparedVikingsCount = 0 this.vikings.forEach(viking => { viking.prepareForBattle(this.increaseCount) }) } increaseCount() { this.preparedVikingsCount++ console.log(`${this.preparedVikingsCount} vikings are now ready to fight!`) } } const vikingOne = new Viking('Olaf') const vikingTwo = new Viking('Odin') new Battle([vikingOne, vikingTwo])

Estamos pasando increaseCountde una clase a otra. Cuando llamamos al increaseCountmétodo Viking, ya hemos cambiado de contexto y en thisrealidad apuntamos al Viking, lo que significa que nuestro increaseCountmétodo no funcionará como se esperaba.

Solución - enlazar

La solución más simple para nosotros son bindlos métodos que se pasarán de nuestro objeto o clase original. Hay diferentes formas de vincular funciones, pero la más común (también en React) es vincularla en el constructor. Entonces tendríamos que agregar esta línea en el Battleconstructor antes de la línea 18:

this.increaseCount = this.increaseCount.bind(this)

Puede vincular cualquier función a cualquier contexto. Esto no significa que siempre tenga que vincular la función al contexto en el que está declarada (este es el caso más común, sin embargo). En su lugar, podría vincularlo a otro contexto. Con bind, siempre establece el contexto para una declaración de función . Esto significa que todas las llamadas para esa función recibirán el contexto enlazado como this. Hay otros dos ayudantes para establecer el contexto.

Las funciones de flecha `() => {}` vinculan automáticamente la función al contexto de declaración

Aplicar y llamar

They both do basically the same thing, just that the syntax is different. For both, you pass the context as first argument. apply takes an array for the other arguments, with call you can just separate other arguments by comma. Now what do they do? Both of these methods set the context for one specific function call. When calling the function without call , the context is set to the default context (or even a bound context). Here is an example:

class Salad { constructor(type) { this.type = type } } function showType() { console.log(`The context's type is ${this.type}`) } const fruitSalad = new Salad('fruit') const greekSalad = new Salad('greek') showType.call(fruitSalad) // The context's type is fruit showType.call(greekSalad) // The context's type is greek showType() // The context's type is undefined

Can you guess what the context of the last showType() call is?

..

.

You’re right, it is the outermost scope, window . Therefore, type is undefined, there is no window.type

This is it, hopefully you now have a clear understanding on how to use this in JavaScript. Feel free to leave suggestions for the next article in the comments.

About the Author: Lukas Gisder-Dubé co-founded and led a startup as CTO for 1 1/2 years, building the tech team and architecture. After leaving the startup, he taught coding as Lead Instructor at Ironhack and is now building a Startup Agency & Consultancy in Berlin. Check out dube.io to learn more.