YOU DON’T KNOW JS – 03: THIS & OBJECT PROTOTYPES en Castellano pdf

Bueno, ya he terminado con otro título de la serie You Don’t Know JS, que os recuerdo que podéis encontrar en github para su consulta y descarga en Inglés. Es dificil decir cual de los libros de la serie me han gustado más, pero este me ha calado y además me ha convencido a deshechar el mecanismo de clases en Javascript, el cual no es  parte del lenguaje, y decidirme a estructurar mi código en base a la delegación de comportamiento. Esto, tal y cómo se deja claro en este título, no sólo hace el código más simple y evita problemas, sino que te hace ser consciente en cada momento de lo que JS puede hacer bien y que lo que no.

Como se ha dicho mucho, Javascript es un gran lenguaje pero hay que saber cómo usarlo correctamente para evitar sorpresas desagradables. Leyendo esta nueva entrega, estás más cerca de conseguirlo. Bueno aquí lo tenéis, espero que os guste tanto como a mí.

THIS & OBJECT PROTOTYPES: Kyle Simpson


1. this or that? (esto o aquello?)

Uno de los mecanismos más confusos en JavaScript es la palabra clave this. Es una palabra clave de identificación especial que se define automáticamente en el ámbito de cada función.

Cualquier tecnología suficientemente avanzada es indistinguible de la magia. — Arthur C. Clarke

El mecanismo de Javascript this no es realmente tan avanzado, pero los desarrolladores a menudo parafrasean esa cita en su propia mente insertando «complejo» o «confuso», y no hay duda de que sin falta de comprensión clara, this puede parecer francamente mágico entre su confusión.

Nota: La palabra «this» es un pronombre terriblemente común en el discurso general. Por lo tanto, puede ser muy difícil, especialmente verbalmente, determinar si estamos usando «esto» como pronombre o si lo estamos usando para referirnos al identificador real de la palabra clave. Para mayor claridad, siempre usaré this para referirme a la palabra clave especial, y «esto» o esto para los otros.

¿Por qué this?

Si el mecanismo this es tan confuso, incluso para desarrolladores JavaScript experimentados, uno puede preguntarse por qué es tan útil. ¿Ocasiona más problemas que benefícios? Antes de entrar en el cómo, debemos examinar el por qué.

Tratemos de ilustrar la motivación y la utilidad de this:

Si este fragmento te confunde, ¡no te preocupes! Llegaremos a eso pronto. Deje esas preguntas a un lado brevemente para que podamos ver el por qué más claramente.

Este fragmento de código permite que las funciones identify() y speak() sean reutilizadas contra objetos de contexto múltiple (me y you), en lugar de necesitar una versión separada de la función para cada objeto.

En lugar de confiar en this, podrías haber pasado explícitamente en un objeto contextual tanto para identify() como para speak().

Sin embargo, el mecanismo this proporciona una forma más elegante de «pasar» implícitamente una referencia a un objeto, lo que conduce a un diseño de API más limpio y a una reutilización más fácil.

Cuanto más complejo sea su patrón de uso, más claramente verá que pasar el contexto como un parámetro explícito es a menudo más complicado que pasar el contexto this. Cuando exploeamos objetos y prototipos, verás la utilidad de una colección de funciones que pueden hacer referencia automáticamente al objeto de contexto apropiado.

Confusiones

Pronto empezaremos a explicar cómo realmente funciona this, pero primero debemos disipar algunas conceptos erróneos sobre cómo no funciona en realidad.

El nombre «this» crea confusión cuando los desarrolladores tratan de pensar en ello demasiado literalmente. Allí son dos significados a menudo asumidos, pero ambos son incorrectos.

En sí mismo

La primera tentación común es asumir que this se refiere a la función misma. Eso es un una inferencia gramatical razonable, al menos.

¿Por qué querría referirse a una función desde dentro de sí misma? Las razones más comunes serían cosas como la recursión (llamar a una función desde dentro de sí misma) o tener un evento que puede desatarse cuando se llama por primera vez.

Los desarrolladores nuevos en los mecanismos de JS a menudo piensan que hacer referencia a la función como un objeto (todas las funciones en JavaScript son objetos!) le permite almacenar el estado (valores en las propiedades) entre llamadas de función. Si bien esto es ciertamente posible y tiene algunos usos limitados, el resto del libro explicará muchos otros patrones con otros mejores lugares para almacenar el estado además de la función objeto.

Pero por un momento, exploraremos el patrón, para ilustrar cómo this no deja que una función obtenga una referencia a sí misma como podríamos haber asumido.

Considere el siguiente código, donde intentamos rastrear cuántas veces una función ( foo ) es llamada:

foo.count sigue siendo 0 , a pesar de que las cuatro sentencias console.log indican claramente que foo(..) fue llamado cuatro veces. La frustración proviene de una interpretación demasiado literal de lo que this (en this.count++) significa.

Cuando el código ejecuta foo.count = 0, de hecho está añadiendo una propiedad countal objeto de función foo. Pero para la referencia this.count dentro de la función, thisde hecho no apunta en absoluto a ese objeto de función, así que aunque los nombres de las propiedades son los mismos, los objetos raíz son diferentes, y se produce confusión.

Nota: Un desarrollador responsable debe preguntar en este punto, «Si estaba incrementando una propiedad count pero no era la que esperaba, ¿qué count estaba incrementando?» De hecho, si cavara más profundo, se daría cuenta de que accidentalmente había creado una variable count global (¡ver Capítulo 2 para ver cómo sucedió!), y que actualmente tiene el valor NaN. Por supuesto, una vez que identifica este resultado peculiar, tiene otra serie de preguntas: «¿Cómo fue global, y por qué terminó siendo NaN en lugar de un valor de countapropiado?» (ver Capítulo 2).

En lugar de detenerse en este punto y averiguar por qué esta referencia no parece comportarse como se esperaba, y responder a esas preguntas difíciles pero importantes, muchos desarrolladores simplemente evitan el problema por completo, y hackean hacia alguna otra solución, como por ejemplo creando otro objeto para sostener la propiedad count:

Si bien es cierto que este enfoque «resuelve» el problema, desafortunadamente simplemente ignora el verdadero problema – la falta de comprensión de lo que this significa y cómo funciona – y en su lugar vuelve a la zona de confort de un mecanismo más familiar: el ámbito léxico.

Nota: El alcance léxico es un mecanismo perfectamente fino y útil; no estoy menospreciando su uso, de ninguna manera (ver «scope & closures» en el título de esta serie de libros). Pero adivinar constantemente cómo usar this y, por lo general, estar equivocado, no es una buena razón para volver al ámbito léxico y nunca aprender por qué this te elude.

Para hacer referencia a un objeto de función desde dentro de sí mismo, this por sí mismo será normalmente insuficiente. Generalmente se necesita una referencia al objeto de función mediante un identificador léxico (variable) que apunta a ello.

Considere estas dos funciones:

En la primera función, llamada «función nombrada», foo es una referencia que puede usarse para referirse a la función desde dentro de sí misma.

Pero en el segundo ejemplo, la función callback pasada a setTimeout(..) no tiene ningún identificador de nombre (por lo que es llamada «función anónima»), así que no hay una manera apropiada de referirse al objeto de función en sí.

Nota: La referencia de la vieja escuela arguments.callee dentro de una función también apunta a la función objeto de la función que se está ejecutando actualmente. Esta referencia es típicamente la única manera de acceder al objeto de una función anónima desde su interior. El mejor enfoque, sin embargo, es evitar el uso de funciones anónimas en su totalidad, al menos para aquellas que requieren una auto-referencia, y en su lugar usar una función con nombre (expresión). arguments.callee está obsoleto y no debería usarse.

Así que otra solución a nuestro ejemplo de ejecución habría sido usar el identificador de foocomo una referencia de objeto de función en cada lugar y no utilizar this en absoluto, lo que funciona:

Sin embargo, ese enfoque también deja de lado la comprensión real de this y se basa enteramente en el alcance léxico de la variable foo.

Otra forma de enfocar la cuestión es obligar a que this apunte realmente al objeto de función foo:

En lugar de evitar this, lo aceptamos. Explicaremos en un momento cómo estas técnicas funcionan de manera mucho más completa, así que no te preocupes si todavía estás un poco confundido.

Su Alcance

El siguiente error más común sobre el significado de this es que de alguna manera se refiere al alcance de la función. Es una pregunta delicada, porque en un sentido hay algo de verdad, pero en el otro, es bastante equivocada.

Para ser claros, this no se refiere, de ninguna manera, al alcance léxico de una función. Es cierto que internamente, el alcance es como un objeto con propiedades para cada uno de los identificadores disponibles. Pero el «objeto» de alcance no es accesible para el código JavaScript. Es una parte interior de la implementación.

Considere el código que intenta (¡y falla!) cruzar el límite y usar this para se referirse implícitamente al alcance léxico de una función:

Hay más de un error en este fragmento. Aunque pueda parecer artificioso, el código que ves es una destilación del código real del mundo real que ha sido intercambiado en los foros de ayuda de la comunidad pública. Es una ilustración maravillosa (si no triste) de cuán equivocadas pueden ser estas suposiciones.

En primer lugar, se intenta hacer referencia a la función bar() a través de este.bar(). Es casi seguro que es un accidente que funcione, pero le explicaremos cómo hacerlo en breve. La forma más natural de haber invocado bar() habría sido omitir el encabezado y hacer una referencia léxica al identificador.

Sin embargo, el desarrollador que escribe tal código está intentando usar esto para crear un puente entre los alcances léxicos de foo() y bar(), para que bar() tenga acceso a la variable a en el alcance interno de foo(). Tal puente no es posible. No se puede utilizar esta referencia para buscar algo en un ámbito léxico. No es posible.

Cada vez que te sientas tratando de mezclar la búsqueda del léxico con esto, recuérdate a ti mismo:
no hay puente.

¿Qué this?

Habiendo dejado de lado varias suposiciones incorrectas, centrémonos ahora en cómo funciona realmente el mecanismo this.

Hemos dicho antes que this no se trata de un enlace en tiempo de autor, sino de un enlace en tiempo de ejecución. Es contextual basado en las condiciones de la invocación de la función. La vinculación de this no tiene nada que ver con el lugar donde se declara una función, sino que tiene que ver con la manera en que se llama la función.

Cuando se invoca una función, se crea un registro de activación, también conocido como contexto de ejecución. Este registro contiene información sobre el lugar desde el que se llamó a la función (la pila de llamadas), cómo se invocó la función, qué parámetros se pasaron, etc. Una de las propiedades de este registro es la referencia a this que se utilizará durante la ejecución de esa función.

En el siguiente capítulo, aprenderemos a encontrar el lugar de llamada de una función para determinar cómo su ejecución vinculará this.

Revisión (TL;DR)

La vinculación de this es una fuente constante de confusión para el desarrollador de JavaScript que no se toma el tiempo de aprender cómo funciona realmente el mecanismo. Adivinar, prueba y error, y copiar y pegar a ciegas de las respuestas de Stack Overflow no es una manera efectiva o apropiada de aprovechar este importante mecanismo this.

Para aprender this, primero tienes que aprender lo que this no es, a pesar de cualquier suposición o concepto erróneo que pueda llevarte por esos caminos. this no es ni una referencia a la función en sí misma, ni es una referencia al alcance léxico de la función.

this es en realidad un enlace que se hace cuando se invoca una función, y lo que hace referencia está determinado enteramente por el sitio donde se llama la función.

2. ¡Todo esto tiene sentido ahora!

En el Capítulo 1, descartamos varios conceptos erróneos sobre this y aprendimos en cambio que se trata de un vínculo hecho para cada invocación de función, basado enteramente en su lugar de llamada (cómo se llama la función).

Lugar de llamada

Para entender este vínculo, tenemos que entender lo que es el sitio de la llamada: la ubicación en código donde se llama una función (no donde se declara). Debemos inspeccionar el sitio de la llamada para responder a la pregunta: ¿a qué se refiere this?

Encontrar el lugar de la llamada es generalmente: «ir a localizar desde dónde se llama una función», pero no siempre es tan fácil, ya que ciertos patrones de codificación pueden oscurecer el verdadero lugar de la llamada.

Lo importante es pensar en el call-stack (la pila de funciones que han sido llamadas para llevarnos al momento actual de la ejecución). El sitio de llamada que nos interesa está en la invocación antes de la función que se está ejecutando actualmente.

Vamos a demostrar la pila de llamadas y el lugar de la llamada:

Tenga cuidado al analizar el código para encontrar el sitio real de la llamada (de la pila de llamadas), porque es lo único que importa para la vinculación de this.

Nota: Puede visualizar una pila de llamadas en su mente mirando la cadena de llamadas de función en como hicimos con los comentarios en el fragmento anterior. Pero esto es meticuloso y propenso a errores. Otra forma de ver la pila de llamadas es utilizando una herramienta de depuración en su navegador. La mayoría los navegadores de escritorio modernos tienen herramientas de desarrollo incorporadas, que incluyen un depurador JS. En el fragmento anterior, podría haber establecido un punto de ruptura en las herramientas para la primera línea de la directiva foo() o simplemente insertar la función debugger; en la primera línea. Cuando se carga la página, el depurador hará una pausa en esta ubicación y le mostrará una lista de las funciones que han sido llamadas para llegar a esa línea, que será su pila de llamadas. Así que, si estás intentando diagnosticar this use las herramientas del desarrollador para obtener la pila de llamadas, luego busque la segunda desde la parte superior, y eso le mostrará el sitio real de la llamada.

Nada más que reglas

Ahora devolvemos nuestra atención a cómo el sitio de la llamada determina hacia dónde this apuntará durante la ejecución de una función.

Usted debe inspeccionar el lugar de la llamada y determinar cuál de las 4 reglas se aplica. Primero explicaremos cada una de estas 4 reglas independientemente, y luego ilustraremos su orden de precedencia, si se pueden aplicar múltiples reglas al sitio de la llamada.

Vinculación por defecto

La primera regla que examinaremos proviene del caso más común de llamadas a funciones:
invocación de funciones independientes. Piense en esta regla como la regla general predeterminada cuando no se aplica ninguna de las otras reglas.

Considere este código:

Lo primero que hay que tener en cuenta, si no se sabía ya, es que las variables declaradas en el ámbito global, como var a = 2, son sinónimos de propiedades objeto-global del mismo nombre. No son copias el uno del otro, son el uno del otro. Piensa en ello como las dos caras de la misma moneda.

En segundo lugar, vemos que cuando foo() es llamado, this.a se resuelve a nuestra variable global a. ¿Por qué? Porque en este caso, el enlace por defecto se refiere a la llamada de función y, por lo tanto, apunta al objeto global.

¿Cómo sabemos que aquí se aplica la norma vinculante por defecto? Examinamos el sitio de la llamada para ver cómo se llama a foo(). En nuestro fragmento, foo() se llama con una referencia de función simple, sin decoración. Ninguna de las otras reglas que vamos a demostrar se aplicará aquí, por lo que se aplicará la vinculación por defecto en su lugar.

Si el modo estricto está activado, el objeto global no es elegible para la vinculación por defecto, por lo que se fija a undefined.

Un detalle sutil pero importante es: a pesar de que en general estas reglas vinculantes se basan enteramente en el lugar de la llamada, el objeto global sólo es elegible para la vinculación por defecto si el contenido de foo() no se está ejecutando en modo estricto ; el estado de modo estricto del lugar de la llamada de foo() es irrelevante.

Nota: Mezclar intencionalmente el modo estricto y el modo no estricto en su propio código no es generalmente recomendable. Todo su programa probablemente debería ser Estricto o no. Sin embargo, a veces se incluye una librería de terceros que tiene un modo estricto diferente a su propio código, por lo que se debe tener cuidado con estos detalles sutiles de compatibilidad.

Vinculación implícita

Otra regla a considerar es: ¿tiene el sitio de llamada un objeto de contexto, también conocido como un objeto que es propietario o contenedor, aunque estos términos alternativos podrían ser ligeramente engañosos?

Considere:

En primer lugar, observe la forma en que se declara foo() y luego se añade como propiedad de referencia a obj. Independientemente de si foo() se declara inicialmente en el objeto, o se añade como referencia más tarde (como muestra este fragmento), en ninguno de los dos casos la función es realmente «propiedad» o «contenida» por el objeto obj.

Sin embargo, el emplazamiento de llamada utiliza el contexto de obj para hacer referencia a la función, de modo que podría decirse que el objeto obj «posee» o «contiene» la referencia de función en el momento en que se llama la función.

Cualquiera que sea el nombre que elijas para llamar a este patrón, en el punto en que se llama foo(), está precedido por una referencia de objeto obj. Cuando existe un objeto contextual para una referencia de función, la regla de vinculación implícita dice que es ese objeto el que debe utilizarse para el vínculo this de la llamada de función.

Debido a que obj es el this para la llamada de foo(), this.a es sinónimo de obj.a. Sólo el nivel superior/último de una cadena de referencia de propiedad de objeto es importante para el sitio de llamada. Para ejemplo:

Pérdida implícita

Una de las frustraciones más comunes que crea la vinculación this es cuando una función implícitamente vinculada pierde esa vinculación, lo que normalmente significa que vuelve a la vinculación por defecto, ya sea del objeto global o undefined, dependiendo del modo estricto.

Considera:

Aunque bar parece ser una referencia a obj.foo, realmente, es sólo otra referencia al propio foo. Además, el sitio de la llamada es lo que importa, y el sitio de la llamada es bar(), que es una llamada sencilla, sin decoración y por lo tanto se aplica la vinculación por defecto.

La manera más sutil, más común y más inesperada en que esto ocurre es cuando consideramos pasar una función de devolución de llamada:

El paso de parámetros es sólo una asignación implícita, y como estamos pasando una función, es una asignación de referencia implícita, por lo que el resultado final es el mismo que el fragmento anterior.

¿Qué sucede si la función a la que está pasando la devolución de llamada no es tuya, sino una incorporada al lenguaje? No hay diferencia, se obtiene el mismo resultado.

Piense en esta cruda pseudo-implementación teórica de setTimeout() proporcionada incorporada al lenguaje desde el entorno JavaScript:

Es bastante común que nuestras llamadas de retorno de función pierdan esta unión, como acabamos de ver. Pero otra forma en que esto nos puede sorprender es cuando la función que hemos pasado nuestra llamada de retorno a intencionalmente cambia esto por la llamada. Los manejadores de eventos en las bibliotecas JavaScript más populares son bastante aficionados a forzar su devolución de llamada para que tenga un «esto» que apunte, por ejemplo, al elemento DOM que disparó el evento. Si bien eso puede ser útil a veces, otras veces puede ser francamente enfurecedor. Desafortunadamente, estas herramientas rara vez le permiten elegir.

De cualquier manera el esto se cambia inesperadamente, usted no está realmente en el control de cómo su referencia de la función de la llamada de vuelta será ejecutada, así que usted no tiene ninguna manera (todavía) de controlar el llamado-sitio para dar su atadura prevista. Veremos en breve una forma de «arreglar» ese problema arreglando los esta.

Vinculación explícita

Con la vinculación implícita que acabamos de ver, tuvimos que mutar el objeto en cuestión para incluir una referencia sobre sí mismo a la función, y utilizar esta referencia de función de propiedad para vincular this indirectamente (implícitamente) al objeto.

Pero, ¿qué pasa si se quiere forzar una llamada de función a usar un objeto particular para el vínculo de this, sin poner una referencia de función de propiedad en el objeto?

