Apéndice

Herencia prototípica

A excepción de null y undefined, cada tipo de datos primitivo tiene un prototipo, un wrapper de objeto correspondiente que proporciona métodos para trabajar con valores. Cuando se invoca un método o una búsqueda de propiedad en una primitiva, JavaScript une la primitiva en segundo plano y llama al método, o bien realiza la búsqueda de propiedades en el objeto wrapper.

Por ejemplo, un literal de string no tiene métodos propios, pero puedes llamar al método .toUpperCase() en él gracias al wrapper de objetos String correspondiente:

"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL

Esto se denomina herencia prototípica, que hereda propiedades y métodos del constructor correspondiente de un valor.

Number.prototype
> Number { 0 }
>  constructor: function Number()
>  toExponential: function toExponential()
>  toFixed: function toFixed()
>  toLocaleString: function toLocaleString()
>  toPrecision: function toPrecision()
>  toString: function toString()
>  valueOf: function valueOf()
>  <prototype>: Object { … }

Puedes crear primitivas con estos constructores, en lugar de solo definirlas por su valor. Por ejemplo, cuando se usa el constructor String, se crea un objeto de cadena, no un literal de cadena, un objeto que no solo contiene el valor de nuestra cadena, sino todas las propiedades y los métodos heredados del constructor.

const myString = new String( "I'm a string." );

myString;
> String { "I'm a string." }

typeof myString;
> "object"

myString.valueOf();
> "I'm a string."

En general, los objetos resultantes se comportan como los valores que usamos para definirlos. Por ejemplo, aunque definir un valor numérico con el constructor new Number da como resultado un objeto que contiene todos los métodos y propiedades del prototipo Number, puedes usar operadores matemáticos en esos objetos como lo harías en literales de números:

const numberOne = new Number(1);
const numberTwo = new Number(2);

numberOne;
> Number { 1 }

typeof numberOne;
> "object"

numberTwo;
> Number { 2 }

typeof numberTwo;
> "object"

numberOne + numberTwo;
> 3

Muy rara vez, necesitarás usar estos constructores, ya que la herencia prototípica integrada de JavaScript significa que no proporcionan beneficios prácticos. La creación de primitivas con constructores también puede generar resultados inesperados porque el resultado es un objeto, no un literal simple:

let stringLiteral = "String literal."

typeof stringLiteral;
> "string"

let stringObject = new String( "String object." );

stringObject
> "object"

Esto puede complicar el uso de operadores de comparación estrictos:

const myStringLiteral = "My string";
const myStringObject = new String( "My string" );

myStringLiteral === "My string";
> true

myStringObject === "My string";
> false

Inserción automática de punto y coma (ASI)

Mientras analizan una secuencia de comandos, los intérpretes de JavaScript pueden usar una función llamada inserción automática de punto y coma (ASI) para intentar corregir instancias de punto y coma omitidos. Si el analizador de JavaScript encuentra un token no permitido, intenta agregar un punto y coma antes de ese token para corregir el posible error de sintaxis, siempre que se cumplan una o más de las siguientes condiciones:

  • Ese token está separado del token anterior por un salto de línea.
  • Ese token es }.
  • El token anterior es ), y el punto y coma insertado sería el punto y coma final de una declaración do...while.

Para obtener más información, consulta las reglas de ASI.

Por ejemplo, omitir el punto y coma después de las siguientes instrucciones no causará un error de sintaxis debido a ASI:

const myVariable = 2
myVariable + 3
> 5

Sin embargo, ASI no puede contabilizar múltiples sentencias en la misma línea. Si escribes más de una declaración en la misma línea, asegúrate de separarlas con punto y coma:

const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier

const myVariable = 2; myVariable + 3;
> 5

ASI es un intento de corrección de errores, no una clase de flexibilidad sintáctica integrada en JavaScript. Asegúrate de utilizar punto y coma cuando sea apropiado, de modo que no dependas de él para producir el código correcto.

Modo estricto

Los estándares que rigen la forma en que se escribe JavaScript han evolucionado mucho más allá de todo lo que se consideró durante los primeros diseños del lenguaje. Cada cambio nuevo en el comportamiento esperado de JavaScript debe evitar causar errores en sitios web anteriores.

