Nonsense

¿Por qué 'this' se comporta así?

Pregunta: ¿Por qué el this se comporta así (no el cómo, que está en mil sitios, sino el por qué)? […] ¿Qué tipo de escenarios/patrones/diseños posibilita? (si es que hay alguno aparte de confundir a javeros y netteros ;) – @gulnor

Esta es una pregunta complicada. Más que nada porque no creo que haya una única respuesta. Aún así, lo que tengo claro es que mucha gente titularía esta pregunta “this, ¿por qué eres así? ¿Qué te he hecho yo?”. Pero en fin, intentemos contestar seriamente :)

El Contexto Histórico

Si hay algo que no voy a hacer jamás en la vida es escribir la historia de JavaScript. Sin embargo, me temo que sí se hace necesario que nos situemos, al menos parcialmente, en el contexto de creación de JavaScript.

Incluso aunque no nos ajustemos al mito de los 11 días, hay que entender que la creación y desarrollo de JavaScript fue muy rápida. Es decir, a esos 11 días de desarrollo inicial, creo que no se suman muchos más de planteamientos e investigación. En general creo que da un poco igual cuánto tiempo exacto llevara, siempre que tengamos clara esa situación de urgencia.

Urgencia

Es bastante claro, para cualquiera que desarrolle software, por qué es relevante el tema de la urgencia. Enfrentados a una entrega urgente en un proyecto en el que tenemos bastante libertad para definir los requisitos (salvo unas directrices y restricciones generales, Brendan Eich tuvo bastante libertad a la hora de tomar decisiones), lo más normal es que tendamos a simplificar.

Hay un detalle más o menos curioso de JavaScript que es la cantidad de palabras (keywords) interesantes que tiene reservadas pero que no tienen uso. Ya al estandarizarse por primera vez en el 97 la especificación incluía palabras no sólo como class o super sino como export e import. O enum, que está ahí desde la primera versión y a día de hoy (ES2018) sigue sin tener uso. Esto, creo yo, da una idea de reservarse caminos abiertos pero “ahora” sacrificar bastantes funcionalidades o capacidades a cambio de conseguir la entrega en el tiempo requerido.

Y con esto, no quiero decir que se optara por chapuzas, sino que se optó por simplificaciones, por opciones más sencillas que permitieran un desarrollo más inmediato, aunque sacrificaran otros aspectos. Se dejaron abiertas puertas pero también entiendo que Eich decidió desarrollar algo que pudiera alcanzar en ese tiempo disponible.

Prototipos y ausencia de Clases

Guiado por esas simplificaciones necesarias y, también, por su inspiración en Scheme y Self como “lenguajes guía” en sus decisiones, Eich opta por un sistema de herencia/delegación basado en prototipos y en la ausencia total de clases.

Aquí hay que tener cuidado con ciertas asunciones. Es el año 1995.

En este mismo año nacen Java y Ruby, entre otros. Python es de 1991, Lua de 1993, Perl del 87 (con versiones relevantes en el 93 y 94), C++ o “c con clases” del 85. C# es de 2000. Cada una de esas cosas no es particularmente relevante para JavaScript. Algunos sí tienen alguna influencia otros no. Pero en conjunto, mirando todos estos lenguajes hay que observar un detalle que sí puede tener cierta relevancia: La orientación a objetos de estos lenguajes no es igual. Por esto, es arriesgado asumir que alguna en concreto y de forma específica, represente de manera única “la forma correcta” o “apropiada” de implementarlo. Incluso hoy, cuando se ha impuesto mucho la aproximación de Java / C#, sigue habiendo discusión de cómo tienen que o pueden ser las cosas en cuanto a la “orientación a objetos”. Digo esto porque tenemos que pensar entonces que son simplemente formas diferentes de hacer y que no necesariamente son mejores o peores. Respecto a this, que otros lenguajes pueden llamar self, @ o de otros modos, ocurre algo similar.

El caso es que elegir un sistema de delegación que se basa en objetos y prototipos y que no incluye el uso de clases es relevante sobre el funcionamiento de this.

Yo

En general, cuando un lenguaje permite la creación de objetos con propiedades y métodos, necesita proporcionar algún modo para que se pueda referir a sí mismo, para que, por ejemplo, en un método del objeto podamos referirnos a una propiedad del propio objeto. Ese referirnos a “el propio objeto” sobre el que estamos operando es lo que necesitamos poder hacer.

Pero como decía antes, dado que hay variaciones en cómo están implementados los objetos (“la orientación a objetos”), puede haber también ciertas diferencias en cuanto a lo que significan “yo mismo” o “el propio objeto”, “este mismo objeto”.

Objetos, Propiedades y Funciones en JavaScript

Por ahora tenemos como decisión relevante la de usar prototipos y no clases. A esto añadimos otra también relevante que es la de hacer que las funciones sean valores de primera clase en el lenguaje.

Una manera bastante sencilla de implementar objetos en tu lenguaje, especialmente cuando has tomado estas decisiones, es hacer que los objetos sean simplemente una colección de propiedades, entendidas como slots. Es decir, básicamente una tabla (por lo menos de forma conceptual, no quiere decir que lo tengas que implementar como una tabla).

Esto tiene bastantes ventajas. Te permite en cualquier momento añadir, modificar, eliminar propiedades en un objeto. Y no sólo eso, el hecho de ser dinámicamente tipado, y así simplemente ser una colección de slots sin más, te da una flexibilidad muy alta.