«Todas» las funciones en el lenguaje tienen algunas utilidades disponibles (a través de su [[Prototype]] — más sobre eso más adelante) que pueden ser útiles para esta tarea. Específicamente, las funciones tienen los métodos call(...) y apply(..). Técnicamente, los entornos de host JavaScript a veces proporcionan funciones que son lo suficientemente especiales (¡una forma amable de decirlo!) como para que no tengan tal funcionalidad. Pero esos son pocos. La gran mayoría de las funciones proporcionadas, y ciertamente todas las funciones que usted creará, tienen acceso a todos call(...) y apply(..).

¿Cómo funcionan estas utilidades? Ambas toman, como primer parámetro, un objeto a utilizar para el this, y luego invocan la función con ese this especificado. Puesto que usted declara directamente lo que quiere que sea, nosotros lo llamamos vinculando explícitamente.

Considere:

Invocar a foo con vinculación explícita mediante foo.call(…) nos permite forzar this a ser obj.

Si pasa un valor primitivo simple (de tipo string, boolean o number) como vinculación de this, el valor primitivo se envuelve en su forma de objeto ( new String(..), new Boolean(..), o new Number(..), respectivamente). Esto se denomina a menudo «boxing».

Nota: Con respecto a la vinculación de this, call(..) y apply(..) son idénticos. Se comportan de manera diferente con sus parámetros adicionales, pero eso no es algo que nos importe actualmente.

Desafortunadamente, la vinculación explícita por sí sola todavía no ofrece ninguna solución al problema mencionado anteriormente, de que una función «pierda» la vinculación de thisprevista, o simplemente lo tenga «recubiero» por un framework, etc.

Vinculación fuerte

Pero un patrón de variación alrededor de la vinculación explícita actualmente permite realizar un truco.

Considere:

Examinemos cómo funciona esta variación. Creamos una función bar() que, internamente, llama manualmente foo.call(obj), invocando así por la fuerza a foo con obj como vinculación para this. No importa cómo invoque más tarde la función bar, siempre invocará manualmente foo con obj. Esta encuadernación es explícita y fuerte, así que la llamamos encuadernación dura.

La forma más típica de envolver una función con una vinculación dura crea una transmisión de cualquier argumento pasado y cualquier valor de retorno recibido:

Otra forma de expresar este patrón es crear un ayudante reutilizable:

Dado que la vinculación fuete es un patrón tan común, se proporciona cono una utilidad incorporada como ES5: Function.prototype.bind, y se usa así:

bind(...) devuelve una nueva función que está codificada para llamar a la función original con el contexto this QUE usted especificó.

Nota: A partir de ES6, la función de vinculación producida por bind(..) tiene una propiedad .name que deriva de la función objetivo original. Por ejemplo: bar = foo.bind(..)debería tener un valor bar.name de "bound foo", que es el nombre de llamada de la función que debería aparecer en una traza de pila.

Llamada a la API «Contexts»

Las funciones de muchas bibliotecas, y de hecho muchas nuevas funciones incorporadas en el lenguaje JavaScript y en el entorno host, proporcionan un parámetro opcional, normalmente llamado «context», que está diseñado para evitar que tenga que usar bind(..) para asegurarse de que su función de devolución de llamada utiliza un this particular.

Por ejemplo:

Internamente, estas varias funciones utilizan casi con toda seguridad el enlace explícito vía call(...) o apply(...), ahorrándole el problema.

Vinculación con new

La cuarta y última regla para vinculación de this nos obliga a repensar un concepto erróneo muy común acerca de las funciones y objetos en JavaScript.

En los lenguajes tradicionales orientados a clases, los «constructores» son métodos especiales adjuntos a las clases, que cuando la clase es instanciada con un el operador new, se llama al constructor de esa clase. Esto por lo general se parece a:

JavaScript tiene un operador new, y el patrón de código para usarlo parece básicamente idéntico al que vemos en esos lenguajes orientados a clases; la mayoría de los desarrolladores asumen que el mecanismo de JavaScript está haciendo algo similar. Sin embargo, realmente no hay ninguna conexión con la funcionalidad orientada a clases que implica el uso de new en JS.

Primero, redefinamos lo que es un «constructor» en JavaScript. En JS, los constructores son sólo funciones que se llaman con el operador new delante de ellos. No están vinculados a clases, ni están instanciando una clase. Ni siquiera son funciones especiales. Son sólo funciones regulares que son, en esencia, secuestradas por el uso de new en su invocación.

Por ejemplo, la función Number(..) actuando como constructor, citando la especificación ES5.1:

15.7.2. El constructor de números
Cuando se llama a un Número como parte de una nueva expresión es un constructor: inicializa el objeto recién creado.

Por lo tanto, casi cualquier función antigua, incluyendo las funciones objeto incorporadas en el lenguaje como Number(...) (ver Capítulo 3) puede ser llamada con new delante de ella, y eso hace que esa función llame a un constructor. Esta es una distinción importante pero sutil: en realidad no existen «funciones constructoras», sino más bien llamadas de construcción de funciones.

Cuando se invoca una función con new delante de ella, también conocida como llamada al constructor, las siguientes cosas se hacen automáticamente:

  1. se crea un objeto nuevo (también conocido como construido) de la nada
  2. El objeto de nueva construcción está ligado a [[Prototype]]
  3. El objeto de nueva construcción se fija como el enlace para esa llamada de función.
  4. a menos que la función devuelva su propio objeto alternativo, la nueva llamada de función invocada devolverá automáticamente el nuevo objeto construido.

Los pasos 1, 3 y 4 se aplican a nuestra discusión actual. Saltaremos el paso 2 por ahora y volveremos a él en el capítulo 5.

Considere este código:

Llamando a foo(...) con new delante, hemos construido un nuevo objeto y hemos puesto ese nuevo objeto como this para la llamada de foo(…). Así que new es la última forma se pùede vincular el this en una llamada a una función. Llamaremos a esto vinculación nueva.

Todo En Orden

Así que, ahora hemos descubierto las 4 reglas para vincular this en llamadas de función. Todo lo que tiene que hacer es encontrar el lugar de la llamada e inspeccionarlo para ver qué regla se aplica. Pero, ¿qué pasa si el sitio de la llamada tiene múltiples reglas elegibles? Debe haber un orden de precedencia de estas reglas, por lo que a continuación mostraremos en qué orden aplicar las reglas.

Debe quedar claro que la vinculación por defecto es la regla de menor prioridad de las 4. Por lo tanto, vamos a dejarla a un lado.

¿Cuál es más importante, vinculante implícitamente o vinculante explícitamente? Vamos a probarlo:

Por lo tanto, la vinculación explícita tiene prioridad sobre la vinculación implícita, lo que significa que usted debe preguntar primero si se aplica la vinculación explícita antes de verificar la vinculación implícita.

Ahora, sólo tenemos que averiguar dónde encaja la nueva vinculación en la precedencia.

Bien, la nueva vinculación es más precedente que la vinculación implícita. Pero, ¿cree que la vinculación connew es más o menos precedente que la vinculación explícita?

Nota: new y call / apply no se pueden utilizar juntos, por lo que no se permite el uso de new foo.call(obj1) para probar la vinculación con new directamente contra una vinculación explícita. Pero todavía podemos usar la vinculación fuerte para probar la precedencia de las dos reglas.

Antes de que exploremos esto en un listado de código, pensemos en cómo funciona físicamente la vinculación fuerte, que es que Function.prototype.bind(..) crea una nueva función de envoltura que está codificada para ignorar su propio this vinculado (sea lo que sea), y usar uno manual que nosotros proporcionemos.

Por este razonamiento, parecería obvio asumir que la vinculación fuerte (que es una forma de vinculación explícita) precede a la vinculación con new, y por lo tanto no puede ser anulada con new. Vamos a comprobarlo:

!!! bar se vincula fuertemente con obj1, pero new bar(3) no cambió obj1.a para ser 3 como esperábamos. En cambio, la vinculación fuerte a obj1 en la llamada a bar (...)puede ser anulada con new. Desde que se aplica new, recuperamos el objeto recién creado, al que llamamos baz, y vemos de hecho que baz.a tiene el valor 3.

Esto debería sorprenderte si vuelves a nuestro «falso» ayudante de vinculación:

Si razonas sobre cómo funciona el código del ayudante, no hay forma de que una llamada anule la vinculación fuerte a obj como acabamos de observar.

Pero la función incorporada Function.prototype.bind(...) a partir de ES5 es más sofisticada, un poco más de hecho. Aquí está el (ligeramente reformateado) polyfill proporcionado por la página MDN para bind(...):

Nota: El polyfill bind(..) que se muestra arriba difiere del bind(..) incorporado en ES5 con respecto a las funciones de vinculación fuertes que se utilizarán con new (ver abajo por qué es útil).

Debido a que el polyfill no puede crear una función sin un .prototype como lo hace la utilidad incorporada, hay algunos matices indirectos para aproximar el mismo comportamiento. Anda con cuidado si planeas usar new con una función de vinculación fuerte y confías en este polyfill.

La parte que permite una nueva anulación es:

En realidad no nos zambulliremos en la explicación de cómo funciona este truco (es complicado y está más allá de nuestro alcance aquí), pero esencialmente la utilidad determina si la función de vinculación fuerte ha sido llamada con new (resultando en un objeto de nueva construcción poseiendo su this), y si es así, usa ese objeto de nueva creación en lugar de la vinculación fuerte previamente especificada para this.

¿Por qué es útil que new pueda anular la vinculación fuerte?

La razón principal de este comportamiento es crear una función (que puede ser usada con new para objetos para construir objetos) que esencialmente ignora elthis de la vinculación fuerte pero que preselecciona algunos o todos los argumentos de la función. Una de las capacidades de bind(...) es que cualquier argumento pasado después del primer argumento vinculante this, se predetermina como argumento estándar para la función subyacente (técnicamente llamada «aplicación parcial», que es un subconjunto de «currying»). Por ejemplo:

Determinar this

Ahora, podemos resumir las reglas para determinar this desde el sitio de llamada de una llamada de función, en su orden de precedencia. Haga estas preguntas en este orden y deténgase cuando se aplique la primera regla.

  1. ¿Se llama a la función con new (vinculación con new)? Si es así, this es el nuevo objeto construido.var bar = nuevo foo()
  2. ¿Se llama a la función call o apply (vinculación explícita), incluso oculta dentro de una vinculación fuerte con bind? Si es así, this es el objeto explícitamente especificado.var bar = foo.call( obj2)
  3. ¿Se llama a la función con un contexto (vinculación implícita), también conocido como objeto propietario o contenedor? Si es así, this es ese objeto de contexto.var bar = obj1.foo()
  4. De lo contrario, por defecto es el this por defecto (vinculación por defecto). Si está en modo estricto, seleccione undefined; de lo contrario, seleccione el objeto global.var bar = foo()

Eso es todo. Eso es todo lo que se necesita para entender las reglas de this. para llamadas de función normales. Bueno… casi.

Excepciones de vinculación

Como de costumbre, hay algunas excepciones a las «reglas». El comportamiento de la vinculación de this puede sorprender en algunos escenarios, en los que se pretendía una vinculación diferente pero se termina con una vinculación predeterminada (véase más arriba).

this Ignorado

Si pasa nulo o indefinido como un parámetro de vinculación this a call, apply, o bind, esos valores son efectivamente ignorados, y en su lugar la regla de vinculación por defecto se aplica a la invocación.

¿Por qué pasarías intencionadamente algo como null por una vinculación de this?

Es muy común usar apply(..) para distribuir matrices de valores como parámetros a una llamada de función. Del mismo modo, bind(..) puede preparar parámetros (valores preestablecidos), lo que puede ser muy útil.

Ambas utilidades requieren una vinculación de this para el primer parámetro. Si a las funciones en cuestión no les importa this, necesitan un valor de marcador de posición, y null puede parecer una opción razonable como se muestra en este fragmento.

Nota: No lo tratamos en este libro, pero ES6 tiene el operador ... de esparcido que le permitirá sintácticamente «extender» una matriz como parámetros sin necesidad de apply(...), como foo(...[1,2]), lo que equivale a foo(1,2) — evitando sintácticamente una vinculación de this si es innecesaria. Desafortunadamente, no existe un sustituto sintáctico ES6 para el «currying», por lo que el parámetro this de la llamada bind(..) todavía necesita atención.

Sin embargo, hay un ligero «peligro» oculto en usar siempre null cuando no te importa la vinculación de this. Si alguna vez lo utiliza contra una llamada de función (por ejemplo, una función de biblioteca de terceros que no controla), y esa función hace una referencia a this, la regla de vinculación por defecto significa que podría, inadvertidamente, hacer referencia (¡o peor aún, mutar!) al objeto global («ventana en el navegador»).

Obviamente, esta trampa puede llevar a una variedad de errores muy difíciles de diagnosticar/rastrear.

this más seguro

Tal vez una práctica algo «más segura» sea pasar un objeto específicamente configurado para this, lo que garantiza que no será un objeto que pueda crear efectos secundarios problemáticos en su programa. Tomando prestada la terminología de las redes (y de los militares), podemos crear un objeto «DMZ» (zona desmilitarizada) — nada más especial que un objeto completamente vacío, no delegado (ver Capítulos 5 y 6).

Si siempre pasamos un objeto DMZ como vinculaciones this ignoradas que no creemos que debamos tener en cuenta, estamos seguros de que cualquier uso oculto o inesperado de this estará restringido al objeto vacío, que aísla el objeto global de nuestro programa de los efectos secundarios.

Dado que este objeto está totalmente vacío, personalmente me gusta darle el nombre de variable ø (el símbolo matemático en minúsculas para el conjunto vacío). En muchos teclados (como el US-layout en Mac), este símbolo se escribe fácilmente con ⌥ + o (option + o) (en Linux Alt Gr + o). Algunos sistemas también permiten configurar teclas de acceso rápido para símbolos específicos. Si no te gusta el símbolo ø, o tu teclado no lo hace tan fácil de escribir, por supuesto puedes llamarlo como quieras.

Cualquiera que sea su nombre, la forma más fácil de configurarlo como totalmente vacío es Object.create(null) (consulte el Capítulo 5). Object.create(null) es similar a { } , pero sin la delegación a Object.prototype, por lo que es «más vacío» que sólo { }.

No sólo funcionalmente «más seguro», hay una especie de beneficio estilístico para ø, en el sentido de que transmite semánticamente «Quiero que el this esté vacío» un poco más claramente que lo que lo hace null. Pero de nuevo, nombre su objeto DMZ como prefiera.

Indirección (referencia indirecta o «desreferencia»)

Otra cosa que hay que tener en cuenta es que se pueden (¡intencionadamente o no!) crear «referencias indirectas» a funciones, y en esos casos, cuando se invoca esa referencia de función, la vinculación por defecto también se aplica.

Una de las maneras más comunes en que las referencias indirectas ocurren es a partir de una asignación:

El valor de resultado de la expresión de asignación p.foo = o.foo es una referencia sólo al objeto de función subyacente. Como tal, el sitio de llamada efectivo es sólo foo(), no p.foo() o o.foo() como se podría esperar. Según las normas anteriores, se aplica la norma de vinculación por defecto.

Recordatorio: independientemente de cómo se llegue a la invocación de una función utilizando la regla de vinculación por defecto, el estado de modo estricto del contenido de la función invocada que hace la referencia a this — no el sitio de llamada de la función — determina el valor de vinculación por defecto: o bien el objeto global si está en modo no estrictoo bien undefined si está en modo estricto.

Suavizado de la vinculación

Vimos antes que la vinculación fuerte era una estrategia para evitar que una llamada de función volviera a caer en la regla de vinculación por defecto inadvertidamente, forzándola a estar vinaculada a un this específico (¡a menos que se use new para anularlo!). El problema es que la vinculación fuerte reduce en gran medida la flexibilidad de una función, lo que impide la anulación manual de this con la vinculación implícita o incluso los intentos posteriores de vinculación explícita.

Sería bueno que hubiera una manera de proporcionar un valor por defecto diferente para la vinculación por defecto (no global o undefined), mientras se deja que la función pueda vincular manualmente this mediante técnicas de vinculación implícitas o explícitas.

Podemos construir una utilidad de vinculación «blanda» que emula nuestro comportamiento deseado.

La utilidad softBind(..) proporcionada aquí funciona de forma similar a la utilidad ES5 bind(..) incorporada, excepto por nuestro comportamiento de vinculación suave. Envuelve la función especificada en una lógica que comprueba this en el momento de la llamada y, si es global o undefined, utiliza un valor predeterminado alternativo por defecto( obj ). De lo contrario, el this se deja intacto. También proporciona «curry» opcional (ver la discusión de bind(...) más arriba).

Vamos a demostrar su uso:

La versión de vinculación blanda de la función foo() puede fijar manualmente this -vinculadanado a obj2 u obj3 como se muestra, pero vuelve a obj si la vinculación por defecto se aplica de otra manera.

this léxico

Las funciones normales se rigen por las 4 reglas que acabamos de cubrir. Pero ES6 introduce un tipo especial de función que no utiliza estas reglas: la función flecha.

Las funciones flecha no se asignan por la palabra clave function, sino por el operador =>llamado «flecha gorda». En lugar de utilizar las cuatro reglas estándar this, las funciones flecha adoptan la vinculación de this del alcance adjunto (función o global).

Vamos a ilustrar el alcance léxico de la función flecha:

La función flecha creada en foo() captura léxicamente lo que sea que sea el this de foo() en su tiempo de llamada. Puesto que foo() tenía this vinculado a obj1, bar(una referencia a la función de flecha devuelta) también tendrá this -vinculado a obj1. La unión léxica de una función flecha no puede ser anulada (incluso con new !).

El caso de uso más común probablemente será en el uso de rellamadas, tales como manejadores de eventos o temporizadores:

Mientras que las funciones flecha proporcionan una alternativa al uso de bind(...) en una función para asegurar su this, que puede parecer atractivo, es importante notar que esencialmente están desactivando el mecanismo tradicional this a favor de un alcance léxico más ampliamente entendido. Antes de ES6, ya tenemos un patrón bastante común para hacerlo, que básicamente es casi indistinguible del espíritu de las funciones de flecha de ES6:

Mientras que self = this y las funciones flecha parecen buenas «soluciones» para no querer usar bind(...), esencialmente están huyendo de this en lugar de entenderlo y abrazarlo.

Si te encuentras escribiendo código de estilo this, pero la mayor parte o todo el tiempo, derrotas el mecanismo this con «trucos» como el mecanismo léxico self = this o la función flecha, quizás deberías hacer:

  1. Utiliza sólo el ámbito léxico y olvídate de la falsa pretensión de código de estilo this.
  2. Acepta completamente los mecanismos de estilo this, incluyendo el uso de bind(...) donde sea necesario, y trata de evitar los trucos de self = this y el truco de this léxico de la función flecha.

Un programa puede usar efectivamente ambos estilos de código (léxico y this), pero dentro de la misma función, y de hecho para los mismos tipos de búsquedas, mezclar los dos mecanismos usualmente implica un código más difícil de mantener, y probablemente trabajar demasiado duro para ser claro.

Revisión

Para determinar la vinculación de this para una función de ejecución es necesario encontrar el lugar de llamada directa de esa función. Una vez examinadas, se pueden aplicar cuatro reglas al sitio de la llamada, en este orden de precedencia:

  1. Llamado con new? Utilice el objeto recién construido.
  2. ¿Llamado con call o apply (o bind)? Utilice el objeto especificado.
  3. ¿Llamado con un objeto de contexto que posee la llamada? Usa ese objeto de contexto.
  4. Predeterminado: undefined o en modo estricto, por lo demás objeto global.