ES5 soluciona algunos problemas antiguos con la semántica de JavaScript sin interrumpir las implementaciones existentes, ya que introduce el "modo estricto", una forma de habilitar un conjunto de reglas de lenguaje más restrictivo para una secuencia de comandos completa o una función individual. Para habilitar el modo estricto, usa el literal de string "use strict", seguido de un punto y coma, en la primera línea de una secuencia de comandos o una función:

"use strict";
function myFunction() {
  "use strict";
}

El modo estricto evita ciertas acciones "no seguras" o funciones obsoletas, arroja errores explícitos en lugar de los más comunes y prohíbe el uso de sintaxis que puedan entrar en conflicto con funciones de lenguaje futuras. Por ejemplo, las decisiones iniciales de diseño en torno al alcance de variable hicieron que sea más probable que los desarrolladores "contaminan" por error el alcance global cuando declaran una variable, sin importar el contexto que la contiene, mediante la omisión de la palabra clave var:

(function() {
  mySloppyGlobal = true;
}());

mySloppyGlobal;
> true

Los entornos de ejecución modernos de JavaScript no pueden corregir este comportamiento sin el riesgo de dañar cualquier sitio web que dependa de él, ya sea por error o deliberadamente. En cambio, el JavaScript moderno lo evita, ya que permite que los desarrolladores habiliten el modo estricto para trabajos nuevos y habiliten el modo estricto de forma predeterminada solo en el contexto de funciones nuevas de lenguaje en las que no rompen las implementaciones heredadas:

(function() {
    "use strict";
    mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal

Debes escribir "use strict" como un literal de string. Un literal de plantilla (use strict) no funcionará. También debes incluir "use strict" antes de cualquier código ejecutable en el contexto deseado. De lo contrario, el intérprete lo ignorará.

(function() {
    "use strict";
    let myVariable = "String.";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal

(function() {
    let myVariable = "String.";
    "use strict";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope

Por referencia, por valor

Cualquier variable, incluidas las propiedades de un objeto, los parámetros de la función y los elementos de un arreglo, conjunto o mapa, puede contener un valor básico o un valor de referencia.

Cuando se asigna un valor básico de una variable a otra, el motor de JavaScript crea una copia de ese valor y lo asigna a la variable.

Cuando asignas un objeto (instancias de clase, arrays y funciones) a una variable, en lugar de crear una copia nueva de ese objeto, la variable contiene una referencia a la posición almacenada del objeto en la memoria. Debido a esto, cambiar un objeto al que hace referencia una variable cambia el objeto al que se hace referencia, no solo un valor contenido por esa variable. Por ejemplo, si inicializas una variable nueva con una que contiene una referencia de objeto y, luego, la utilizas para agregar una propiedad a ese objeto, la propiedad y su valor se agregan al objeto original:

const myObject = {};
const myObjectReference = myObject;

myObjectReference.myProperty = true;

myObject;
> Object { myProperty: true }

Esto es importante no solo para alterar objetos, sino también para realizar comparaciones estrictas, ya que la igualdad estricta entre objetos requiere que ambas variables hagan referencia a el mismo objeto para evaluarlo como true. No pueden hacer referencia a diferentes objetos, incluso si esos objetos son idénticos en términos de estructura.

const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};

myObject === myNewObject;
> false

myObject === myReferencedObject;
> true

Asignación de memoria:

JavaScript usa la administración automática de memoria, lo que significa que no es necesario asignar ni desasignar la memoria de forma explícita durante el desarrollo. Si bien los detalles de los enfoques de los motores de JavaScript para la administración de la memoria están fuera del alcance de este módulo, comprender cómo se asigna la memoria proporciona un contexto útil para trabajar con valores de referencia.

Hay dos "áreas" en la memoria: la "pila" y el "montón". La pila almacena datos estáticos (valores primitivos y referencias a objetos) porque la cantidad fija de espacio necesaria para almacenar estos datos se puede asignar antes de que se ejecute la secuencia de comandos. El montón almacena objetos, que necesitan espacio asignado de forma dinámica porque su tamaño puede cambiar durante la ejecución. La memoria se libera mediante un proceso llamado "recolección de elementos no utilizados", que quita de la memoria los objetos sin referencias.

El subproceso principal

JavaScript es, en esencia, un lenguaje de un solo subproceso con un modelo de ejecución “síncrono”, lo que significa que solo puede ejecutar una tarea a la vez. Este contexto de ejecución secuencial se denomina subproceso principal.

El subproceso principal se comparte con otras tareas del navegador, como analizar HTML, renderizar y volver a renderizar partes de la página, ejecutar animaciones de CSS y controlar interacciones del usuario que van desde lo simple (como destacar texto) hasta lo complejo (como interactuar con los elementos del formulario). Los proveedores de navegadores encontraron formas de optimizar las tareas que realiza el subproceso principal, pero las secuencias de comandos más complejas aún pueden usar demasiados de los recursos del subproceso principal y afectar el rendimiento general de la página.

Algunas tareas se pueden ejecutar en subprocesos en segundo plano llamados trabajadores web, con algunas limitaciones:

  • Los subprocesos de trabajo solo pueden actuar en archivos JavaScript independientes.
  • Redujeron considerablemente el acceso a la ventana y la IU del navegador, o bien no lograron hacerlo.
  • Están limitados en cuanto a la forma en que se puede comunicar con el subproceso principal.

Estas limitaciones las hacen ideales para tareas enfocadas y de uso intensivo de recursos que, de lo contrario, podrían ocupar el subproceso principal.

La pila de llamadas

La estructura de datos que se usa para administrar “contextos de ejecución” (el código que se ejecuta de forma activa) es una lista llamada pila de llamadas (a menudo, solo “la pila”). Cuando se ejecuta una secuencia de comandos por primera vez, el intérprete de JavaScript crea un “contexto de ejecución global” y lo envía a la pila de llamadas, con declaraciones dentro de ese contexto global que se ejecutan una a la vez, de arriba abajo. Cuando el intérprete encuentra una llamada a una función mientras ejecuta el contexto global, envía un “contexto de ejecución de funciones” para esa llamada a la parte superior de la pila, pausa el contexto de ejecución global y ejecuta el contexto de ejecución de la función.

Cada vez que se llama a una función, el contexto de ejecución de la función para esa llamada se envía a la parte superior de la pila, justo encima del contexto de ejecución actual. La pila de llamadas funciona de "primero en entrar, primero en salir", lo que significa que la llamada a función más reciente, que es la más alta en la pila, se ejecuta y continúa hasta que se resuelve. Cuando se completa esa función, el intérprete la quita de la pila de llamadas, y el contexto de ejecución que contiene esa llamada a función vuelve a ser el elemento más alto de la pila y reanuda la ejecución.

Estos contextos de ejecución capturan cualquier valor necesario para su ejecución. También establecen las variables y las funciones disponibles dentro del alcance de la función según su contexto superior, y determinan y establecen el valor de la palabra clave this en el contexto de la función.

El bucle de eventos y la cola de devoluciones de llamada

Esta ejecución secuencial implica que las tareas asíncronas que incluyen funciones de devolución de llamada, como recuperar datos de un servidor, responder a la interacción del usuario o esperar temporizadores establecidos con setTimeout o setInterval, bloquearían el subproceso principal hasta que se complete la tarea o interrumpirían inesperadamente el contexto de ejecución actual en el momento en que se agregue el contexto de ejecución de la función de devolución de llamada a la pila. Para solucionar este problema, JavaScript administra las tareas asíncronas mediante un "modelo de simultaneidad" controlado por eventos compuesto por el "bucle de eventos" y la "cola de devoluciones de llamada" (a veces conocida como "cola de mensajes").

Cuando se ejecuta una tarea asíncrona en el subproceso principal, el contexto de ejecución de la función de devolución de llamada se coloca en la cola de devoluciones de llamada, no en la parte superior de la pila de llamadas. El bucle de eventos es un patrón que a veces se denomina reactor, que sondea de forma continua el estado de la pila de llamadas y la cola de devoluciones de llamada. Si hay tareas en la cola de devoluciones de llamada y el bucle de eventos determina que la pila de llamadas está vacía, las tareas de la cola de devoluciones de llamada se envían a la pila, una a la vez, para su ejecución.