Esa flexibilidad que te da, unida a que las funciones sean valores de primera clase, produce que puedas hacer cosas como

function f() { /*...*/ };
let o = { };

o.prop = f;

Y esto está muy bien, porque tenemos un sistema de objetos muy sencillo y muy flexible y encima es muy fácil implementar la idea de que un objeto tenga “métodos”… ¡Son simplemente funciones que ponemos en un slot! Y las funciones son valores así que podemos hacerlo sin más problema.

¿Dónde estoy? ¿Quién soy yo? ¿No queda cerveza?

¡Ojo que aquí viene todo el meollo de la cuestión! ¡No pierdan de vista la bolita!

En un sistema así, ¿cómo resolvemos ahora esa necesidad de que, en un “método de un objeto”, podamos hacer referencia a “este objeto”, “yo mismo”? Sin duda debería haber unas cuantas maneras. Podríamos tener algún mecanismo por el cual al hacer llamada a una función, esta supiera “a qué objeto está asignada”, por ejemplo. Ah, pero esto, claramente no es válido.

function f() { /*...*/ };
let o = { };
let p = { }

o.prop = f;
p.prap = f;

o.prop();
p.prap();

No, porque la función es libre y no nos vale con saber que un slot de un objeto la referencia (sí, todo son punteros).

En realidad, lo que ocurre es que al ser el diseño así y poder cambiar en tiempo de ejecución las propiedades, el propio concepto de “este mismo objeto”, queda distorsionado.

O más que distorsionado, lo que ocurre es que “este mismo objeto” tiene que pasar a significar algo igualmente dinámico, algo que se debe establecer en el propio momento de ejecutar la llamada. No hay otra alternativa. En ningún otro momento podemos saber a qué se refiere “este mismo objeto” en una llamada a una función determinada salvo en el momento preciso de ejecutar esa llamada. Y más aún, a lo único que tiene sentido que se refiera es a lo que diga esa llamada concreta (o.prop()o).

Y además es algo que debe ocurrir para todas las llamadas, para cada llamada; así tiene sentido que this no herede o se resuelva del contexto léxico contenedor como se hace con otras referencias, ya que no está ligado al contexto de definición sino al contexto de llamada.

Es cierto que recientemente, años después, se han añadido las arrow functions que precísamente lo que hacen es resolver this desde el contexto contenedor. Personalmente creo que en este detalle no es la mejor decisión posible y que añade complejidades más que reducirlas. Habría preferido, por ejemplo que las arrow functions no pudieran hacer referencia a this y así limitar su uso. Sin embargo, entiendo que esta nueva sintaxis y semántica de las funciones tenía otras motivaciones en la comunidad de desarrollo. Así es la vida, La turbamulta manda.

Es decir, el hecho de que this se refiera al sujeto sobre el que estamos realizando la llamada, no es tanto una elección de diseño cuanto la única opción que nos queda dado todo el resto del diseño.

Otros valores de 'this'

Personalmente creo que buena parte de la confusión alrededor de this en JavaScript no viene tanto de lo explicado más arriba -que sinceramente me parece perfectamente razonable-, sino que viene de algunas omisiones de diseño para los otros casos.

Es decir, en mi opinión no habría confusión, o habría mucha menos, si hubiera, por ejemplo, alguna restricción que simplemente no nos permitiera hacer referencia a this en una llamada que se hace a una función sin hacerse sobre ningún sujeto. Por ejemplo un ReferenceError. En lugar de eso, y aquí creo que es difícil buscar motivos más allá de la urgencia; temo que se optó por el apaño 1) 2) de “por omisión” asignar el objeto global a this en la llamada para asignar algo. (Posteriormente el “arreglo” de esto ha sido dejarlo como undefined y entonces sí, fallará con un error, aunque será un error igual que si intentamos des-referenciar undefined en cualquier otro caso.)

Digo que esto es lo que añade confusión, porque permite que ocurra son normalidad algo que nunca nos interesa. Permite que escribamos código aparentemente válido pero conceptualmente erróneo. El único caso en que queremos ejecutar una función que haga referencia a “yo mismo”, “este mismo sujeto”, es cuando realmente hay tal concepto, no haciendo llamadas que no tienen un sujeto.

Luego está la opción de manipular la asignación del sujeto de forma explícita (no sólo call/apply o bind, sino en general muchas funciones que reciben una función como parámetro). En estos casos, creo que el comportamiento no deja duda a preguntarnos por qué. Lo estamos pidiendo de forma explícita.

Y finalmente está el problema de new. Pero aquí creo que se trata de otra cosa. Quiero decir, es bastante lógico e incluso razonablemente intuitivo que si this es un valor que depende de la llamada y que la llamada se hace a través de new, entonces “este objeto” debe referirse al objeto que crea new, que para eso lo usamos. El tema con new ya sería discutir cómo o por qué encajar la creación de objetos de esta forma en un sistema de prototipos. Pero eso, como digo, ya es otro tema y no tiene que ver con this.

1)
O por lo menos aparente apaño, ya que si nos atenemos a la decisión explícita que hacer que el lenguaje sea permisivo y dé pocos errores porque es para gente sin demasiada experiencia, entonces es una decisión perfectamente cromulente
2)
cromulente – (irónico) De apariencia legítima pero en realidad espurio