Tenga cuidado de no invocar accidental o involuntariamente la norma vinculante predeterminada. En los casos en los que se desea «con seguridad» ignorar una vinculació de this, un objeto «DMZ» como ø = Object.create(null) es un buen valor de marcador de posición que protege el objeto global de efectos secundarios no deseados.

En lugar de las cuatro reglas de ǜinculación estándar, las funciones flecha ES6 utilizan la delimitación léxica para la vinculación de this, lo que significa que adoptan la vinculación de this (sea lo que sea) de su llamada de función de envolvente. Son esencialmente un reemplazo sintáctico de self = this en código pre-ES6.

5. Objetos

En los capítulos 1 y 2, explicamos cómo la vinculación de this apunta a varios objetos dependiendo del lugar de llamada de la invocación de la función. Pero, ¿qué son exactamente los objetos y por qué tenemos que señalarlos? Exploraremos los objetos en detalle en este capítulo.

Sintaxis

Los objetos vienen en dos formas: la forma declarativa (literal) y la forma construida.

La sintaxis literal de un objeto se ve así:

La forma construida se ve así:

La forma construida y la forma literal dan como resultado exactamente el mismo tipo de objeto. La única diferencia es que puede añadir uno o más pares clave/valor a la declaración literal, mientras que con los objetos construidos, debe añadir las propiedades una a una.

Nota: Es extremadamente raro usar la «forma construida» para crear objetos como se acaba de mostrar. Siempre querrás usar la forma literal de la sintaxis. Lo mismo ocurrirá con la mayoría de los objetos incorporados (ver abajo).

Tipo

Los objetos son la base general sobre la que se construye gran parte de JS. Son uno de los 6 tipos primarios (llamados «tipos de lenguaje» en la especificación) en JS:

  • string
  • number
  • boolean
  • null
  • undefined
  • object

Tenga en cuenta que los primitivos simples ( string, number, boolean, null y undefined ) no son objetos en sí mismos. null es a veces referido como un tipo de objeto, pero esta idea errónea proviene de un error en el lenguaje que causa que typeof nulldevuelva el string object incorrectamente (y confusamente). De hecho, null es su propio tipo primitivo.

Es un error común decir que «todo en JavaScript es un objeto». Esto claramente no es cierto.

Por el contrario, hay algunos subtipos de objetos especiales, a los que podemos referirnos como primitivos complejos.

function es un subtipo de objeto (técnicamente, un «objeto llamable»). Se dice que las funciones en JS son de «primera clase» en el sentido de que son básicamente objetos normales (con la semántica de comportamiento llamable unida), y por lo tanto pueden ser manejadas como cualquier otro objeto plano.

Las matrices son también una forma de objetos, con un comportamiento extra. La organización de los contenidos en matrices es ligeramente más estructurada que para los objetos generales.

Objetos incorporados

Hay varios otros subtipos de objetos, normalmente denominados objetos incorporados. Para algunos de ellos, sus nombres parecen implicar que están directamente relacionados con sus simples primitivas contra-pero de hecho, su relación es más complicada, la cual exploraremos en breve.

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

Estos objetos integrados tienen la apariencia de ser tipos reales, incluso clases, si se confía en la similitud con otros lenguajes como la clase String de Java.

Pero en JS, en realidad son sólo funciones incorporadas. Cada una de estas funciones incorporadas puede ser utilizada como constructor (es decir, una llamada de función con el operador new — ver Capítulo 2), con el resultado de un nuevo objeto construido del subtipo en cuestión. Por ejemplo:

Veremos en detalle en un capítulo posterior cómo funciona exactamente Object.prototype.toString..., pero brevemente, podemos inspeccionar el subtipo interno tomando prestado el método base por defecto toString(), y se puede ver que revela que strObject es un objeto que de hecho fue creado por el constructor de String.

El valor primitivo «Yo soy una cadena» no es un objeto, es un valor primitivo literal e inmutable. Para realizar operaciones sobre él, como comprobar su longitud, acceder a su contenido de caracteres individuales, etc., se necesita un objeto String.

Afortunadamente, el lenguaje coacciona automáticamente una «cadena» primitiva a un objeto String cuando es necesario, lo que significa que casi nunca es necesario crear explícitamente el Objeto. La mayoría de la comunidad de JS prefiere usar la forma literal para un valor, cuando sea posible, en lugar de la forma de objeto construido.

Considere:

En ambos casos, llamamos a una propiedad o método en un string primitivo, y el motor lo convierte automáticamente a un objeto String, de forma que el acceso a la propiedad/método funciona.

El mismo tipo de coerción ocurre entre el número literal primitivo 42 y la nueva envoltura de objeto new Number(42), cuando se usan métodos como 42.359.toFixed(2). Lo mismo ocurre con los objetos booleanos de los primitivos "booleanos".

null y undefined no tienen forma de envoltura de objeto, sólo sus valores primitivos. Por el contrario, los valores Date sólo se pueden crear con su forma de objeto construida, ya que no tienen una forma literal.

Los Object, Array, Function y RegExp (expresiones regulares) son todos objetos independientemente de si se utiliza la forma literal o construida. La forma construida ofrece, en algunos casos, más opciones de creación que la forma literal. Dado que los objetos se crean de cualquier manera, la forma literal más simple es casi universalmente preferible. Sólo utilice la forma construida si necesita las opciones adicionales.

Los objetos Error raramente se crean explícitamente en el código, pero normalmente se crean automáticamente cuando se lanzan excepciones. Se pueden crear con la forma construida new Error(...), pero a menudo es innecesario.

Contenido

Como se mencionó anteriormente, el contenido de un objeto consiste en valores (de cualquier tipo) almacenados en ubicaciones específicamente nombradas, a las que llamamos propiedades.

Es importante tener en cuenta que mientras decimos «contenido» lo que implica que estos valores se almacenan dentro del objeto, eso es simplemente una apariencia. El motor almacena valores de forma dependiente de la implementación y puede que no los almacene en algún contenedor de objetos. Lo que se almacena en el contenedor son estos nombres de propiedad, que actúan como indicadores (técnicamente, referencias) de dónde se almacenan los valores.

Considere:

Para acceder al valor de la ubicación a en myObject, necesitamos usar el operador . o el operador [ ]. La sintaxis .a se suele denominar acceso por «propiedad», mientras que la sintaxis ["a"] se suele denominar acceso por «clave». En realidad, ambos acceden a la misma ubicación y sacan el mismo valor, por lo que los términos pueden utilizarse indistintamente. A partir de ahora utilizaremos el término más común, «acceso a la propiedad».

La principal diferencia entre las dos sintaxis es que el operador . requiere un Identificador compatible con el nombre de propiedad tras él, mientras que la sintaxis ["..."] puede tomar básicamente cualquier string compatible con UTF-8/unicode como nombre para la propiedad. Para hacer referencia a una propiedad con el nombre «Super-Fun!, por ejemplo, ha de usarsela sintaxis [«Super-Fun!»], dado que Super-Fun! no es una Identificador válido para el nombre de la propiedad.

Además, dado que la sintaxis ["..."] utiliza el valor de un string para especificar la ubicación, esto significa que el programa puede construir programáticamente el valor de la cadena, como por ejemplo:

En los objetos, los nombres de propiedad son siempre un string. Si utiliza cualquier otro valor además de un string (primitivo) como propiedad, primero se convertirá en un string. Esto incluye incluso los números, que son comúnmente usados como índices de matrices, así que tenga cuidado de no confundir el uso de números entre objetos y matrices.

Nombres de propiedades calculados

La sintaxis de acceso a la propiedad myObject[...] que acabamos de describir es útil si necesita utilizar un valor de expresión calculado como nombre de clave, como myObject[prefijo + nombre]. Pero eso no es realmente útil cuando se declaran objetos usando la sintaxis objeto-literal.

ES6 añade nombres de propiedades calculados, donde puede especificar una expresión, rodeada por un par [ ], en la posición de nombre-clave de una declaración objeto-literal:

El uso más común de los nombres de propiedad calculados será probablemente para los Symbol de ES6, que no trataremos en detalle en este libro. En resumen, son un nuevo tipo de datos primitivos que tienen un valor opaco e inimaginable (técnicamente un valor string). Se le desaconsejará trabajar con el valor real de Symbol (que teóricamente puede ser diferente entre diferentes motores JS), por lo que el nombre del Symbol, como Symbol.Algo (¡sólo un nombre inventado!), será el que utilice:

Propiedad vs. Método

A algunos desarrolladores les gusta hacer una distinción al hablar de un acceso a la propiedad sobre un objeto, si el valor al que se accede es una función. Debido a que es tentador pensar en la función como perteneciente al objeto, y en otros lenguajes, las funciones que pertenecen a objetos (aka, «clases») son referidas como «métodos», no es raro escuchar, «acceso por método» en oposición a «acceso por propiedad».

La especificación hace esta misma distinción, curiosamente.

Técnicamente, las funciones nunca «pertenecen» a objetos, por lo que decir que una función a la que se accede por casualidad en una referencia de objeto es automáticamente un «método» parece un poco semántico.

Es cierto que algunas funciones tienen referencias this en ellas, y que a veces estos thisse refieren a la referencia de objeto en el emplazamiento. Pero este uso realmente no hace que esa función sea más un «método» que cualquier otra función, ya que está ligada dinámicamente en tiempo de ejecución, en el lugar de la llamada, y por lo tanto su relación con el objeto es indirecta, en el mejor de los casos.

Cada vez que se accede a una propiedad sobre un objeto, se trata de un acceso por propiedad, independientemente del tipo de valor que se recupere. Si usted obtiene una función de ese acceso a la propiedad, no es mágicamente un método» en ese momento. No hay nada especial (fuera de lo posible implícita esta vinculación como se ha explicado anteriormente) acerca de una función que proviene de un acceso de propiedad. Las funciones que pertenecen a objetos (también conocidos como «clases») se denominan «métodos», no es infrecuente escuchar, «acceso de método» en lugar de «acceso de propiedad».

Por ejemplo:

someFoo y myObject.someFoo son sólo dos referencias separadas a la misma función, y ninguna implica que la función sea especial o «propiedad» de ningún otro objeto. Si foo() se definió para tener una referencia this dentro de él, esa vinculación implícita de myObject.someFoo sería la única diferencia observable entre las dos referencias. Ninguna de las dos referencias tiene realmente sentido llamarse «método».

Tal vez se podría argumentar que una función se convierte en un método, no en el momento de la definición, sino durante el tiempo de ejecución sólo para esa invocación, dependiendo de cómo se llame en su lugar de llamada (con o sin contexto de referencia de objeto – ver Capítulo 2 para más detalles). Incluso esta interpretación es un poco exagerada.

La conclusión más segura es probablemente que «function» y «method» son intercambiables en JavaScript.

Nota: ES6 añade una referencia super, que típicamente se va a usar con clase (ver Apéndice A). La forma en que se comporta super (vinculación estática en lugar de vinculación tardía como this) da más peso a la idea de que una función que vincula superen alguna parte es más un «método» que una «función». Pero de nuevo, estos son sólo sutiles matices semánticos (y mecánicos).

Incluso cuando se declara una expresión de función como parte del objeto-literal, esa función no pertenece mágicamente más al objeto — incluso así, sólo hay múltiples referencias al mismo objeto de función:

Nota: En el Capítulo 6, cubriremos una abreviatura ES6 para foo: función foo(){...}en la sintaxis de la declaración en nuestro objeto literal.

Arrays

Las matrices también utilizan forma de acceso [ ], pero como se mencionó anteriormente, tienen una organización un poco más estructurada sobre cómo y dónde se almacenan los valores (aunque todavía no hay restricciones sobre qué tipo de valores se almacenan). Los arrays asumen indexación numérica, lo que significa que los valores se almacenan en ubicaciones, usualmente llamadas índices, en números enteros no negativos, tales como 0 y 42.

Los arrays son objetos, así que aunque cada índice es un número entero positivo, también puede agregar propiedades al array:

Tenga en cuenta que la adición de propiedades nombradas (sin tener en cuenta la sintaxis del operador . o [ ]) no cambia la longitud reportada del array.

Podría utilizar un array como un objeto de clave/valor simple, y nunca añadir índices numéricos, pero esto es una mala idea porque los arrays tienen un comportamiento y optimizaciones específicas para su uso previsto, y del mismo modo con objetos simples. Utilice objetos para almacenar pares clave/valor y matrices para almacenar valores en índices numéricos.

Tenga cuidado: Si intenta añadir una propiedad a un array, pero el nombre de la propiedad parece un número, terminará como un índice numérico (modificando así el contenido del array):

Duplicación de objetos

Una de las características más solicitadas cuando los desarrolladores recién asumen el lenguaje JavaScript es cómo duplicar un objeto. Parece que debería haber un método copy() incorporado, ¿verdad? Resulta que es un poco más complicado que eso, porque no está completamente claro cuál, por defecto, debería ser el algoritmo para la duplicación.

Por ejemplo, considere este objeto:

¿Cuál debería ser exactamente la representación de una copia de myObject?

En primer lugar, debemos responder si debe ser una copia superficial o profunda? Una copia superficial terminaría con a en el nuevo objeto como una copia del valor 2, pero las propiedades b, c, y d como referencias a los mismos lugares que las referencias en el objeto original. Una copia profunda duplicaría no sólo myObject, sino anotherObject y anotherArray. Pero luego tenemos problemas de que anotherArray tiene referencias a anotherObjet y a myObjet en él, por lo que también deberían duplicarse en lugar de reservarse como referencia. Ahora tenemos un problema infinito de duplicación circular debido a la referencia circular.

¿Deberíamos detectar una referencia circular y simplemente romper la relación circular transversal (dejando el elemento profundo no totalmente duplicado)? ¿Deberíamos equivocarnos completamente? ¿Algo en el medio? Además, no está muy claro lo que significaría «duplicar» una función? Hay algunos trucos como realizar una serialización toString() del código fuente de una función (que varía a través de implementaciones y no es confiable en todos los motores dependiendo del tipo de función que se está inspeccionando).

Entonces, ¿cómo resolvemos todas estas preguntas difíciles? Varios frameworks de trabajo de JS han escogido cada uno sus propias interpretaciones y han tomado sus propias decisiones. Pero, ¿cuál de estos (si los hay) debería adoptar JS como estándar? Durante mucho tiempo, no hubo una respuesta clara.

Una solución de subconjunto es que los objetos que son seguros para JSON (es decir, que pueden ser serializados a una cadena JSON y luego re-parseados a un objeto con la misma estructura y valores) pueden ser fácilmente duplicados con:

Por supuesto, eso requiere que usted se asegure de que su objeto es seguro para JSON. Para algunas situaciones, eso es trivial. Para otros, es insuficiente.

Al mismo tiempo, una copia superficial es bastante comprensible y tiene muchos menos problemas, por lo que ES6 ha definido Object.assign(..) para esta tarea. Object.assign(...) toma un objeto de destino como primer parámetro y uno o más objetos fuente como parámetros posteriores. Se itera sobre todo lo enumerable (ver abajo), claves poseídas (inmediatamente presente) en el objeto(s) de origen y los copia (vía = sólo asignación) al destino. También, amablemente, devuelve el objetivo, como se puede ver a continuación:

Nota: En la siguiente sección, describimos «descriptores de propiedades» (características de propiedades) y mostramos el uso de Object.defineProperty(..). Sin embargo, la duplicación que se produce para Object.assign(...) es puramente de tipo asignación, por lo que cualquier característica especial de una propiedad (como writable) en un objeto de origen se conserva en el objeto de destino.

Descriptores de la propiedad

Antes de ES5, el lenguaje JavaScript no permitía que su código inspeccionara o distinguiera entre las características de las propiedades, como por ejemplo si la propiedad era de sólo lectura o no.

Pero a partir de ES5, todas las propiedades se describen en términos de un descriptor de propiedad.

Considere este código:

Como puede ver, el descriptor de la propiedad (llamado «descriptor de datos» ya que es sólo para mantener un valor de datos) para nuestra propiedad de objeto normal a es mucho más que su valor de 2. Incluye otras 3 características: writable, enumerable y configurable.

Aunque podemos ver cuáles son los valores por defecto de las características del descriptor de la propiedad cuando creamos una propiedad normal, podemos usar Object.defineProperty(..) para añadir una nueva propiedad, o modificar una existente (si es configurable), con las características deseadas.

Por ejemplo:

Usando defineProperty(...), añadimos la propiedad normal a a myObject de una manera explícita manualmente. Sin embargo, usted generalmente no usaría este enfoque manual a menos que quisiera modificar una de las características del descriptor de su comportamiento normal.

Writable

La capacidad de cambiar el valor de una propiedad es controlada por writable. Considere:

Como pueden ver, nuestra modificación del valor falló silenciosamente. Si lo intentamos en modo estricto, obtenemos un error:

TypeError nos dice que no podemos cambiar una propiedad no escribible.

Nota: Discutiremos getters/setters pronto, pero brevemente, usted puede observar que writable:false significa que un valor no puede ser cambiado, lo cual es algo equivalente a si usted definió un setter no operable (no modificable). En realidad, su configurador no operable necesitaría lanzar un TypeError cuando es llamado, para ser verdaderamente conforme a writable:false.

Configurable

Siempre que una propiedad sea actualmente configurable, podemos modificar la definición de su descriptor, utilizando la misma utilidad defineProperty(..).

La llamada final defineProperty(..) resulta en un TypeError, independientemente del modo estricto, si intenta cambiar la definición del descriptor de una propiedad no configurable. Tenga cuidado: como puede ver, cambiar de configurable a false es una acción unidireccional, ¡y no se puede deshacer!

Nota: Hay una excepción matizada que hay que tener en cuenta: incluso si la propiedad ya es configurable:false, writable siempre se puede cambiar de true a false sin error, pero no uede volver a true si ya es false.

Otra cosa configurable:false previene la posibilidad de utilizar el operador delet para eliminar una propiedad existente.

Como puede ver, el último delete falló (silenciosamente) porque hicimos que la propiedad a no fuera configurable.

delete sólo se utiliza para eliminar propiedades de objeto (que se pueden eliminar) directamente del objeto en cuestión. Si una propiedad de objeto es la última referencia restante a algún objeto/función, y se borra, se elimina la referencia y ahora el objeto/función no referenciado puede recogerse en la «basura». Pero, no es apropiado pensar en el borrado como una herramienta para liberar memoria asignada como lo hace en otros idiomas (como C/C++). delete es sólo una operación de eliminación de propiedad de objeto — nada más.

Enumerable

La característica final del descriptor que mencionaremos aquí (hay otras dos, que trataremos en breve cuando hablemos de getter/setters) es enumerable.

El nombre probablemente lo hace obvio, pero esta característica controla si una propiedad aparecerá en ciertas enumeraciones objeto-propiedad, como la del bucle for...in. Estableciéndola a false para evitar que aparezca en tales enumeraciones, a pesar de que todavía es completamente accesible. Estableciendo a true para mantenerla presente.

Todas las propiedades normales definidas por el usuario son enumerable por defecto, ya que esto es lo más habitual. Pero si tienes una propiedad especial que quieres ocultar de la enumeración, establece enumerable:false.

Demostraremos la enumerabilidad con mucho más detalle en breve, así que mantén un marcador mental en
este tema.

Como puede ver, la última llamada eliminada falló (silenciosamente) porque hicimos que la propiedad a no fuera configurable.

Inmutabilidad

A veces se desea hacer propiedades u objetos que no puedan ser alterados (ya sea por accidente o intencionalmente). ES5 añade soporte para el manejo de esto en una variedad de diferentes matices.

Es importante notar que todos estos enfoques crean una inmutabilidad superficial. Es decir, afectan sólo al objeto y a sus características de propiedad directas. Si un objeto tiene una referencia a otro objeto (array, objeto, función, etc), el contenido de ese objeto no se ve afectado y permanece mutable.

Nota: No es muy común crear objetos inmutables profundamente arraigados en los programas de JS. Los casos especiales ciertamente pueden requerirlo, pero como patrón general de diseño, si usted desea sellar o congelar todos sus objetos, es posible que desee dar un paso atrás y reconsiderar el diseño de su programa para que sea más resistente a los posibles cambios en los valores de los objetos.

Constante de objeto

Combinando writable:false y configurable:false, puede crear esencialmente una constante (que no puede ser cambiada, redefinida o borrada) como una propiedad de objeto, como:

Prevenir Extensiones

Si desea evitar que se añadan nuevas propiedades a un objeto, pero dejar el resto de las propiedades del objeto en paz, llame a Object.preventExtensions(..):

Sellar

Object.seal(...) crea un objeto «sellado», lo que significa que toma un objeto existente y esencialmente llama Object.preventExtensions(...) sobre él, pero también marca todas sus propiedades existentes como configurables:false.

Por lo tanto, no sólo no puede añadir más propiedades, sino que tampoco puede reconfigurar o eliminar ninguna de las propiedades existentes (aunque todavía puede modificar sus valores).

Congelar

Object.freeze(...) crea un objeto «congelado», lo que significa que toma un objeto existente y esencialmente llama Object.seal(...) sobre él, pero también marca todas las propiedades «data accessor» como writable:false, de modo que sus valores no pueden ser cambiados.

Este enfoque es el nivel más alto de inmutabilidad que se puede alcanzar para un objeto en sí, ya que evita cualquier cambio en el objeto o en cualquiera de sus propiedades directas (aunque, como se mencionó anteriormente, el contenido de cualquier otro objeto referenciado no se ve afectado).

Puede «congelar» un objeto llamando a Object.freeze(...) en el objeto, y luego iterando recursivamente sobre todos los objetos a los que hace referencia (que no se habrían visto afectados hasta ahora), y llamar a Object.freeze(...) sobre ellos también. Sin embargo, tenga cuidado, ya que eso podría afectar a otros objetos (compartidos) que no pretendías afectar.

[[Get]]

Hay un detalle sutil, pero importante, sobre cómo se realizan los accesos a las propiedades.

Considere:

myObject.a es un acceso a propiedad, pero no sólo busca en myObject una propiedad del nombre a, como podría parecer.

De acuerdo con la especificación, el código anterior en realidad realiza una operación [[Get]] (algo así como una llamada de función: [[Get]]()) en myObject. La operación [[Get]] incorporada por defecto para un objeto primero inspecciona el objeto para una propiedad del nombre solicitado, y si lo encuentra, devolverá el valor en consecuencia.

Sin embargo, el algoritmo [[Get]] define otro comportamiento importante si no encuentra una propiedad del nombre solicitado. Examinaremos en el Capítulo 5 lo que sucede a continuación (recorrido de la cadena de [[Prototype]], si la hay).

Pero un resultado importante de esta operación [[Get]] es que si no puede por ningún medio obtener un valor para la propiedad solicitada, en su lugar devuelve el valor undefined.

Este comportamiento es diferente de cuando se hace referencia a las variables por sus nombres de identificador. Si se hace referencia a una variable que no puede ser resuelta dentro de la búsqueda de alcance léxico aplicable, el resultado no es undefined como lo es para las propiedades del objeto, sino que se lanza un ReferenceError.

Desde una perspectiva de valor, no hay diferencia entre estas dos referencias — ambas resultan en undefined. Sin embargo, la operación [[Get]] de abajo, aunque sutil a primera vista, potencialmente realizaba un poco más de «trabajo» para la referencia myObject.b que para la referencia myObject.a.

Inspeccionando sólo los resultados del valor, no se puede distinguir si una propiedad existe y tiene el valor explícito undefined, o si la propiedad no existe y fue el valor de retorno por defecto después de que [[Get]] no devolviera algo explícitamente. Sin embargo, veremos en breve cómo se pueden distinguir estos dos escenarios.

[[Put]]

Como hay una operación [[Get]] definida internamente para obtener un valor de una propiedad, debería ser obvio que también hay una operación [[Put]] por defecto.

Puede ser tentador pensar que una asignación a una propiedad en un objeto simplemente invocaría [[Put]] para establecer o crear esa propiedad en el objeto en cuestión. Pero la situación etiene más matizes que eso.

Cuando se invoca [[Put]], la forma en que se comporta difiere en función de una serie de factores, incluyendo (de manera más impactante) si la propiedad ya está presente en el objeto o no.

Si la propiedad está presente, el algoritmo [[Put]] se comprobará aproximadamente:

  1. ¿Es la propiedad un descriptor de accesorios (ver la sección «Getters & Setters» más abajo)? Si es así, llame al setter, si lo hay.
  2. ¿Es la propiedad un descriptor de datos con writable = false? ** Si es así, falla silenciosamente en modo no estricto, o tira TypeError en modo estricto.**
  3. De lo contrario, establece el valor de la propiedad existente normalmente.

Si la propiedad todavía no está presente en el objeto en cuestión, la operación [[Put]] tiene aún más matizes. Revisaremos este escenario en el Capítulo 5 cuando discutamos [[Prototype]] para darle más claridad.

Getters & Setters

Las operaciones por defecto [[Put]] y [[Get]] para objetos controlan completamente cómo se ajustan los valores a las propiedades existentes o nuevas, o cómo se recuperan de las propiedades existentes, respectivamente.

Nota: Usando las capacidades futuras/avanzadas del lenguaje, puede ser posible anular las operaciones por defecto [[Get]] o [[Put]] para un objeto entero (no sólo por propiedad). Esto está más allá del alcance de nuestra discusión en este libro, pero será tratado más adelante en la serie «You Don’t Know JS».

ES5 introdujo una forma de anular parte de estas operaciones por defecto, no a nivel de objeto sino a nivel de propiedad, mediante el uso de getters y setters. Los Getters son propiedades que en realidad llaman a una función oculta para recuperar un valor. Los setters son propiedades que en realidad llaman a una función oculta para fijar un valor.

Cuando se define una propiedad para que tenga un getter o un setter o ambos, su definición se convierte en un «descriptor de accesorios» (a diferencia de un «descriptor de datos»). Para los descriptores de accesorios, las características value y grabable del descriptor son irrelevantes e ignoradas, y en su lugar JS considera las características set y get de la propiedad (así como configurable y enumerable). Considere:

Ya sea a través de la sintaxis objeto-literal con get a() {...} o mediante definición explícita con defineProperty(..), en ambos casos creamos una propiedad en el objeto que en realidad no tiene valor, pero cuyo acceso resulta automáticamente en una llamada de función oculta a la función getter, siendo el valor que devuelva el resultado del acceso a la propiedad.

Ya que sólo definimos un getter para a, si intentamos establecer el valor de a más tarde, la operación set no arrojará un error sino que arrojará silenciosamente la asignación a la basura. Incluso si hubiera un setter válido, nuestro getter personalizado está codificado para devolver sólo 2, por lo que la operación de set sería irrelevante.

Para hacer que este escenario sea más sensato, las propiedades también deben definirse con setters, que anulan la operación por defecto [[Put]] (también conocida como asignación), por propiedad, tal y como se espera. Es casi seguro que siempre querrá declarar tanto un getter como setter (tener sólo uno u otro a menudo lleva a un comportamiento inesperado/sorprendente):

Nota: En este ejemplo, en realidad almacenamos el valor especificado 2 de la asignación (operación [[Put]]) en otra variable _a_. El nombre _a_ es puramente por convención para este ejemplo e implica que no hay nada especial en su comportamiento – es una propiedad normal como cualquier otra.

Existencia

Anteriormente mostramos que un acceso a una propiedad como myObject.a puede dar lugar a un valor undefined si se almacena allí el valor undefined explícitamente o si una propiedad no existe en absoluto. Entonces, si el valor es el mismo en ambos casos, ¿de qué otra manera los distinguimos?

Podemos preguntarle a un objeto si tiene cierta propiedad sin pedirle el valor de esa propiedad:

El operador in comprobará si la propiedad está en el objeto, o si existe en cualquier nivel superior de la en el recrrido de la cadena de [[Prototype]] del objeto (ver Capítulo 5). Por el contrario, hasOwnProperty(..) comprueba si sólo myObject tiene la propiedad o no, y no consultará la cadena [[Prototype]]. Volveremos a las diferencias importantes entre estas dos operaciones en el Capítulo 5 cuando exploremos [[Prototype]] en detalle.

hasOwnProperty(..) es accesible para todos los objetos normales a través de la delegación a Object.prototype (ver Capítulo 5). Pero es posible crear un objeto que no esté vinculado a Object.prototype (a través de Object.create(null) — ver Capítulo 5). En este caso, una llamada a un método como myObject.hasOwnProperty(..) fallaría.

En ese escenario, una forma más robusta de realizar dicha comprobación es Object.prototype.hasOwnProperty.call(myObject, "a"), que toma prestado el método base hasOwnProperty(..) y utiliza explícitamente el vínculo this(ver Capítulo 2) para aplicarlo contra nuestro myObject.

Nota: El operador in tiene la apariencia de que comprobará la existencia de un valor dentro de un contenedor, pero en realidad comprueba la existencia de un nombre de propiedad. Esta diferencia es importante con respecto a los arrays, ya que la tentación de intentar una comprobación como 4 in [2, 4, 6] es fuerte, pero esto no se comportará como se esperaba.

Enumeración

Anteriormente, explicábamos brevemente la idea de «enumerabilidad» cuando mirábamos la característica del descriptor de la propiedad enumerable. Volvamos a eso y examinémoslo más de cerca detalle.

Notará que myObject.b existe y tiene un valor accesible, pero no aparece en un bucle for...in (aunque, sorprendentemente, se revela por la comprobación de la existencia del operador in). Esto se debe a que «enumerable» significa básicamente «será incluido si las propiedades del objeto son iteradas a través de del objeto».

Nota: los bucles for...in aplicados a matrices pueden dar resultados algo inesperados, en el sentido de que la enumeración de una matriz incluirá no sólo todos los índices numéricos, sino también cualquier propiedad enumerable. Es una buena idea usar for...in sólo en objetos, y el bucle for tradicional en iteraciones de índices numéricos para los valores almacenados en matrices.

Otra forma de distinguir entre propiedades enumerables y no enumerables:

propertyIsEnumerable(..) comprueba si el nombre de propiedad dado existe directamente en el objeto y también es enumerable:true.

Object.keys(..) devuelve un array de todas las propiedades enumerables, mientras que Object.getOwnPropertyNames(..) devuelve un array de todas las propiedades, enumerables o no.

Mientras que in y hasOwnProperty(..) difieren en si consultan la cadena [[Prototype]] o no, Object.keys(..) y Object.getOwnPropertyNames(..)ambos inspeccionan sólo el objeto directo especificado.

No hay (actualmente) ninguna manera incorporada de obtener una lista de todas las propiedades que sea equivalente a lo que el operador in consultaría (recorriendo todas las propiedades en toda la cadena [[Prototype]], como se explica en el Capítulo 5). Usted podría aproximar tal utilidad recorriendo recursivamente la cadena [[Prototype]] de un objeto, y para cada nivel, capturando la lista de Object.keys(...) — sólo propiedades enumerables.

Iteración

El bucle for..in itera sobre la lista de propiedades enumerables de un objeto (incluyendo su cadena [[Prototype]]). ¿Pero qué pasa si en vez de eso quieres iterar sobre los valores?

Con matrices indexadas numéricamente, la iteración sobre los valores se hace típicamente con un bucle estándar for, como:

Esto no es iterar sobre los valores, sino sobre los índices, donde se usa el índice para referenciar el valor, como myArray[i].

ES5 también agregó varios asistentes de iteración para arrays, incluyendo forEach(...), every(...), y some(...). Cada uno de estos ayudantes acepta una función callback para aplicar a cada elemento del array, diferenciándose sólo en que responden respectivamente a un valor de retorno de la callback.

forEach(...) iterará sobre todos los valores del array, e ignora cualquier valor de retorno de llamada. every(...) continúa hasta el final o hasta que la llamada de retorno devuelva un valor false (o «falsy»), mientras que some(...) continúa hasta el final o hasta que la llamada de retorno devuelva un valor true (o «truthy»).

Estos valores de retorno especiales dentro de every(...), y some(...) actúan como una declaración de ruptura dentro de un bucle normal, en el sentido de que detienen la iteración antes de que llegue al final.

Si itera en un objeto con un bucle for...in, también está obteniendo sólo los valores indirectamente, porque en realidad itera sólo sobre las propiedades enumerables del objeto, lo que le permite acceder a las propiedades manualmente para obtener los valores.

Nota: En contraste con la iteración sobre los índices de un array de forma ordenada numéricamente (para bucles for u otros iteradores), el orden de iteración sobre las propiedades de un objeto no está garantizado y puede variar entre diferentes motores JS. No confíe en ningún orden observado para nada que requiera coherencia entre entornos, ya que cualquier orden observado no es fiable.

Pero, ¿qué pasa si quiere iterar los valores directamente en lugar de los índices de la matriz (o las propiedades de los objetos)? Útilmente, ES6 agrega una sintaxis for...of para iterar sobre matrices (y objetos, si el objeto define su propio iterador personalizado):

El bucle for..of pide un objeto iterador (de una función interna por defecto conocida como @@iterator en jerga especializada) de la cosa a ser iterada, y el bucle entonces itera sobre los sucesivos valores de retorno de llamar al método next() de ese objeto iterador, una vez para cada iteración de bucle.

Las matrices tienen un @@itrator incorporado, así que for...of trabaja fácilmente sobre ellas, como se muestra. Pero vamos a iterar manualmente el array, usando el @@@iterador incorporado, para ver cómo funciona:

Nota: Se obtiene en la propiedad interna @@iterator de un objeto utilizando un Símbolo ES6: Symbol.iterator. Mencionamos brevemente la semántica de símbolos anteriormente en el capítulo (ver «Nombres de propiedades calculadas»), por lo que aquí se aplica el mismo razonamiento. Siempre querrá hacer referencia a estas propiedades especiales por referencia a un símbolo en lugar de por el valor especial que puede tener. Además, a pesar de las implicaciones del nombre, @@iterator no es el objeto iterador en sí, sino una función que devuelve el objeto iterador — ¡un detalle sutil pero importante!

Como revela el fragmento anterior, el valor de retorno de la llamada next() de un iterador es un objeto de la forma { value: .. , done: ..} donde el value es el valor de iteración actual, y done es un boolean que indica si hay más para iterar.

Note que el valor 3 fue devuelto con un done:false, lo que parece extraño a primera vista. Tienes que llamar a next() una cuarta vez (lo que hace automáticamente el bucle for...of en el fragmento anterior) para obtener done:true y saber que realmente has terminado de iterar. La razón de esta rareza está más allá del alcance de lo que discutiremos aquí, pero proviene de la semántica de las funciones del generador ES6.

Mientras que las matrices se iteran automáticamente en bucles for...of, los objetos regulares no tienen un @@iterador incorporado. Las razones de esta omisión intencional son más complejas de lo que examinaremos aquí, pero en general era mejor no incluir alguna implementación que pudiera resultar problemática para futuros tipos de objetos.

Es posible definir su propio @@iterador por defecto para cualquier objeto que desee iterar. Por ejemplo:

Nota: Usamos Object.defineProperty(..) para definir nuestro @@iteratorpersonalizado (mayormente para que pudiéramos hacerlo no enumerable), pero usando Symbol como un nombre de propiedad computado (cubierto anteriormente en este capítulo), podríamos haberlo declarado directamente, como var myObject = {a:2, b:3,[Symbol.iterator]: funtion(){ /*.. */ } }.

Cada vez que el bucle for..of llama a next() en el iterador del objeto myObject, el puntero interno avanzará y regresará el siguiente valor de la lista de propiedades del objeto (vea una nota anterior sobre el orden de iteración de las propiedades/valores del objeto).

La iteración que acabamos de demostrar es una iteración simple valor por valor, pero, por supuesto, puede definir iteraciones arbitrariamente complejas para sus estructuras de datos personalizadas, como desee. Los iteradores personalizados combinados con los for..of de ES6 son una nueva y poderosa herramienta sintáctica que formula objetos definidos por el usuario.

Por ejemplo, una lista de objetos Pixel (con valores de coordenadas x e y) podría decidir ordenar su iteración basándose en la distancia lineal desde el origen (0,0), o filtrar puntos que están «demasiado lejos», etc. Siempre y cuando su iterador devuelva el { value: ... } esperado y devuelven los valores de las llamadas next(), y { done: true } después de completar la iteración, el for..of de ES6’s puede iterar sobre él

De hecho, puede incluso generar iteradores «infinitos» que nunca «terminan» y siempre devuelven un nuevo valor (como un número aleatorio, un valor incrementado, un identificador único, etc.), aunque probablemente no usará tales iteradores con un bucle for..of ilimitado, ya que nunca terminaría y colgaría su programa.

Este iterador generará números aleatorios «para siempre», así que tenemos cuidado de sacar sólo 100 valores para que nuestro programa no se cuelgue.

Revisión (TL;DR)

Los objetos en JS tienen tanto una forma literal (como var a = {... }) como una forma construida (como var a = new Array(...)). Casi siempre se prefiere la forma literal, pero la forma construida ofrece, en algunos casos, más opciones de creación.

Muchas personas afirman erróneamente que «todo en JavaScript es un objeto», pero esto es incorrecto. Los objetos son uno de los 6 (o 7, dependiendo de su perspectiva) tipos primitivos. Los objetos tienen subtipos, incluyendo la function, y también pueden estar especializados en el comportamiento, como [object Array] como etiqueta interna que representa el subtipo del objeto array.

Los objetos son colecciones de pares clave/valor. Se puede acceder a los valores como propiedades, a través de la sintaxis .propName o ["propName"]. Cada vez que se accede a una propiedad, el motor invoca la operación interna por defecto [[Get]] (y [[Put]] para establecer los valores), que no sólo busca la propiedad directamente en el objeto, sino que también atraviesa la cadena [[Prototype]] (ver Capítulo 5) si no se encuentra.

Las propiedades tienen ciertas características que pueden ser controladas a través de descriptores de propiedades, tales como writable y configurable. Además, los objetos pueden tener su mutabilidad (y la de sus propiedades) controlada a varios niveles de inmutabilidad usando Object.preventExtensions(..), Object.seal(..), y Object.freeze(..).

Las propiedades no tienen que contener valores — pueden ser también «propiedades accesorias», con getters/setters. También pueden ser enumerables o no, lo que controla si aparecen en iteraciones en el bucle for..in, por ejemplo.

También puede iterar los valores de las estructuras de datos (matrices, objetos, etc.) usando la sintaxis del bucle ES6 for..of, que busca un objeto @@itator incorporado o personalizado que consiste en un método next() para avanzar a través de los valores de los datos uno a la vez.

6. Mezclando Objetos de «Clase»

Siguiendo nuestra exploración de los objetos del capítulo anterior, es natural que ahora dirijamos nuestra atención a la «programación orientada a objetos (OO)», con «clases». En primer lugar, examinaremos la «orientación de clase» como un patrón de diseño, antes de examinar la mecánica de las «clases»: «instanciación», «herencia» y «polimorfismo (relativo)».

Veremos que estos conceptos no se mapean de forma muy natural al mecanismo del objeto en JS, y las medidas (mixins, etc.) que muchos desarrolladores de JavaScript utilizan para superar estos retos.

Nota: Este capítulo dedica bastante tiempo (¡la primera mitad!) a la teoría de la «programación orientada a objetos». Eventualmente relacionamos estas ideas con código JavaScript concreto y real en la segunda mitad, cuando hablamos de «Mixins». Pero hay mucho concepto y pseudo-código para vadear primero, así que no te pierdas… ¡sólo sigue con ello!

Teoría de la clase

Clase/Herencia describe una cierta forma de organización y arquitectura de código — una forma de modelar dominios problemáticos del mundo real en nuestro software.

OO o programación orientada a clases enfatiza que los datos intrínsecamente tienen un comportamiento asociado (por supuesto, ¡diferente dependiendo del tipo y naturaleza de los datos!) que opera sobre ellos, así que el diseño apropiado es empaquetar (también conocido como encapsular) los datos y el comportamiento juntos. Esto se denomina a veces «estructuras de datos» en la informática formal.

Por ejemplo, una serie de caracteres que representan una palabra o frase se suele llamar «string». Los caracteres son los datos. Pero casi nunca te preocupan sólo los datos, normalmente quieres hacer cosas con los datos, así que los comportamientos que pueden aplicarse a esos datos (calcular su longitud, añadir datos, buscar, etc.) están todos diseñados como métodos de una clase String.

Cualquier string dado es sólo una instancia de esta clase, lo que significa que es un empaquetado cuidadosamente recogido tanto de los datos de caracteres como de la funcionalidad que podemos realizar en el.

Las clases también implican una forma de clasificar una cierta estructura de datos. La forma en que lo hacemos es pensar en cualquier estructura como una variación específica de una definición básica más general.

Exploremos este proceso de clasificación mirando un ejemplo comúnmente citado. Un coche puede describirse como una implementación específica de una «clase» de cosas más general, llamada un vehículo.

Modelamos esta relación en software con clases definiendo una clase Vehículo (Vehicle) y una clase Coche (Car).

La definición de Vehicle podría incluir cosas como la propulsión (motores, etc.), la capacidad de transportar personas, etc., que serían todos los comportamientos. Lo que definimos en Vehicle es todo lo que es común a todos (o a la mayoría de) los diferentes tipos de vehículos (los «aviones, trenes y automóviles»).

Puede que no tenga sentido en nuestro software redefinir la esencia básica de la «capacidad de transportar personas» una y otra vez para cada tipo de vehículo. En lugar de eso, definimos esa capacidad una vez en Vehicle, y luego cuando definimos Car, simplemente indicamos que «hereda» (o «extiende») la definición base de Vehicle. La definición de Car se dice para especializar la definición general de Vehicle.

Mientras que Vehicle y Car definen colectivamente el comportamiento por medio de métodos, los datos en una instancia serían cosas como la mátrícula de un coche específico, etc.

Y así surgen las clases, la herencia y la instanciación.

Otro concepto clave en las clases es el «polimorfismo», que describe la idea de que un comportamiento general de una clase padre puede ser anulado en una clase hija para darle más detalles. De hecho, el polimorfismo relativo nos permite referenciar el comportamiento base a partir del comportamiento anulado.

La teoría de la clase sugiere fuertemente que una clase padre y una clase hija comparten el mismo nombre de método para un cierto comportamiento, de modo que el de la hija anula al del padre (diferencialmente). Como veremos más adelante, hacerlo en su código JavaScript es optar por la frustración y la fragilidad del código.

Patrón de Diseño de «Clase»

Es posible que nunca hayas pensado en las clases como un «patrón de diseño», ya que es más común ver la discusión de los populares «Patrones de Diseño OO», como «Iterator», «Observer», «Factory», «Singleton», etc. Como se presenta de esta manera, es casi una suposición que las clases OO son los mecanismos de nivel inferior por los cuales implementamos todos los patrones de diseño (de nivel superior), como si OO fuera una base dada para todo el código (apropiado).

Dependiendo de su nivel de educación formal en programación, es posible que haya oído hablar de la «programación procedimental» como una forma de describir el código que sólo consiste en procedimientos (también conocidos como funciones) que llaman a otras funciones, sin ninguna abstracción superior. Es posible que le hayan enseñado que las clases eran la manera correcta de transformar el «código espagueti» de estilo procedimental en un código bien formado y bien organizado.

Por supuesto, si tienes experiencia con la «programación funcional» (Monads «mónada» en castellano, etc.), sabes muy bien que las clases son sólo uno de varios patrones de diseño comunes. Pero para otros, esta puede ser la primera vez que se preguntan a si mismo si las clases realmente son una base fundamental para el aprendizaje, o si son una abstracción opcional encima del código.

Algunos lenguajes (como Java) no te dan la opción, así que no es muy opcional en absoluto –todo es una clase. Otros lenguajes como C/C++ o PHP le ofrecen sintaxis tanto procedimentales como orientadas a clases, y se deja más a la elección del desarrollador qué estilo o mezcla de estilos es apropiado.

«Clases» de JavaScript

¿Dónde está JavaScript en este sentido? JS ha tenido algunos elementos sintácticos de clase (como new y instanceof) durante bastante tiempo, y más recientemente en ES6, algunas adiciones, como la palabra clave class (ver Apéndice A).

¿Pero eso significa que JavaScript realmente tiene clases? Simple y llanamente: No.

Dado que las clases son un patrón de diseño, puede, con bastante esfuerzo (como veremos en el resto de este capítulo), implementar aproximaciones para gran parte de la funcionalidad clásica de clases. JS trata de satisfacer el deseo extremadamente generalizado de diseñar con clases, proporcionando una sintaxis similar a la de las clases.

Aunque tengamos una sintaxis que parece clases, es como si los mecánicos de JavaScript estuvieran luchando contra ti usando el patrón de diseño de clases, porque detrás del telón, los mecanismos sobre los que construyes están funcionando de forma muy diferente. El azúcar sintáctico y las librerías «Class» de JS (extremadamente utilizadas) contribuyen en gran medida a ocultarte esta realidad, pero tarde o temprano te enfrentarás al hecho de que las clases que tienes en otros idiomas no son como las «clases» que estás fingiendo en JS.

Esto se reduce a que las clases son un patrón opcional en el diseño de software, y usted tiene la opción de usarlas en JavaScript o no. Dado que muchos desarrolladores tienen una fuerte afinidad con el diseño de software orientado a clases, pasaremos el resto de este capítulo explorando lo que se necesita para mantener la ilusión de clases con lo que JS proporciona, y los puntos débiles que experimentamos.

Mecánica de clases

En muchos lenguajes orientados a clases, la «biblioteca estándar» proporciona una estructura de datos «stack» (push, pop, etc.) como clase Stack. Esta clase tendría un conjunto interno de variables que almacena los datos, y tendría un conjunto de comportamientos públicamente accesibles («métodos») proporcionados por la clase, lo que le da a su código la habilidad de interactuar con los datos (ocultos) (añadiendo y quitando datos, etc.).

Pero en tales idiomas, usted no opera directamente en el Stack (a menos que haga una referencia a un miembro de la clase de manera Stática, lo cual está fuera del alcance de nuestra discusión). La clase Stack es meramente una explicación abstracta de lo que cualquier «stack» debería hacer, pero no es en sí misma un «stack». Debe instanciar la clase Stack antes de tener una estructura de datos concreta contra la que operar.

Construcciones

La metáfora tradicional del pensamiento basado en la «clase» y la «instancia» proviene de la construcción de un edificio.

Un arquitecto proyecta todas las características de un edificio: cuán ancho, cuán alto, cuántas ventanas y en qué lugares, incluso qué tipo de material utilizar para las paredes y el techo. A ella no le importa necesariamente, en este momento, dónde se construirá el edificio, ni tampoco cuántas copias de ese edificio se construirán.

Tampoco le importa mucho el contenido del edificio: los muebles, el papel de pared, los ventiladores de techo, etc. sólo qué tipo de estructura los contendrá.

Los planos arquitectónicos que produce son sólo planos de un edificio. En realidad no constituyen un edificio en el que podamos entrar y sentarnos. Necesitamos un constructor para esa tarea. Un constructor tomará esos planos y los seguirá, exactamente, mientras construye el edificio. En un sentido muy real, está copiando las características previstas de los planos al edificio físico. Una vez terminado, el edificio es una instanciación física de los planos, ojalá una copia esencialmente perfecta. Y luego el constructor puede moverse al solar de al lado y hacerlo todo de nuevo, creando otra copia.

La relación entre el edificio y el plano es indirecta. Usted puede examinar un plano para entender cómo fue estructurado el edificio, para cualquier parte donde la inspección directa del edificio en sí fue insuficiente. Pero si quieres abrir una puerta, tienes que ir al edificio mismo — el plano simplemente tiene líneas dibujadas en una página que representan donde debería estar la puerta.

Una clase es un plano. Para obtener realmente un objeto con el que podamos interactuar, debemos construir (también conocido como «instanciar») algo de la clase. El resultado final de tal «construcción» es un objeto, típicamente llamado «instancia», sobre el que podemos llamar directamente a los métodos y acceder a cualquier propiedad de datos públicos, según sea necesario.

Este objeto es una copia de todas las características descritas por la clase.

Es probable que no espere entrar a un edificio y encontrar, enmarcado y colgado en la pared, una copia de los planos utilizados para planear el edificio, aunque los planos probablemente estén archivados en una oficina de registros públicos. De forma similar, generalmente no se utiliza una instancia de objeto para acceder directamente a su clase y manipularla, pero normalmente es posible al menos determinar de qué clase viene una instancia de objeto.

Es más útil considerar la relación directa de una clase con una instancia de objeto, que cualquier relación indirecta entre una instancia de objeto y la clase de la que proviene. Una clase se instancia en la forma de un objeto mediante una operación de copia.

 

Como puede ver, las flechas se mueven de izquierda a derecha, y de arriba hacia abajo, lo que indica las operaciones de copia que ocurren, tanto conceptual como físicamente.

Constructor

Las instancias de clases son construidas por un método especial de la clase, usualmente del mismo nombre que la clase, llamado constructor. El trabajo explícito de este método es inicializar cualquier información (estado) que la instancia necesite.

Por ejemplo, considere este pseudo-código suelto (sintaxis inventada) para clases:

Para hacer una instancia «CoolGuy», llamaríamos al constructor de la clase:

Note que la clase CoolGuy tiene un constructor CoolGuy(), que es realmente lo que llamamos cuando hacemos new CoolGuy(...). El constructor nos devuelve un objeto (una instancia de nuestra clase), y podemos llamar al método showOff(), que imprime ese truco especial(specialTrick) de CoolGuy.

Obviamente, saltar la cuerda(jumping rope) hace de Joe un tipo genial.

El constructor de una clase pertenece a la clase, casi universalmente con el mismo nombre que la clase. Además, los constructores siempre necesitan que se les llame con new para que el motor de lenguaje sepa que quiere construir una nueva instancia de clase.

Herencia de clases

En los lenguajes orientados a clases, no sólo se puede definir una clase que se puede instanciar, sino que se puede definir otra clase que hereda de la primera clase.

A menudo se dice que la segunda clase es una «clase hija», mientras que la primera es la «clase padre». Estos términos obviamente provienen de la metáfora de padres e hijos, aunque las metáforas aquí están un poco estiradas, como verás en breve.

Cuando un padre tiene un hijo biológico, las características genéticas del padre se copian en el hijo. Obviamente, en la mayoría de los sistemas de reproducción biológica, hay dos padres que contribuyen de igual manera con los genes a la mezcla. Pero para los propósitos de la metáfora, asumiremos sólo un padre.

Una vez que el niño existe, él o ella está separado del padre. El niño fue fuertemente influenciado por la herencia de sus padres, pero es único y distinto. Si un niño termina con el pelo rojo, eso no significa que el pelo del padre era o se vuelve rojo automáticamente.

De manera similar, una vez que se define una clase hija, es separada y distinta de la clase padre. La clase hija contiene una copia inicial del comportamiento de la clase padre, pero luego puede anular cualquier comportamiento heredado e incluso definir un nuevo comportamiento.

Es importante recordar que estamos hablando de clases padres e hijas, que no son cosas físicas. Aquí es donde la metáfora de padre e hijo se vuelve un poco confusa, porque en realidad deberíamos decir que una clase padre es como el ADN de un padre y una clase hija es como el ADN de un niño. Tenemos que hacer (también conocido como «instanciar») a una persona de cada grupo de ADN para tener realmente una persona física con la que conversar.

Dejemos de lado a los padres biológicos y a los hijos, y miremos la herencia a través de un lente ligeramente diferente: diferentes tipos de vehículos. Esa es una de las metáforas más canónicas (y a menudo molestas) para entender la herencia.

Volvamos a la discusión sobre el Vehículo y el Coche de este capítulo. Considere este pseudo-código suelto (sintaxis inventada) para clases heredadas:

Nota: Para mayor claridad y brevedad, se han omitido los constructores de estas clases.

Definimos la clase Vehícle o para asumir un motor, una forma de encender el motor, y una forma de conducir. Pero nunca se fabricaría sólo un «vehículo» genérico, por lo que en realidad se trata de un concepto abstracto en este momento.

Entonces definimos dos tipos específicos de vehículos: Car y SpeedBoat. Cada uno hereda las características generales de Vehícle, pero luego especializa las características apropiadamente para cada tipo. Un coche necesita 4 ruedas, y un barco de velocidad necesita 2 motores, lo que significa que necesita atención extra para encender ambos motores.

Polimorfismo

Car define su propio método drive(), que anula el método del mismo nombre que heredó de Vehículo. Pero entonces, el método drive() llama a inheritited:drive(), lo que indica que Car puede hacer referencia al pre-sobreescrito drive() que heredó. pilot()de SpeedBoat también hace referencia a su copia heredada de drive().

Esta técnica se denomina «polimorfismo» o «polimorfismo virtual». Más específicamente en nuestro momento actual, lo llamaremos «polimorfismo relativo».

El polimorfismo es un tema mucho más amplio de lo que vamos a agotar aquí, pero nuestra actual semántica «relativa» se refiere a un aspecto particular: la idea de que cualquier método puede hacer referencia a otro método (del mismo nombre o diferente) en un nivel superior de la jerarquía hereditaria. Decimos «relativo» porque no definimos absolutamente a qué nivel de herencia (también conocido como clase) queremos acceder, sino que nos referimos relativamente a él diciendo esencialmente «mira un nivel más arriba».

En muchos idiomas, se utiliza la palabra clave super, en lugar de la heredada de este ejemplo: que se apoya en la idea de que una «superclase» es el padre/ancestro de la clase actual.

Otro aspecto del polimorfismo es que el nombre de un método puede tener múltiples definiciones en diferentes niveles de la cadena de herencia, y estas definiciones se seleccionan automáticamente cuando se resuelve qué métodos se están llamando.

Vemos dos ocurrencias de ese comportamiento en nuestro ejemplo anterior: drive() se define tanto en Vehículo como en Coche, y ignition() se define tanto en Vehículo como en SpeedBoat.

Nota: Otra cosa que los lenguajes tradicionales orientados a clases proporcionan vía super es una forma directa para que el constructor de una clase hija haga referencia al constructor de su clase padre. Esto es muy cierto porque con las clases reales, el constructor pertenece a la clase. Sin embargo, en JS, es al revés – en realidad es más apropiado pensar en la «clase» que pertenece al constructor (las referencias de tipos como Foo.prototype...). Dado que en JS la relación entre hijo y padre existe sólo entre los .prototype de los dos objetos de los respectivos constructores, los propios constructores no están directamente relacionados, y por lo tanto no hay una forma sencilla de referirse relativamente a uno desde el otro (ver Apéndice A para la class ES6 que «resuelve» esto con super).

Una implicación interesante del polimorfismo puede verse específicamente con ignition(). Dentro de pilot(), se hace una referencia relativa-polimórfica a (la heredada) versióndrive() de Vehículo. Pero ese drive() hace referencia a un método de ignition()sólo por nombre (sin referencia relativa).

¿Qué versión de ignition() usará el motor, la del Vehículo o la del SpeedBoat? Utiliza la versión SpeedBoat de ignition(). Si tuviera que instanciar la propia clase Vehículo y luego llamar a su drive() el motor del lenguaje usaría la definición del método ignition() de Vehículo.

Dicho de otra manera, la definición del método ignition() cambia dependiendo de la clase (nivel de herencia) a la que se hace referencia en una instancia.

Esto puede parecer un detalle académico demasiado profundo. Pero entender estos detalles es necesario para contrastar adecuadamente comportamientos similares (pero distintos) en el mecanismo [[Prototype]] de JavaScript.

Cuando las clases son heredadas, hay una manera de que las clases mismas (¡no las instancias de objetos creadas a partir de ellas!) hagan referencia relativa a la clase heredada, y esta referencia relativa es usualmente llamada super.

Recuerde esta imagen de antes:

Observe cómo tanto para la instanciación ( a1, a2 , b1 y b2 ) como para la herencia ( Bar ), las flechas indican una operación de copia.

Conceptualmente, parecería que una clase hija Bar puede acceder al comportamiento en su clase padre Foo usando una referencia polimórfica relativa (también conocida como super). Sin embargo, en realidad, a la clase hija sólo se le da una copia del comportamiento heredado de su clase padre. Si el hijo «anula» un método que hereda, tanto la versión original como la versión anulada del método se mantienen realmente, de modo que ambas son accesibles.

No permita que el polimorfismo lo confunda al pensar que una clase hija está vinculada a su clase padre. Una clase hija recibe una copia de lo que necesita de la clase padre. La herencia de clase implica copias.

Herencia Múltiple

¿Recuerda nuestra discusión anterior sobre los padres, los hijos y el ADN? Dijimos que la metáfora era un poco extraña porque biológicamente la mayoría de los hijos provienen de dos padres. Si una clase pudiera heredar de otras dos clases, se ajustaría más a la metáfora padre/hijo.

Algunos lenguajes orientados a clases le permiten especificar más de una clase «padre» de la que «heredar». Herencia múltiple significa que cada definición de clase padre es copiada en la clase de hijo.

En la superficie, esto parece una adición poderosa a la orientación a clases, dándonos la habilidad de tener más funcionalidades. Sin embargo, sin duda hay algunas cuestiones complicadas que surgen. Si ambas clases padre proporcionan un método llamado drive(), ¿a qué versión se resolvería una referencia drive() en la hija? ¿Tendría que especificar siempre manualmente a qué drive() de los padres se refiere, perdiendo así parte de la gracia de la herencia polimórfica?

 

Estas complicaciones van mucho más allá de esta mirada rápida. Los tratamos aquí sólo para que podamos contrastarlos con el funcionamiento de los mecanismos de JavaScript.

JavaScript es más simple: no proporciona un mecanismo nativo para «herencia múltiple». Muchos ven esto como algo bueno, porque los ahorros en complejidad compensan con creces la funcionalidad «reducida». Pero esto no impide que los desarrolladores intenten fingirlo de varias maneras, como veremos a continuación.

Mixins

El mecanismo de objetos de JavaScript no realiza automáticamente el comportamiento de copia cuando usted «hereda» o «instancia». Claramente, no hay «clases» en JavaScript para instanciar, sólo objetos. Y los objetos no se copian a otros objetos, sino que se enlazan entre sí (más sobre eso en el Capítulo 5).

Dado que los comportamientos de clase observados en otros lenguajes implican copias, examinemos cómo los desarrolladores de JS falsifican el comportamiento de copia faltante de las clases en JavaScript: mixins(mezclas). Veamos dos tipos de «mezcla»: explícita e implícita.

Mixins Explícitos

Volvamos a repasar nuestro ejemplo de Vehículo y Coche de antes. Dado que JavaScript no copiará automáticamente el comportamiento de Vehículo a Coche, podemos crear una utilidad que copie manualmente. Tal utilidad es a menudo llamada extend(...) por muchas bibliotecas/frameworks, pero la llamaremos mixin(...) aquí para propósitos ilustrativos.

Nota: Sutilmente pero de forma importante, ya no estamos tratando con clases, porque no hay clases en JavaScript. Vehículo y coche son sólo objetos de los que hacemos copias desde y hacia, respectivamente.

El coche tiene ahora una copia de las propiedades y funciones del Vehículo. Técnicamente, las funciones no se duplican, sino que se copian las referencias a las funciones. Por lo tanto, Car ahora tiene una propiedad llamada ignition, que es una referencia copiada a la función ignition(), así como una propiedad llamada engines con el valor copiado 1 del Vehículo.

El coche ya tenía una propiedad (función) drive(), de modo que la referencia de propiedad no se anuló (véase la sentencia if en mixin(...) más arriba).

«Revisando» el Polimorfismo

Examinemos esta declaración: Vehicle.drive.call( this ). Esto es lo que yo llamo «pseudo-polimorfismo explícito». Recordemos que en nuestro pseudo-código anterior la línea drive() fue heredada, a la que llamamos «polimorfismo relativo».

JavaScript no tiene (antes de ES6; ver Apéndice A) una utilidad para el polimorfismo relativo. Por lo tanto, debido a que tanto el coche como el vehículo tenían una función del mismo nombre: drive(), para distinguir una llamada a uno u otro, debemos hacer una referencia absoluta (no relativa). Nosotros especificamos explícitamente el objeto Vehículo por su nombre, y llamamos a la función drive() en él.

Pero si hiciésemos Vehicle.drive(), el vínculo para esa llamada de función sería el objeto Vehículo en lugar del objeto Coche (ver Capítulo 2), que no es lo que queremos. Por lo tanto, en su lugar usamos .call( this) (Capítulo 2) para asegurarnos de que drive() se ejecuta en el contexto del objeto Car.

Nota: Si el identificador del nombre de la función Car.drive() no se hubiera solapado con («enmascarado»; ver Capítulo 5) Vehicle.drive(), no estaríamos ejerciendo un «polimorfismo de método». Así, una referencia a Vehicle.drive() habría sido copiada por la llamada mixin(..), y podríamos haber accedido directamente con this.drive(). El identificador elegido que se superpone al enmascarammiento es la razón por la que tenemos que usar el más complejo enfoque del pseudo-polimorfismo explícito.

En los lenguajes orientados a clases, que tienen un polimorfismo relativo, el vínculo entre el coche y el vehículo se establece una vez, en la parte superior de la definición de clase, lo que hace que haya sólo lugar para mantener tales relaciones.

Pero debido a las peculiaridades de JavaScript, el pseudo-polimorfismo explícito (¡debido al enmascaramiento!) crea un frágil vínculo manual/explícito en cada una de las funciones en las que se necesita tal referencia (pseudo)polimórfica. Esto puede aumentar significativamente el costo de mantenimiento. Además, mientras que el pseudo-polimorfismo explícito puede emular el comportamiento de la «herencia múltiple», sólo aumenta la complejidad y la fragilidad.

El resultado de estos enfoques suele ser un código más complejo, más difícil de leer y más difícil de mantener. Siempre que sea posible, debe evitarse el seudopolimorfismo explícito, ya que el coste supera al beneficio en la mayoría de los aspectos.

Mezclando copias

Recuperar la utilidad mixin(...) desde arriba:

Ahora, examinemos cómo funciona el mixin(..). Se itera sobre las propiedades de sourceObj (Vehicle en nuestro ejemplo) y si no hay ninguna propiedad coincidente de ese nombre en targetObj (Car en nuestro ejemplo), hace una copia. Como estamos haciendo la copia después de que el objeto inicial existe, tenemos cuidado de no copiar sobre una propiedad de destino.

Si hicimos las copias primero, antes de especificar el contenido específico del coche, podríamos omitir esta comprobación en targetObj, pero eso es un poco más torpe y menos eficiente, por lo que generalmente es menos preferible:

En cualquier caso, hemos copiado explícitamente el contenido no solapado de Vehicle en Car. El nombre «mixin» proviene de una forma alternativa de explicar la tarea: Car tiene el contenido de Vehicle mezclado, al igual que usted mezcla chispas de chocolate en su masa de galletas favorita.

Como resultado de la operación de copia, Car funcionará de forma algo separada del Vehicle. Si añade una propiedad al coche, no afectará al vehículo, y viceversa.

Nota: Aquí se han ojeado algunos detalles menores. Todavía hay algunas maneras sutiles en las que los dos objetos pueden «afectarse» el uno al otro incluso después de copiar, como si ambos compartieran una referencia a un objeto común (como una matriz).

Dado que los dos objetos también comparten referencias a sus funciones comunes, esto significa que incluso la copia manual de funciones (o mixins) de un objeto a otro no emula la duplicación real de clase en una instancia que ocurre en los lenguajes orientados a clases.

Las funciones JavaScript no pueden ser realmente duplicadas (de una manera estándar y fiable), por lo que lo que se obtiene es una referencia duplicada al mismo objeto de función compartido (las funciones son objetos; véase el Capítulo 3). Si modifica uno de los objetos de función compartidos (como ignition()) añadiendo propiedades encima de él, por ejemplo, tanto el Vehículo como el Coche se verían «afectados» a través de la referencia compartida.

Las mezclas explícitas son un buen mecanismo en JavaScript. Pero parecen más poderosos de lo que realmente son. En realidad no se obtiene mucho beneficio al copiar una propiedad de un objeto a otro, en lugar de simplemente definir las propiedades dos veces, una vez en cada objeto. Y eso es especialmente cierto dado el matiz de referencia función-objeto que acabamos de mencionar.

Si mezcla explícitamente dos o más objetos en su objeto de destino, puede emular parcialmente el comportamiento de «herencia múltiple», pero no hay una manera directa de manejar colisiones si el mismo método o propiedad se está copiando desde más de una fuente. Algunos desarrolladores/bibliotecas han ideado técnicas de «vinculación tardía» y otras soluciones exóticas, pero fundamentalmente estos «trucos» proporcionan generalmente más esfuerzo (¡y menos rendimiento!) que la recompensa.

Tenga cuidado de usar sólo mezclas explícitas donde realmente ayude a hacer más legible el código, y evite el patrón si le resulta más difícil rastrear el código, o si encuentra que crea dependencias innecesarias o difíciles de manejar entre objetos.

Si empieza a ser más difícil usar correctamente las mezclas que antes de usarlas, probablemente debería dejar de usarlas. De hecho, si tiene que usar una biblioteca/utilidad compleja para resolver todos estos detalles, podría ser una señal de que lo está haciendo de la manera más difícil, quizás innecesariamente. En el Capítulo 6, trataremos de destilar una manera más simple que logre los resultados deseados sin todo el alboroto.

Herencia parasitaria

Una variación de este patrón de mezcla explícita, que en algunos aspectos es explícita y en otros implícita, se denomina «herencia parasitaria», popularizada principalmente por Douglas Crockford.

Así es como puede funcionar:

Como puede ver, inicialmente hacemos una copia de la definición de la «clase padre» (objeto) Vehículo, luego mezclamos nuestra definición de «clase hijo» (objeto) (preservando las referencias privilegiadas de la clase padre según sea necesario), y pasamos este coche objeto compuesto como nuestra instancia hijo.

Nota: cuando llamamos new Car(), un nuevo objeto es creado y referenciado por la referencia this de Car(ver Capítulo 2). Pero como no usamos ese objeto, y en su lugar devolvemos nuestro propio objeto de coche, el objeto creado inicialmente se descarta. Por lo tanto, Car() podría ser llamado sin la palabra clave new, y la funcionalidad de arriba sería idéntica, pero sin la creación de objetos desperdiciados / recolección de basura.

Mezclas implícitas

Las mezclas implícitas están estrechamente relacionadas con el pseudo-polimorfismo explícito como se explicó anteriormente. Como tal, vienen con las mismas precauciones y advertencias.

Considere este código:

Con Something.cool.call(this), que puede ocurrir tanto en una llamada «constructor» (la más común) como en una llamada de método (mostrada aquí), esencialmente «tomamos prestada» la función Something.cool() y la llamamos en el contexto de Another (a través de la vinculación de this; ver Capítulo 2) en lugar de Something. El resultado final es que las asignaciones que Something.cool() hace se aplican contra el objeto Anotheren lugar del objeto Something.

Entonces, se dice que nos «mezclamos» en el comportamiento de Something con (o dentro de) Another.

Mientras que esta clase de técnica parece tomar una ventaja útil de la revinculación de this, es la frágil llamada Something.cool.call(this), que no puede convertirse en una referencia relativa (y por lo tanto más flexible), a la que usted debe prestar atención con precaución. Generalmente, evite tales construcciones cuando sea posible para mantener un código más limpio y más mantenible.

Revisión

Las clases son un patrón de diseño. Muchos lenguajes proporcionan sintaxis que permite el diseño natural de software orientado a clases. JS también tiene una sintaxis similar, pero se comporta de forma muy diferente a lo que estás acostumbrado con las clases en esos otros idiomas.

Las clases significan copias.

Cuando se instancian las clases tradicionales, se produce una copia del comportamiento de la clase a la instancia. Cuando las clases son heredadas, una copia del comportamiento de padre a hijo también ocurre.

El polimorfismo (tener diferentes funciones en múltiples niveles de una cadena de herencia con el mismo nombre) puede parecer que implica un vínculo relativo referencial desde el hijo hasta el padre, pero sigue siendo sólo el resultado del comportamiento de copia.

JavaScript no crea automáticamente copias (como las clases implican) entre objetos.

El patrón de mezcla (tanto explícito como implícito) se usa a menudo para emular el comportamiento de copia de clase, pero esto usualmente lleva a una sintaxis fea y quebradiza como el pseudo-polimorfismo explícito (OtherObj.methodName.call(this, ...)), que a menudo resulta más difícil de entender y mantener el código.

Las mezclas explícitas tampoco son exactamente lo mismo que una copia de clase, ya que los objetos (¡y las funciones!) sólo tienen referencias compartidas duplicadas, no objetos/funciones duplicados por sí mismos. No prestar atención a tales matices es la fuente de una variedad de problemas.

En general, las clases falsas en JS a menudo colocan más minas terrestres para codificación futura que para resolver problemas reales presentes.

5. Prototipos

En los capítulos 3 y 4, mencionamos la cadena [[Prototype]] varias veces, pero no hemos dicho qué es exactamente. Ahora examinaremos los prototipos en detalle.

Nota: Todos los intentos de emular el comportamiento de copia de clase, como se describió anteriormente en el Capítulo 4, etiquetados como variaciones de «mixins», evitan completamente el mecanismo de cadena [[Prototype]] que examinamos aquí en este capítulo.

[[Prototype]]

Los objetos en JavaScript tienen una propiedad interna, denotada en la especificación como [[Prototype]], que es simplemente una referencia a otro objeto. A casi todos los objetos se les da un valor no nulo para esta propiedad, en el momento de su creación.

Nota: Veremos en breve que es posible que un objeto tenga una conexión [[Prototype]]vacía, aunque esto es algo menos común.

Considere:

¿Para qué se usa la referencia [[Prototype]]? En el Capítulo 3, examinamos la operación [[Get]] que se invoca cuando se hace referencia a una propiedad en un objeto, como myObject.a. Para esa operación por defecto [[Get]], el primer paso es comprobar si el objeto en sí tiene una propiedad a, y si es así, se utiliza.

Nota: Los proxies ES6 están fuera de nuestro alcance de discusión en este libro (¡serán cubiertos en un libro posterior de la serie!), pero todo lo que discutimos aquí sobre el comportamiento normal [[Get]] y [[Put]] no se aplica si un proxy está involucrado.

Pero es lo que sucede si a no está presente en myObject lo que llama nuestra atención ahora sobre el enlace [[Prototype]] del objeto.

La operación por defecto [[Get]] procede a seguir el enlace [[Prototype]] del objeto si no puede encontrar la propiedad solicitada directamente en el objeto.

Nota: En breve explicaremos lo que hace Object.create(...) y cómo funciona. Por ahora, sólo asume que crea un objeto con el enlace [[Prototype]] que estamos examinando al objeto especificado.

Así, tenemos myObject que ahora está [[Prototype]] vinculado a anotherObject. Claramente myObject.a no existe en realidad, pero sin embargo, el acceso a la propiedad tiene éxito (encontrándose en anotherObject en su lugar) y de hecho encuentra el valor 2.

Pero, si tampoco se encontró un a en anotherObject, su cadena [[Prototype]], si no está vacía, vuelve a ser consultada y seguida.

Este proceso continúa hasta que se encuentra un nombre de propiedad que coincida o hasta que finalice la cadena [[Prototype]]. Si no se encuentra ninguna propiedad que coincida al final de la cadena, el resultado del retorno de la operación [[Get]] es undefined.

Similar a este proceso de búsqueda de cadena [[Prototype]], si usas un bucle for...in para iterar sobre un objeto, cualquier propiedad que pueda ser alcanzada a través de su cadena (y que también sea enumerable — ver Capítulo 3) será enumerada. Si utiliza el operador in para comprobar la existencia de una propiedad en un objeto, se comprobará toda la cadena del objeto (independientemente de la enumerabilidad).

Por lo tanto, la cadena [[Prototype]] es consultada, un eslabón a la vez, cuando se realizan búsquedas de propiedades de varias maneras. La búsqueda se detiene una vez que se encuentra la propiedad o la cadena termina.

Object.prototype

¿Pero dónde exactamente termina la cadena [[Prototype]]?

El extremo superior de cada cadena normal [[Prototype]] es el Object.prototypeincorporado. Este objeto incluye una variedad de utilidades comunes usadas en todo JS, ya que todos los objetos normales (incorporados, no de extensión específica del host) en JavaScript «descienden de» (o lo que es lo mismo, tienen en la parte superior de su cadena [[Prototype]]) el objeto Object.prototype.

Algunas utilidades con las que puede estar familiarizado incluyen .toString() y .valueOf(). En el Capítulo 3, presentamos otro: .hasOwnProperty(...). Y otra función de Object.prototype con la que quizá no esté familiarizado, pero que trataremos más adelante en este capítulo, es .isPrototypeOf(..).

Propiedades de asignación y enmascaramiento

En el Capítulo 3, mencionamos que establecer propiedades en un objeto era algo más que simplemente añadir una nueva propiedad al objeto o cambiar el valor de una propiedad existente. Ahora volveremos a examinar esta situación más a fondo.

Si el objeto myObject ya tiene una propiedad accesoria de datos normal llamada foodirectamente presente en él, la asignación es tan simple como cambiar el valor de la propiedad existente.

Si foo no está ya presente directamente en myObject, la cadena [[Prototype]] se recorre, igual que para la operación [[Get]]. Si foo no se encuentra en ninguna parte de la cadena, la propiedad foo se añade directamente a myObject con el valor especificado, como se esperaba.

Sin embargo, si foo ya está presente en algún lugar más alto de la cadena, un comportamiento sutil (y quizás sorprendente) puede ocurrir con la asignación myObject.foo = "bar". Examinaremos esto más profundamente en un momento.

Si el nombre de la propiedad foo termina tanto en myObject como en un nivel superior de la cadena [[Prototype]] que comienza en myObject, esto se denomina enmascaramiento. La propiedad foo directamente en myObject enmascara, oculta cualquier propiedad foo que aparezca más arriba en la cadena, porque la búsqueda en myObject.foo siempre encontrará la propiedad foo más cercana de la cadena.

Como acabamos de insinuar, seguir a foo en myObject no es tan simple como parece. Ahora examinaremos tres escenarios para la asignación myObject.foo = "bar" cuando foo no está ya directamente en myObject, sino en un nivel superior de la cadena de myObject [[Prototype]]:

  1. Si una propiedad accesoria de datos normal (ver Capítulo 3) llamada foo se encuentra en cualquier lugar más alto en la cadena [[Prototype]], y no está marcada como de sólo lectura (writable:false) entonces una nueva propiedad llamada foo se añade directamente a myObject, resultando en una propiedad enmascarada.
  2. Si se encuentra un foo más arriba en la cadena [[Prototype]], pero está marcado como de sólo lectura (writable:false), entonces tanto la configuración de esa propiedad existente como la creación de la propiedad enmascarada en myObject no están permitidas. Si el código se está ejecutando en modo estricto, se lanzará un error. De lo contrario, se ignorará silenciosamente la configuración del valor de la propiedad. De cualquier manera, no se producen enmascaramientos.
  3. Si se encuentra un foo más arriba en la cadena [[Prototype]] y es un setter (ver Capítulo 3), entonces siempre se llamará al setter. No se añadirá foo a myObject (o lo que es lo mismo, no enmascarará), ni se redefinirá el setter de foo.

La mayoría de los desarrolladores asumen que la asignación de una propiedad ([[Put]]) siempre resultará en enmascaramiento si la propiedad ya existe más arriba en la cadena [[Prototype]], pero como puede ver, eso sólo es cierto en una (#1) de las tres situaciones descritas.

Si quiere enmascarar foo en los casos #2 y #3, no puede usar la asignación =, sino que debe usar Object.defineProperty(..) (vea el Capítulo 3) para agregar foo a myObject.

Nota: El Caso #2 puede ser el más sorprendente de los tres. La presencia de una propiedad de sólo lectura evita que una propiedad del mismo nombre sea creada implícitamente (enmascarada) en un nivel inferior de una cadena [[Prototype]]. La razón de esta restricción es principalmente para reforzar la ilusión de las propiedades heredadas de una clase. Si piensa que el foo en un nivel superior de la cadena ha sido heredado (copiado) a myObject, entonces tiene sentido hacer cumplir la naturaleza no escribible de esa propiedad foo en myObject. Sin embargo, si separas la ilusión del hecho y reconoces que en realidad no ocurrió tal copia de herencia (ver Capítulos 4 y 5), es un poco antinatural que myObjectse vea impedido de tener una propiedad foo sólo porque algún otro objeto tenía un foo no escribible. Es aún más extraño que esta restricción sólo se aplique a la asignación =, pero no se aplique cuando se utiliza Object.defineProperty(..).

Enmascarar con métodos lleva a un feo pseudo-polimorfismo explícito (ver Capítulo 4) si es necesario delegar entre ellos. Por lo general, el enmascaramiento es más complicado y sutil de lo que vale la pena, por lo que debes tratar de evitarlo si es posible. Vea el Capítulo 6 para un patrón de diseño alternativo, el cual, entre otras cosas, desalienta la creación de enmascaramientos a favor de alternativas más limpias.

Los enmascaramientos pueden incluso ocurrir implícitamente de manera sutil, por lo que se debe tener cuidado si se trata de evitarlas. Considere:

Aunque puede parecer que myObject.a++ debería (a través de la delegación) buscar y simplemente incrementar la propiedad anotherObject.a, en su lugar, la operación ++corresponde a myObject.a = myObject.a + 1. El resultado es [[Get]]] buscando una propiedad vía [[Prototype]] para obtener el valor actual 2 de anotherObject.a, incrementando el valor en uno, luego [[Put]] asignando el valor 3 a una nueva propiedad enmascarada a en myObject. Oops!

Tenga mucho cuidado cuando trate con propiedades delegadas que modifique. Si querías incrementar anotherObject.a, la única manera apropiada es anotherObject.a++.

«Class»

En este punto, puede que te estés preguntando: «¿Por qué un objeto necesita enlazarse con otro objeto?» ¿Cuál es el verdadero beneficio? Esa es una pregunta muy apropiada, pero primero debemos entender lo que [[Prototype]] no es antes de que podamos entender y apreciar completamente lo que es y cómo es útil.

Como explicamos en el Capítulo 4, en JavaScript, no hay patrones abstractos/planos para objetos llamados «clases» como los hay en los lenguajes orientados a clases. JavaScript sólo tiene objetos.

De hecho, JavaScript es casi único entre los lenguajes como quizás el único lenguaje con el derecho de usar la etiqueta «orientado a objetos», porque es uno de una lista muy corta de lenguajes donde un objeto puede ser creado directamente, sin ninguna clase.

En JavaScript, las clases no pueden (¡ya que no existen!) describir lo que puede hacer un objeto. El objeto define su propio comportamiento directamente. Sólo está el objeto.

«Funciones de «Clase

Hay un tipo peculiar de comportamiento en JavaScript que ha sido abusado sin vergüenza durante años para prodicir cosas que parecezcan «clases». Examinaremos este enfoque en detalle.

El peculiar comportamiento de «clase» depende de una extraña característica de las funciones: todas las funciones por defecto tienen una propiedad pública, no enumerable (ver Capítulo 3) llamada prototype, que apunta a un objeto arbitrario.

A este objeto se le suele llamar «prototipo de Foo», porque accedemos a él a través de una referencia de propiedad desafortunadamente llamada Foo.prototype. Sin embargo, esa terminología está destinada a llevarnos a la confusión, como veremos en breve. En su lugar, lo llamaré «el objeto antes conocido como el prototipo de Foo». Sólo bromeaba. Que tal: «objeto etiquetado arbitrariamente como ‘Foo dot prototype'»?

Lo llamemos como lo llamemos, ¿qué es exactamente este objeto?

La forma más directa de explicarlo es que cada objeto creado a partir de la llamada de un new Foo() (ver Capítulo 2) terminará (algo arbitrariamente) enlazando [[Prototype]]con este objeto «Foo dot prototype».

Vamos a ilustrarlo:

Cuando a se crea llamando a un new Foo(), una de las cosas (ver Capítulo 2 para los cuatro pasos) que sucede es que a obtiene un enlace [[Prototype]] interno al objeto al que apunta Foo.prototype.

Deténgase por un momento y reflexione sobre las implicaciones de esa declaración.

En los lenguajes orientados a la clase, se pueden hacer múltiples copias (también conocidas como «instancias») de una clase, como sacar algo de un molde. Como vimos en el Capítulo 4, esto sucede porque el proceso de instanciar (o heredar de) una clase significa, «copiar el plan de comportamiento de esa clase en un objeto físico», y esto se hace de nuevo para cada nueva instancia.

Pero en JavaScript, no hay tales acciones de copia realizadas. No se crean múltiples instancias de una clase. Puede crear múltiples objetos [[Prototype]] que enlazan a un objeto común. Pero por defecto, no se produce ninguna copia, y por lo tanto estos objetos no terminan totalmente separados y desconectados entre sí, sino más bien, bastante enlazados.

new Foo() resulta en un nuevo objeto (lo llamamos a), y ese nuevo objeto a es internamente vinculado vía [[Prototype]] al objeto Foo.prototype.

Terminamos con dos objetos, unidos entre sí. Eso es todo. No instanciamos una clase. Ciertamente no hicimos ninguna copia del comportamiento de una «clase» a un objeto concreto. Sólo causamos que dos objetos se enlazaran entre sí.

De hecho, el secreto, que elude a la mayoría de los desarrolladores de JS, es que la llamada new Foo() no tiene casi nada que ver directamente con el proceso de creación del enlace. Fue una especie de efecto secundario accidental. new Foo() es una forma indirecta y redondeada de terminar con lo que queremos: un nuevo objeto vinculado a otro objeto.

¿Podemos conseguir lo que queremos de una manera más directa? Si! El héroe es Object.create(..). Pero llegaremos a eso en un momento.

¿Qué hay en un nombre?

En JavaScript, no hacemos copias de un objeto («clase») a otro («instancia»). Hacemos enlaces entre objetos. Para el mecanismo [[Prototype]], visualmente, las flechas se mueven de derecha a izquierda, y de abajo hacia arriba.

 

Este mecanismo se llama a menudo «herencia prototípica» (exploraremos el código en detalle en breve), que comúnmente se dice que es la versión en lenguaje dinámico de la «herencia clásica». Es un intento de aprovechar el entendimiento común de lo que significa «herencia» en el mundo orientado a la clase, pero ajustando la semántica entendida, para adaptarla a los scripts dinámicos.

La palabra «herencia» tiene un significado muy fuerte (ver capítulo 4), con muchos precedentes mentales. Simplemente añadiendo «prototípica» delante para distinguir el comportamiento realmente casi opuesto en JavaScript ha dejado en su estela casi dos décadas de misteriosa confusión.

Me gusta decir que pegar «prototípica» delante de «herencia» para revertir drásticamente su significado real es como sostener una naranja en una mano, una manzana en la otra, e insistir en llamar a la manzana «naranja roja». No importa qué etiqueta confusa le ponga delante, eso no cambia el hecho de que una fruta es una manzana y la otra es una naranja.

El mejor enfoque es llamar claramente manzana a una manzana, para usar la terminología más precisa y directa. Esto hace que sea más fácil entender tanto sus similitudes como sus muchas diferencias, porque todos tenemos una comprensión sencilla y compartida de lo que significa «manzana».

Debido a la confusión y mezcla de términos, creo que la etiqueta «herencia prototípica» en sí misma (y tratar de aplicar mal toda la terminología asociada a la orientación de clases, como «clase», «constructor», «instancia», «polimorfismo», etc.) ha hecho más daño que bien al explicar cómo funciona realmente el mecanismo de JavaScript.

«Herencia» implica una operación de copia, y JavaScript no copia las propiedades de los objetos (de forma nativa, por defecto). En su lugar, JS crea un enlace entre dos objetos, donde un objeto puede delegar esencialmente el acceso de propiedad/función a otro objeto. «Delegación» (ver Capítulo 6) es un término mucho más preciso para el mecanismo de enlace de objetos de JavaScript.

Otro término que a veces se utiliza en JavaScript es «herencia diferencial». La idea aquí es que describimos el comportamiento de un objeto en términos de lo que es diferente de un descriptor más general. Por ejemplo, usted explica que un automóvil es un tipo de vehículo, pero que tiene exactamente 4 ruedas, en lugar de volver a describir todos los detalles específicos de lo que constituye un vehículo general (motor, etc.).

Si tratas de pensar en cualquier objeto dado en JS como la suma total de todos los comportamientos disponibles a través de la delegación, y en tu mente aplastas todo ese comportamiento en una cosa tangible, entonces puedes (más o menos) ver cómo podría encajar la «herencia diferencial».

Pero al igual que con la «herencia prototípica», la «herencia diferencial» pretende que su modelo mental es más importante que lo que está sucediendo físicamente en el lenguaje. Pasa por alto el hecho de que el objeto B en realidad no se construye diferencialmente, sino que se construye con características específicas definidas, junto a «agujeros» en los que no se define nada. Es en estos «agujeros» (vacíos o falta de definición) donde la delegación puede tomar el relevo y, sobre la marcha, «llenarlos» con un comportamiento delegado.

El objeto no es, por defecto nativo, integrado en el objeto diferencial único, a través de la copia, que implica el modelo mental de «herencia diferencial». Como tal, la «herencia diferencial» no es tan natural como para describir cómo funciona realmente el mecanismo [[Prototype]] de JavaScript.

Puedes elegir preferir la terminología de «herencia diferencial» y el modelo mental, como una cuestión de gusto, pero no se puede negar el hecho de que sólo se ajusta a las acrobacias mentales de tu mente, no al comportamiento físico del motor.

«Constructores»

Volvamos a algún código anterior:

¿Qué nos lleva exactamente a pensar que Foo es una «clase»?

Por un lado, vemos el uso de la palabra clave new, tal como lo hacen los lenguajes orientados a clases cuando construyen instancias de clase. Por otro lado, parece que estamos ejecutando un método constructor de una clase, porque Foo() es en realidad un método que se llama, igual que se llama al constructor de una clase real cuando instancias esa clase.

Para fomentar la confusión de la semántica del «constructor», el objeto arbitrariamente etiquetado Foo.prototype tiene otro truco bajo la manga. Considere este código:

El objeto Foo.prototype por defecto (en el momento de la declaración en la línea 1 del fragmento de código!) obtiene una propiedad pública, no enumerable (ver Capítulo 3) llamada .constructor, y esta propiedad es una referencia a la función (Foo en este caso) a la que está asociado el objeto. Además, vemos que el objeto a creado por la llamada al «constructor» new Foo() parece tener también una propiedad en él llamada .constructor que apunta de forma similar a «la función que lo creó».

Nota: Esto no es realmente cierto. a no tiene ninguna propiedad .constructor, y aunque a.constructor se resuelve de hecho a la función Foo, «constructor» no significa «fue construido por», como parece. Explicaremos esta extrañeza en breve.

Oh, sí, también… por convención en el mundo JavaScript, las «clases» son nombradas con una letra mayúscula, por lo que el hecho de que sea Foo en lugar de foo es una fuerte pista de que pretendemos que sea una «clase». Eso es totalmente obvio para ti, ¿verdad?

Nota: Esta convención es tan fuerte que muchos de los linters de JS se quejan si usas newen un método con un nombre en minúsculas, o si no usamos new en una función que empieza con mayúsculas. Eso deja perplejo el hecho de que tengamos que luchar tanto para conseguir una «orientación de clase» (falsa) en JavaScript que creamos reglas de linter para asegurarnos de que usamos mayúsculas, aunque la mayúscula no signifique nada en absoluto para el motor JS.

Constructor o Llamada?

En el fragmento anterior, es tentador pensar que Foo es un «constructor», porque lo llamamos con new y observamos que «construye» un objeto.

En realidad, Foo no es más un «constructor» que cualquier otra función de tu programa. Las funciones mismas no son constructoras. Sin embargo, cuando pones la palabra clave newdelante de una llamada de función normal, eso hace que esa llamada de función sea una «llamada al constructor». De hecho, new secuestra cualquier función normal y llama de una manera que construye un objeto, además de cualquier otra cosa que iba a hacer.

Por ejemplo:

NothingSpecial es sólo una simple y antigua función normal, pero cuando se llama con new, construye un objeto, casi como un efecto secundario, que por casualidad le asignamos a a. La llamada era una llamada de constructor, pero NothingSpecial no es, en sí misma, un constructor.

En otras palabras, en JavaScript, lo más apropiado es decir que un «constructor» es cualquier función llamada con la palabra clave new delante.

Las funciones no son constructores, pero las llamadas de función son «llamadas de constructor» si y sólo si se usa new.

Mecánica

¿Son estos los únicos desencadenantes comunes para discusiones de «clases» en JavaScript?

No exactamente. Los desarrolladores de JS se han esforzado por simular lo más posible la orientación de clase:

Este fragmento muestra dos trucos adicionales de «orientación a clases» en juego:

  1. this.name = name: añade la propiedad .name a cada objeto (a y b, respectivamente; vea el Capítulo 2 sobre la vinculación this), de forma similar a como las instancias de clase encapsulan los valores de los datos.Foo.prototype.myName = ....: quizás la técnica más interesante, esto añade una propiedad (función) al objeto Foo.prototype. Ahora, a.myName() funciona, pero quizás sorprendentemente. ¿Cómo?

En el fragmento anterior, es muy tentador pensar que cuando se crean a y b, las propiedades/funciones del objeto Foo.prototype se copian en cada uno de los objetos a y b. Sin embargo, eso no es lo que sucede.

Al principio de este capítulo, explicamos el enlace [[Prototype]], y cómo proporciona los pasos de búsqueda inversa si una referencia de propiedad no se encuentra directamente en un objeto, como parte del algoritmo por defecto [[Get]].

Por lo tanto, en virtud de cómo se crean a y b, cada uno termina con una conexión interna [[Prototype]] a Foo.prototype. Cuando myName no se encuentra en a o b, respectivamente, se encuentra (a través de delegación, ver Capítulo 6) en Foo.prototype.

Deduciendo el «Constructor»

Recordemos la discusión anterior sobre la propiedad .constructor, y cómo parece que un .constructor ==== Foo siendo true significa que tiene una propiedad .constructor real, apuntando a Foo? No es correcto.

Esto es sólo una desafortunada confusión. En realidad, la referencia de .constructortambién se delega hasta Foo.prototype, que pasa a tener, por defecto, un .constructor que apunta a Foo.

Parece terriblemente conveniente que un objeto a «construido por» Foo tenga acceso a una propiedad .constructor que apunta a Foo. Pero eso no es más que una falsa sensación de seguridad. Es un feliz accidente, casi tangencial, que un a.constructor apunte a Fooa través de esta delegación [[Prototype]] por defecto. De hecho, hay varias maneras en las que la fatídica suposición de que .constructor significa «fue construido por» puede volver a morderte.

Por un lado, la propiedad .constructor en Foo.prototype sólo está por defecto en el objeto creado cuando se declara la función Foo. Si crea un nuevo objeto y reemplaza la referencia predeterminada del objeto .prototype de una función, el nuevo objeto no obtendrá mágicamente un .constructor por defecto.

Considere:

Object(...) no «construyó» a1, ¿verdad? Seguro que parece que Foo() lo «construyó». Muchos desarrolladores piensan que Foo() hace la construcción, pero donde todo se desmorona es cuando piensas que «constructor» significa «fue construido por», porque por ese razonamiento, a1.constructor debería ser Foo, ¡pero no lo es!

¿Qué está pasando? a1 no tiene propiedad .constructor, así que delega la cadena [[Prototye]] a Foo.prototype. Pero ese objeto tampoco tiene un .constructor(¡como el objeto por defecto Foo.prototype hubiera tenido!), así que sigue delegando, esta vez hasta Object.prototype, la parte superior de la cadena de delegación. De hecho, ese objeto tiene un constructor .constructor, que apunta a la función Object(..)incorporada.

Un concepto erróneo, atrapado.

Por supuesto, puedes volver a añadir .constructor al objeto Foo.prototype, pero esto requiere trabajo manual, especialmente si quieres que coincida con el comportamiento nativo y que no sea enumerable (ver Capítulo 3).

Por ejemplo:

Eso es mucho trabajo manual para arreglar .constructor. Además, todo lo que realmente estamos haciendo es perpetuar la idea errónea de que «constructor» significa «fue construido por». Es una ilusión cara.

El hecho es que el .constructor sobre un objeto apunta arbitrariamente, por defecto, a una función que, recíprocamente, tiene una referencia al objeto — una referencia que llama .prototype. Las palabras «constructor» y «prototipo» sólo tienen un significado por defecto suelto que podría ser, o no, válido más tarde. Lo mejor es recordar que «constructor no significa construido por».

.constructor no es una propiedad mágica inmutable. No es ennumerable (ver fragmento arriba), pero su valor es writable (puede ser cambiado), y además, puede agregar o sobreescribir (intencionalmente o accidentalmente) una propiedad de nombre constructoren cualquier objeto en cualquier cadena [[Prototype]], con cualquier valor que considere apropiado.

En virtud de la forma en que el algoritmo [[Get]] atraviesa la cadena [[Prototype]], una referencia de propiedad .constructor encontrada en cualquier lugar puede resolverse de forma muy diferente a la esperada.

¿Ves lo arbitrario que es su significado?

¿El resultado? Alguna referencia arbitraria a la propiedad del objeto como a1.constructorno puede ser realmente confiada para ser la referencia supuesta de la función por defecto. Además, como veremos en breve, por simple omisión, a1.constructor puede incluso acabar apuntando a un lugar bastante sorprendente e insensible.

.constructor es extremadamente poco confiable, y una referencia insegura en la que confiar en su código. En general, estas referencias deben evitarse siempre que sea posible.

«Herencia (de Prototipos)»

Hemos visto algunas aproximaciones de la mecánica de «clases» como típicamente hackeada en programas JavaScript. Pero las «clases» de JavaScript serían bastante huecas si no tuviéramos una aproximación de «herencia».

En realidad, ya hemos visto el mecanismo que comúnmente se llama «herencia prototípica» en funcionamiento cuando se podía «heredar» de Foo.prototype, y así obtener acceso a la función myName(). Pero tradicionalmente pensamos en la «herencia» como una relación entre dos «clases», más que entre «clase» e «instancia».

 

Recordemos esta figura anterior, que muestra no sólo la delegación de un objeto (también conocido como «instancia») a1 a un objeto Foo.prototype, sino de un Bar.prototypea un Foo.prototype, que se asemeja un poco al concepto de herencia de clase padre-hijo. Se parece, excepto por supuesto por la dirección de las flechas, que muestran que son enlaces de delegación en lugar de operaciones de copia.

Y, aquí está el típico código «estilo prototipo» que crea tales enlaces:

Nota: Para entender por qué this apunta a a en el fragmento de código de arriba, vea el Capítulo 2.

La parte importante es Bar.prototype = Object.create( Foo.prototype). Object.create(...) crea un objeto «nuevo» de la nada, y enlaza el [[Prototype]]interno de ese nuevo objeto con el objeto que usted especifique (Foo.prototype en este caso).

En otras palabras, esa línea dice: «Hacer un nuevo objeto ‘Bar.prototype’ que esté vinculado a ‘Foo.prototype'».

Cuando la función Bar() { .. } es declarada, Bar, como cualquier otra función, tiene un enlace .prototype a su objeto por defecto. Pero ese objeto no está vinculado a Foo.prototype como queremos. Así, creamos un nuevo objeto que está enlazado como queremos, deshechando efectivamente el objeto original enlazado incorrectamente.

Nota: Una idea errónea o confusión común aquí es que cualquiera de los siguientes enfoques también funcionaría, pero no funcionan como uno esperaría:

Bar.prototype = Foo.prototype no crea un nuevo objeto al que vincular Bar.prototype. Simplemente hace que Bar.prototype sea otra referencia a Foo.prototype, que enlaza a Bar directamente con el mismo objeto con el que Foo se enlaza: Foo.prototype. Esto significa que cuando empiezas a asignar, como Bar.prototype.myLabel =..., estás modificando no un objeto separado sino el objeto Foo.prototype compartido, lo que afectaría a cualquier objeto vinculado a Foo.prototype. Esto es casi seguro que no es lo que quieres. Si es lo que quieres, entonces es probable que no necesites Bar en absoluto, y deberías usar sólo Foo y hacer tu código más simple.

Bar.prototype = nuew Foo() de hecho crea un nuevo objeto que está debidamente enlazado con Foo.prototype como quisiéramos. Pero, usa el «llamado constructor» de Foo(...) para hacerlo. Si esa función tiene algún efecto secundario (como registrar, cambiar de estado, registrar contra otros objetos, añadir propiedades de datos a this, etc.), esos efectos secundarios ocurren en el momento de enlazar (¡y probablemente contra el objeto equivocado!), y no sólo cuando se crean los eventuales «descendientes» de Bar(), como sería de esperar.

Por lo tanto, nos queda usar Object.create(...) para crear un nuevo objeto que esté correctamente enlazado, pero sin tener los efectos secundarios de llamar a Foo(...). La ligera desventaja es que tenemos que crear un nuevo objeto, tirando el viejo a la basura, en lugar de modificar el objeto por defecto existente que se nos proporciona.

Sería bueno que hubiera una manera estándar y confiable de modificar el enlace de un objeto existente. Antes de ES6, existe una forma de navegación no estándar y no totalmente cruzada, a través de la propiedad .__proto__, que es configurable. ES6 añade una utilidad de ayuda Object.setPrototypeOf(...), que hace el truco de una manera estándar y predecible.

Compare las técnicas pre-ES6 y ES6 estandarizadas para enlazar Bar.prototype con Foo.prototype, lado a lado:

Ignorando la ligera desventaja de rendimiento (tirar un objeto que luego se recoge como basura) de la aproximación Object.create(...), es un poco más corta y puede ser quizás un poco más fácil de leer que la aproximación ES6+. Pero probablemente sea igualmente un lavado de cara sintáctico.

Inspeccionar relaciones de clases

¿Qué pasa si tienes un objeto como a y quieres saber en qué objeto (si lo hay) delega? Inspeccionar una instancia (sólo un objeto en JS) por su ascendencia hereditaria (vinculación de delegación en JS) a menudo se llama introspección (o reflexión) en entornos tradicionales orientados a la clase.

Considere:

¿Cómo introspeccionamos entonces para descubrir su «ascendencia» (vinculación de la delegación)? El primer enfoque abarca la confusión de «clase»:

El operador instanceof toma un objeto plano como su operando izquierdo y una función como su operando derecho. La respuestas de instanceof a la pregunta es: en toda la cadena [[Prototype]] de a, ¿aparece alguna vez el objeto señalado arbitrariamente por Foo.prototype?

Desafortunadamente, esto significa que sólo puedes preguntar sobre la «ascendencia» de algún objeto (a) si tienes alguna función (Foo, con su referencia .prototype adjunta) con la que probar. Si tienes dos objetos arbitrarios, digamos a y b, y quieres saber si los objetos están relacionados entre sí a través de una cadena [[Prototype]], instanceof por si sola no puede ayudar.

Nota: Si utiliza la utilidad .bind(..) incorporada para hacer una función de vínculo fuerte (ver Capítulo 2), la función creada no tendrá una propiedad .prototype. Usando instanceof con una función de este tipo sustituye de forma transparente al .prototypede la función de destino a partir de la cual se creó la función de vinculación fuerte.

Es bastante poco común usar funciones de vinculación fuerte como «llamadas de constructor», pero si lo hace, se comportará como si se invocara la función de destino original, lo que significa que usar instanceof con una función de vinculación fuerte también se comportará de acuerdo con la función original.

Este fragmento ilustra la ridiculez de tratar de razonar sobre las relaciones entre dos objetos usando la semántica de «clase» y instanciaof:

Dentro de isRelatedTo(...), tomamos prestada una función desechable F, reasignamos su .prototype para que apunte arbitrariamente a algún objeto o2, y luego preguntamos si o1 es un «ejemplo de» F. Obviamente o1 no se hereda o desciende o incluso se construye a partir de F, así que debería quedar claro por qué este tipo de ejercicio es tonto y confuso. El problema se reduce a la torpeza de la semántica de clases forzada sobre JavaScript, en este caso revelada por la semántica indirecta de instanceof.

El segundo, y mucho más limpio, enfoque a la reflexión [[prototype]] es:

Note que en este caso, realmente no nos importa (o ni siquiera necesitamos) Foo, sólo necesitamos un objeto (en nuestro caso, etiquetado arbitrariamente como Foo.prototype) para probar contra otro objeto. La pregunta que se plantea es: en toda la cadena [[Prototype]] de a, ¿aparece alguna vez Foo.prototype?

La misma pregunta, y exactamente la misma respuesta. Pero en este segundo enfoque, en realidad no necesitamos la indirección de referenciar una función (Foo) cuya propiedad .prototype será consultada automáticamente.

Sólo necesitamos dos objetos para inspeccionar una relación entre ellos. Por ejemplo:

Note, este enfoque no requiere una función («clase») en absoluto. Sólo usa referencias de objetos directas a b y c, y pregunta sobre su relación. En otras palabras, nuestra utilidad isRelatedTo(...) está incorporada al lenguaje, y se llama isPrototypeOf(...).

También podemos recuperar directamente el [[Prototype]] de un objeto. A partir de ES5, la forma estándar de hacerlo es:

Y notará que la referencia al objeto es lo que esperaríamos:

La mayoría de los navegadores (¡no todos!) han soportado durante mucho tiempo una forma alternativa no estándar de acceder al [[Prototype]] interno:

La extraña propiedad .__proto__ (no estandarizada hasta ES6!) «mágicamente» recupera el [[Prototype]] interno de un objeto como referencia, lo cual es bastante útil si quieres inspeccionar directamente (o incluso atravesar: .__proto__.__proto__…) la cadena.

Tal y como vimos antes con .constructor, .__proto__ no existe realmente en el objeto que está inspeccionando (a en nuestro ejemplo). De hecho, existe (no enumerable; ver Capítulo 2) en el Object.prototype incorporado del lenguaje, junto con las otras utilidades comunes (.toString(), .isPrototypeOf(...), etc).

Además, .__proto__ parece una propiedad, pero en realidad es más apropiado pensar en ella como un getter/setter (ver Capítulo 3).

Aproximadamente, podríamos visualizar .__proto__ implementado (ver Capítulo 3 para definiciones de propiedades de objeto) así:

Así, cuando accedemos (recuperamos el valor de) a.__proto__, es como llamar a.__proto__() (llamando a la función getter). Esa llamada de función tiene a como thisa pesar de que la función getter existe en el objeto Object.prototype (ver Capítulo 2 para estas reglas vinculantes), así que es como decir Object.getPrototypeOf( a ).

.__proto__ es también una propiedad definible, igual que usando Object.setPrototypeOf(..) de ES6 mostrado anteriormente. Sin embargo, generalmente no debería cambiar el [[Prototype]] de un objeto existente.

Hay algunas técnicas muy complejas y avanzadas que se utilizan en algunos frameworks que permiten trucos como «subclasificar» un Array, pero esto es comúnmente desaprobado en la práctica general de la programación, ya que normalmente lleva a que sea mucho más difícil entender/mantener el código.

Nota: A partir de ES6, la palabra clave «class» permitirá algo que se aproxime al «subclassing» de las incorporadas al lenguaje como Array. Ver Apéndice A para la discusión de la sintaxis de clase añadida en ES6.

La única otra excepción estrecha (como se mencionó anteriormente) sería configurar el [[Prototype]] del Objet.prototype de una función predeterminada para que haga referencia a algún otro objeto (además de Object.prototype). Esto evitaría reemplazar completamente ese objeto por defecto con un nuevo objeto enlazado. De lo contrario, es mejor tratar el enlace de objeto [[Prototype]] como una característica de sólo lectura para facilitar la lectura del código más adelante.

Nota: La comunidad JavaScript acuñó extraoficialmente un término para el doble subrayado, específicamente el principal en propiedades como __proto__: «Dunder». Así, los «cool kids» en JavaScript pronunciaban generalmente __proto__ como «dunder proto».

Enlaces de objetos

Como hemos visto, el mecanismo [[Prototype]] es un enlace interno que existe en un objeto que hace referencia a otro objeto.

Esta vinculación se ejerce (principalmente) cuando se hace una referencia de propiedad/método contra el primer objeto, y no existe tal propiedad/método. En ese caso, el enlace [[Prototype]] le dice al motor que busque la propiedad/método en el objeto enlazado. A su vez, si ese objeto no puede cumplir la búsqueda, se sigue su [[Prototype]], y así sucesivamente. Esta serie de enlaces entre objetos forma lo que se llama la «cadena del prototipo».

Creación de Enlaces

Hemos descrito completamente por qué el mecanismo [[Prototype]] de JavaScript no es como las clases, y hemos visto cómo en cambio crea enlaces entre los objetos apropiados.

¿Para qué sirve el mecanismo [[Prototype]]? ¿Por qué es tan común que los desarrolladores de JS hagan tanto esfuerzo (emular clases) en su código para conectar estos enlaces?

¿Recuerdas que dijimos mucho antes en este capítulo que Object.create(..) sería un héroe? Ahora, estamos listos para ver cómo.

Object.create(...) crea un nuevo objeto (bar) vinculado al objeto que especificamos (foo), lo que nos da todo el poder (delegación) del mecanismo [[Prototype]], pero sin ninguna de las complicaciones innecesarias de las funciones new que actúan como clases y llamadas constructoras, confundiendo las referencias .prototype y .constructor, o cualquiera de esas cosas extra.

Nota: Object.create(null) crea un objeto que tiene un enlace [[Prototype]] vacío (osea null), y por lo tanto el objeto no puede delegar en ninguna parte. Dado que un objeto de este tipo no tiene cadena de prototipos, el operador instanceof (explicado anteriormente) no tiene nada que comprobar, por lo que siempre devolverá false. Estos objetos especiales vacíos-[[Prototype]] se denominan a menudo «diccionarios», ya que se utilizan típicamente para almacenar datos en propiedades, sobre todo porque no tienen efectos sorpresa posibles de ninguna propiedad/función delegada en la cadena [[Prototype]], y por lo tanto son almacenamiento de datos puramente planos.

No necesitamos clases para crear relaciones significativas entre dos objetos. Lo único que realmente debería preocuparnos son los objetos enlazados entre sí para la delegación, y Object.create(...) nos da esa vinculación sin todo el arsenal de la clase.

Polyfill de Object.create()

Object.create(...) fue agregado en ES5. Puede que necesites soportar entornos pre-ES5 (como los antiguos IE’s), así que echemos un vistazo a un simple poliyll parcial para Object.create(...) que nos da la capacidad que necesitamos incluso en esos antiguos entornos JS:

Este polyfill funciona usando una función F desechable y anulando su propiedad .prototype para apuntar al objeto al que queremos enlazar. Luego usamos la nueva construcción F() para hacer un nuevo objeto que será enlazado como especificamos.

Este uso de Object.create(..) es, con mucho, el más común, porque es la parte sobre la que se puede realizar un polyfill. Hay un conjunto adicional de funcionalidades que proporciona el estándar ES5 incorporado Object.create(..), que no es poli-llenable para pre-ES5. Como tal, esta capacidad se utiliza mucho menos comúnmente. Por el bien de la integridad, veamos esa funcionalidad adicional:

El segundo argumento de Object.create(..) especifica los nombres de las propiedades que se añadirán al objeto recién creado, declarando el descriptor de cada nueva propiedad (ver Capítulo 3). Debido a que no es posible realizar un polifyll sobre los descriptores de propiedades en pre-ES5, en esta funcionalidad adicional en Object.create(..) tampoco puede realizarse polyfill.

La gran mayoría del uso de Object.create(...) utiliza el subconjunto de funcionalidad polyfill-seguro, por lo que la mayoría de los desarrolladores están de acuerdo con el uso de polyfill parcial en entornos pre-ES5.

Algunos desarrolladores tienen un punto de vista mucho más estricto, que es no realizar polifyll sobre ninguna función a menos que pueda hacerse un polifyll completo. Dado que Object.create(...) es una de esas utilidades sobre las que puede trealizarse un polifyll parcial, esta perspectiva más estrecha dice que si necesita usar cualquiera de las funcionalidades de Object.create(...) en un entorno pre-ES5, en lugar de rrealizar un polifyll, debería usar una utilidad personalizada y no usar el nombre Object.create por completo. En su lugar, puede definir su propia utilidad, como:

No comparto esta opinión estricta. Respaldo totalmente el relleno parcial común de Object.create(...) como se muestra arriba, y el uso en tu código, incluso en pre-ES5. Dejaré que tú tomes tu propia decisión.

Enlaces como Respaldos?

Puede ser tentador pensar que estos vínculos entre objetos proporcionan principalmente una especie de respaldo para las propiedades o métodos «faltantes». Aunque ese puede ser un resultado observado, no creo que represente la manera correcta de pensar acerca de [[Prototype]].

Considere:

Ese código funcionará en virtud de [[Prototype]], pero si lo escribiste de esa manera de modo que anotherObject estuviera actuando como una alternativa en caso de que myObject no pudiera manejar alguna propiedad/método al que algún desarrollador pudiera intentar llamar, lo más probable es que tu software vaya a ser un poco más «mágico» y más difícil de entender y mantener.

Eso no quiere decir que no haya casos en los que las copias de seguridad sean un patrón de diseño apropiado, pero no es muy común o idiomático en JS, así que si te encuentras haciendo esto, podrías dar un paso atrás y reconsiderar si ese es un diseño realmente apropiado y sensato.

Nota: En ES6, se introduce una funcionalidad avanzada llamada Proxy que puede proporcionar algo así como un tipo de comportamiento de «método no encontrado». Proxy está más allá del alcance de este libro, pero será cubierto en detalle en un libro posterior en la serie «You Don’t Know JS».

No te pierdas un punto importante pero con matizes.

Diseñar software en el que pretende que un desarrollador, por ejemplo, llame a myObject.cool() y que funcione aunque no haya un método cool() en myObjectintroduce algo de «magia» en el diseño de su API que puede sorprender a los futuros desarrolladores que mantengan su software.

Sin embargo, puede diseñar su API con menos «magia», pero aprovechando el poder de la conexión [[Prototype]].

Aquí, llamamos myObject.doCool(), que es un método que realmente existe en myObject, haciendo nuestro diseño de API más explícito (menos «mágico»). Internamente, nuestra implementación sigue el patrón de diseño de delegación (ver Capítulo 6), aprovechando la delegación de [[Prototype]] a otroObject.cool().

En otras palabras, la delegación tenderá a ser menos sorprendente/confusa si se trata de un detalle de implementación interna en lugar de estar claramente expuesto en el diseño de su API. Expondremos sobre la delegación con gran detalle en el próximo capítulo.

Revisión

Cuando se intenta acceder a una propiedad de un objeto que no tiene esa propiedad, el enlace [[Prototype]] interno del objeto define dónde debe buscar a continuación la operación [[Get]] (ver Capítulo 3). Esta conexión en cascada de objeto a objeto esencialmente define una «cadena de prototipo» (algo similar a una cadena de alcance anidada) de objetos a atravesar para la resolución de propiedades.

Todos los objetos normales tienen incorporado el Object.prototype como la parte superior de la cadena del prototipo (como el alcance global en la búsqueda de alcance), donde la resolución de la propiedad se detendrá si no se encuentra en ninguna parte antes de la cadena. toString(), valueOf(), y varias otras utilidades comunes existen en este objeto Object.prototype, explicando cómo todos los objetos en el lenguaje son capaces de acceder a ellos.

La manera más común de enlazar dos objetos es usando la palabra clave new con una llamada de función, que entre sus cuatro pasos (ver Capítulo 2), crea un nuevo objeto enlazado a otro objeto.

El «otro objeto» al que se enlaza el nuevo objeto pasa a ser el objeto referenciado por la propiedad arbitrariamente nombrada .prototype de la función llamada con new. Las funciones llamadas con new son a menudo llamadas «constructores», a pesar de que en realidad no están instanciando una clase como lo hacen los constructores en los lenguajes tradicionales orientados a clases.

Mientras que estos mecanismos JavaScript pueden parecerse a la «instanciación de clases» y a la «herencia de clases» de los lenguajes tradicionales orientados a clases, la distinción clave es que en JavaScript no se hacen copias. Más bien, los objetos terminan enlazados entre sí a través de una cadena [[Prototype]] interna.

Por una variedad de razones, entre las cuales el precedente terminológico no es la menor, «herencia» (y «herencia prototípica») y todos los otros términos de OO simplemente no tienen sentido cuando se considera cómo funciona realmente JavaScript (no sólo aplicado a nuestros modelos mentales forzados).

En cambio, «delegación» es un término más apropiado, porque estas relaciones no son copias sino vínculos de delegación.

6. Delegación de Comportamiento

En el capítulo 5, abordamos el mecanismo [[Prototype]] en detalle, y por qué es confuso e inapropiado (a pesar de los innumerables intentos durante casi dos décadas) describirlo como «clase» o «herencia». No sólo nos topamos con la sintaxis bastante verbosa (.prototype ensuciando el código), sino también con los diversos problemas (como la sorprendente resolución .constructor o la fea sintaxis pseudo-polimórfica). Exploramos las variaciones del enfoque «mixin», que mucha gente utiliza para intentar suavizar áreas tan ásperas.

Es una reacción común en este punto preguntarse por qué tiene que ser tan complejo hacer algo aparentemente tan simple. Ahora que hemos bajado el telón y visto lo sucio que se pone todo, no es una sorpresa que la mayoría de los desarrolladores de JS nunca se zambullen tan profundo, y en su lugar relegan tal lío a una biblioteca de «clases» para manejarlo por ellos.

Espero que ya no te contentes con pasar por alto y dejar esos detalles a una biblioteca de «cajas negras». Examinemos ahora cómo podríamos y deberíamos estar pensando en el mecanismo del objeto [[Prototype]] en JS, de una manera mucho más simple y directa que la confusión de clases.

Como una breve revisión de nuestras conclusiones del Capítulo 5, el mecanismo [[Prototype]] es un vínculo interno que existe en un objeto que hace referencia a otro objeto.

Esta vinculación se ejerce cuando se hace una referencia de propiedad/método contra el primer objeto, y no existe tal propiedad/método. En ese caso, el enlace [[Prototype]] le dice al motor que busque la propiedad/método en el objeto enlazado. A su vez, si ese objeto no puede cumplir la búsqueda, se sigue su [[Prototype]], y así sucesivamente. Esta serie de enlaces entre objetos forma lo que se llama la «cadena del prototipo».

En otras palabras, el mecanismo real, la esencia de lo que es importante para la funcionalidad que podemos aprovechar en JavaScript, se trata de que los objetos estén vinculados a otros objetos.

Esa sola observación es fundamental y crítica para entender las motivaciones y enfoques para el resto de este capítulo!

Hacia un diseño orientado a la delegación

Para enfocar correctamente nuestros pensamientos sobre cómo usar [[Prototype]] de la manera más directa, debemos reconocer que representa un patrón de diseño fundamentalmente diferente al de las clases (ver Capítulo 4).

Nota: Algunos principios del diseño orientado a clases siguen siendo muy válidos, así que no tires todo lo que sabes (¡sólo la mayor parte!). Por ejemplo, la encapsulación es bastante potente, y es compatible (aunque no tan común) con la delegación.

Necesitamos tratar de cambiar nuestro pensamiento del patrón de diseño de clases/herencia al patrón de diseño de delegación de comportamiento. Si usted ha basado la mayor parte, o toda su programación, en las clases, esto puede ser incómodo o sentirse antinatural. Es posible que tenga que intentar este ejercicio mental varias veces para acostumbrarse a esta forma muy diferente de pensar.

Voy a repasar algunos ejercicios teóricos primero, y luego veremos lado a lado un ejemplo más concreto para darle un contexto práctico para su propio código.

Teoría de la clase

Digamos que tenemos varias tareas similares («XYZ», «ABC», etc) que necesitamos modelar en nuestro software.

En el caso de las clases, la forma en que se diseña el escenario es