Tipos de datos

En JavaScript, los tipos de datos representan las diferentes categorías de valores que se pueden manejar en el lenguaje.


Cada vez que tengas un valor, caerá en 1 de los tipos de datos en JavaScript. Podemos almacenar un valor de cualquier tipo dentro de una variable.

Los lenguajes de programación que permiten estas cosas, como JavaScript, se denominan “dinámicamente tipados”, lo que significa que allí hay tipos de datos, pero las variables no están vinculadas rígidamente a ninguno de ellos.

📝

Operador `typeof`

El operador typeof devuelve el tipo de dato del operando. Es útil cuando queremos procesar valores de diferentes tipos de forma diferente o simplemente queremos hacer una comprobación rápida.

La llamada a typeof x devuelve una cadena con el nombre del tipo:

typeof undefined // "undefined"
typeof 0 // "number"
typeof 10n // "bigint"
typeof true // "boolean"
typeof "foo" // "string"
typeof Symbol("id") // "symbol"
typeof Math // "object"  (1)
typeof null // "object"  (2)
typeof alert // "function"  (3)
  1. Math es un objeto incorporado que proporciona operaciones matemáticas.
  2. El resultado de typeof null es "object". Esto está oficialmente reconocido como un error de comportamiento de typeof que proviene de los primeros días de JavaScript y se mantiene por compatibilidad. Definitivamente null no es un objeto. Es un valor especial con un tipo propio separado.
  3. El resultado de typeof alert es "function" porque alert es una función. Las funciones pertenecen al tipo objeto. Pero typeof las trata de manera diferente, devolviendo function. Además proviene de los primeros días de JavaScript. Técnicamente dicho comportamiento es incorrecto, pero puede ser conveniente en la práctica.

Primitivos

Son llamados tipos de datos primitivos porque solo pueden contener una cosa.

Métodos de tipos primitivos

JavaScript nos permite trabajar con tipos de datos primitivos (string, number, etc.) como si fueran objetos. Los primitivos también ofrecen métodos que podemos llamar.

Los tipos null y undefined no poseen métodos.

Primitivos como objetos

Aquí el dilema que enfrentó el creador de JavaScript:

La solución es algo enrevesada, pero aquí está:

  1. Los primitivos son aún primitivos. Con un valor único, como es deseable.
  2. El lenguaje permite el acceso a métodos y propiedades de strings, numbers, booleans y symbols.
  3. Para que esto funcione, se crea una envoltura especial, un “object wrapper” (objeto envoltorio) que provee la funcionalidad extra y luego es destruido.

Los “object wrappers” son diferentes para cada primitivo y son llamados: StringNumberBooleanSymbol y BigInt. Así, proveen diferentes sets de métodos.

📝

Ejemplo

let str = "Hola"; 
alert( str.toUpperCase() ); // HOLA

Lo que ocurre es lo siguiente:

  1. El string "str" es primitivo. Al momento de acceder a su propiedad, un objeto especial es creado, uno que conoce el valor del string y tiene métodos útiles como toUpperCase().
  2. Ese método se ejecuta y devuelve un nuevo string (mostrado con alert).
  3. El objeto especial es destruido, dejando solo el primitivo "str".

Cadenas de texto

En JavaScript, los datos textuales son almacenados como strings (cadena de caracteres). No hay un tipo de datos separado para caracteres unitarios.

El formato interno para strings es siempre UTF-16, no está vinculado a la codificación de la página.

Para declarar un string se pueden usar distintos tipos de comillas "", '' o `` (llamados "backticks" o "plantillas literales").

// Usando comillas dobles
let dobles = "Hola";
// Usando comillas simples
let simples = 'Como estas?'
// Usando plantillas literales
let plantilla = `Hola todo bien?`

Las comillas dobles y simples son intercambiables entre si, pero las plantillas literales permiten la interpolación de variables y expresiones dentro del string.

let nombre = "Matias";
 
let saludo = `Hola, ${nombre}`; // Hola, Matias

Si quisieras saber la cantidad de caracteres que tiene una cadena de texto bien podrías contarlas una por una pero seria bastante tedioso, para esto String tiene como propiedad length.

let nombre = "Matias"
// Si nosotros quisieramos saber cuantos caracteres tiene mi nombre podemos acceder a la propiedad length de la variable nombre (el cual es un wrapper String).
 
console.log(nombre.length) // 6 <-- Como resultado me devolvera el numero 6 el cual es la cantidad de caracteres que tiene la cadena de texto.

En el objeto wrapper de String hay muchos métodos útiles que puedes utilizar (cabe recalcar que se podría llegar a los mismos resultados sin utilizar estos métodos pero son de gran ayuda ya que nos ahorran mucho código), en este Link encontraras la lista de los métodos y su funcionalidad.

// Metodos de conversion 
const nombre = "matias"; 
// para convertir una cadena a letras mayusculas o minusculas existen dos metodos .toUpperCase() y .toLowerCase() sucesivamente.
console.log(nombre.toUpperCase()); // "MATIAS"
 
// Metodos de union y de division
let palabras = "Hola JavaScript Mundo";
let arrayPalabras = palabras.split(" ");
console.log(arrayPalabras); // ["Hola", "JavaScript", "Mundo"]

Buenas practicas

Estas son algunas buenas practicas a la hora de utilizar strings pero no son obligatorias, como lo dice la palabra son solo recomendaciones y dependen del contexto.

Números

Los números regulares en JavaScript son almacenados con el formato de 64-bit IEEE-754, conocido como “números de doble precisión de coma flotante”.

// Aunque todos los numeros son almacenados con ese formato podemos diferenciar de forma practica dos tipos: enteros y decimales (de coma flotante).
 
let entero = 10;
 
let decimal = 5.4;

Ahora en la practica esta diferencia tiene algunas implicancias.

Enteros vs coma flotante

  1. Enteros:
    • El interprete de JS puede manejar enteros precisos hasta un máximo de 53 bits (2^53-1).
    • Aunque se almacena en formato de coma flotante, si el numero es entero y esta dentro de este rango, entonces se comportara como un entero preciso
let x = 15; // Entero
 
let y = 10000000; // Entero grande, pero preciso al estar dentro del rango de 53 bits
  1. Coma flotante (Decimales):
    • Todos los números con fracciones, como 1.4 o 4.3, son interpretados y almacenados usando el formato IEEE-754 por JS.
    • Debido a que se almacenan en binario, los números decimales pueden tener problemas de precisión. Por ejemplo, 0.1 + 0.2 no es exactamente igual a 0.3 debido a la forma en que se representan los decimales en binario. Puedes ver el siguiente video donde se explica.
let z = 3.14; // Numero de coma flotante
 
let p = 0.1 + 0.2; // Numero de coma flotante pero que no es exactamente 0.3

Números especiales

En JS existen tres números especiales que tienen comportamientos únicos y son:

Estos valores se generan en situaciones especificas, como errores matemáticos o resultados de cálculos que exceden los limites numéricos.

NaN

Aunque suene contradictorio NaN es considerado por el interprete como un numero, es un valor especial dentro del tipo de dato numérico para representar los resultados que no son números validos. Es decir, es una forma de decir que la operación que lo produjo no tiene sentido numérico.

console.log(typeof NaN); // "number"

Ahora bien tenemos que entender el comportamiento de este valor único, ningún valor de NaN es igual, ni siquiera de si mismo, ya que estamos comparando algo que es indefinido o invalido.

console.log(NaN === NaN) // false
Infinity y -Infinity

Estos dos valores se dan cuando se exceden los limites en una operación matemática:

  1. Infinity:
    • Dividir un numero positivo por 0.
    • Un numero muy grande que supera los limites de la representación numérica en JS.
  2. -Infinity:
    • Dividir un numero negativo por 0.
    • Cuando una operación produce un resultado numérico extremadamente negativo.
let resultado1 = 1 / 0; // Infinity
let resultado2 = Math.pow(10, 1000); // Infinity
 
let resultado3 = -1 / 0; // -Infinity
let resultado4 = -Math.pow(10, 1000); // -Infinity

⚠️

Cosas a tener en cuenta

  • Infinity es mayor que cualquier número en JS.
  • -Infinity es menor que cualquier número en JS.

Booleanos

El tipo boolean tiene sólo dos valores posibles: true y false.

Este tipo se utiliza comúnmente para almacenar valores de sí/no: true significa “sí, correcto, verdadero”, y false significa “no, incorrecto, falso”.

let nameFieldChecked = true; // sí, el campo name está marcado
let ageFieldChecked = false; // no, el campo age no está marcado

Los valores booleanos también son el resultado de comparaciones:

let isGreater = 4 > 1;
 
alert( isGreater ); // true (el resultado de la comparación es "sí")

Indefinidos

Al igual que null, el valor especial undefined forma un tipo propio y su único valor es undefined.

El significado de undefined es “valor no asignado”.

Si una variable es declarada, pero no asignada, entonces su valor es undefined:

let age;
 
alert(age); // muestra "undefined"

⚠️

Asignar undefined a las variables

Técnicamente, es posible asignar undefined a cualquier variable

let age = 100;
 
// cambiando el valor a undefined
age = undefined;
 
alert(age); // "undefined"

…Pero no se recomienda hacer eso. Normalmente, usamos null para asignar un valor “vacío” o “desconocido” a una variable, mientras undefined es un valor inicial reservado para cosas que no han sido asignadas.

Nulos

El valor especial null contiene un solo valor y es su homónimo: null.

let age = null;

En JavaScript, null no es una “referencia a un objeto inexistente” o un “puntero nulo” como en otros lenguajes.

Es sólo un valor especial que representa “nada”, “vacío” o “valor desconocido”.

El código anterior indica que el valor de age es desconocido o está vacío por alguna razón.

BigInt

En JavaScript, el tipo “number” no puede representar de forma segura valores enteros mayores que (2531)(2^{53}-1) (eso es 9007199254740991), o menor que (2531)-(2^{53}-1) para negativos.

Para ser realmente precisos, el tipo de dato “number” puede almacenar enteros muy grandes (hasta 1.7976931348623157103081.7976931348623157 * 10^{308}), pero fuera del rango de enteros seguros ±(2531)±(2^{53}-1) habrá un error de precisión, porque no todos los dígitos caben en el almacén fijo de 64-bit. Así que es posible que se almacene un valor “aproximado”.

Por ejemplo, estos dos números (justo por encima del rango seguro) son iguales:

console.log(9007199254740991 + 1); // 9007199254740992
console.log(9007199254740991 + 2); // 9007199254740992

Podemos decir que ningún entero impar mayor que (2531)(2^{53}-1) puede almacenarse en el tipo de dato “number”.

Para la mayoría de los propósitos, el rango ±(2531)±(2^{53}-1) es suficiente, pero a veces necesitamos números realmente grandes; por ejemplo, para criptografía o marcas de tiempo de precisión de microsegundos.

BigInt se agregó recientemente al lenguaje para representar enteros de longitud arbitraria.

Un valor BigInt se crea agregando n al final de un entero o usando el wrapper:

const bigint = 1234567890123456789012345678901234567890n;
 
const sameBigint = BigInt("1234567890123456789012345678901234567890");
 
const bigintFromNumber = BigInt(10); // lo mismo que 10n

Símbolos

El valor de “Symbol” representa un identificador único.

let id = Symbol("id")

Se garantiza que los símbolos son únicos. Aunque declaremos varios Symbols con la misma descripción, éstos tendrán valores distintos. La descripción es solamente una etiqueta que no afecta nada más.

let id1 = Symbol("id");
let id2 = Symbol("id");
 
alert(id1 == id2); // false

Un symbol es un “valor primitivo único” con una descripción opcional.

⚠️

Los symbols no se auto convierten

La mayoría de los valores en JavaScript soportan la conversión implícita a string. Los Symbols son especiales, éstos no se auto convierten.

let id = Symbol("id");
alert(id); // TypeError: No puedes convertir un valor Symbol en string

Esta es una “protección del lenguaje” para evitar errores, ya que String y Symbol son fundamentalmente diferentes y no deben convertirse accidentalmente uno en otro.

Si realmente queremos mostrar un Symbol, necesitamos llamar el método .toString() explícitamente

let id = Symbol("id");
alert(id.toString()); // Symbol(id), ahora sí funciona

U obtener symbol.description para mostrar solamente la descripción:

let id = Symbol("id");
alert(id.description); // id

Objetos

Los objetos se utilizan para almacenar colecciones de datos y entidades mas complejas asociados con un nombre clave. Podemos crear un objeto usando las llaves {…} con una lista opcional de propiedades. Una propiedad es un par “key:value”, donde key es un string (también llamado “nombre clave”), y value puede ser cualquier cosa.

let user = new Object(); // sintaxis de "constructor de objetos"
let user = {};  // sintaxis de "objeto literal"

Normalmente se utilizan las llaves {…}. Esa declaración se llama objeto literal.

Herencia prototípica

En JavaScript los objetos pueden heredar propiedades y métodos de otros objetos a través de un enlace llamado prototipo. En lugar de heredar de una clase (como en otros lenguajes), cada objeto tiene un vinculo interno a otro objeto que actúa como un "modelo" de referencia.

Cuando intentamos acceder a una propiedad o método de un objeto y no se encuentra directamente en el, JavaScript busca esa propiedad en su prototipo, continuando a lo largo de la cadena de prototipos hasta llegar al objeto final, que es Object.prototype, o hasta que se agoten las referencias.

De esta forma permite al lenguaje compartir propiedades y métodos entre objetos sin la necesidad de copiar manualmente cada uno de ellos.

// Definimos el constructor Animal
function Animal(nombre){
	this.nombre = nombre;
};
 
// Añadimos el método saludar al prototipo de Animal
Animal.prototype.saludar = function() {
	console.log(`Hola, soy ${this.nombre}`); // aunque sea medio raro que nos salude nuestra mascota sirve como ejemplo
};
 
// Definimos el constructor Perro, que "hereda" de Animal
function Perro(nombre, raza){
	// Llamamos al constructor de Animal para inicializar la propiedad nombre
	Animal.call(this, nombre);
	this.raza = raza;
};
 
// Establecemos la herencia del prototipo de Animal en el prototipo de Perro
Perro.prototype = Object.create(Animal.prototype);
 
// Restauramos el constructor de Perro (por defecto apunta a Animal)
Perro.prototype.constructor = Perro;
 
// Ahora podemos crear una instancia de Perro llamada "Nala"
const nala = new Perro("Nala", "Terrier");
 
// Como nala es un objeto de la instancia perro que a su vez hereda de Animal posee en su cadena de prototipos el metodo saludar() que antes creamos
nala.saludar()
 
// Tambien en el prototipo de Perro tenemos la propiedad "raza" por lo cual deberiamos poder acceder
console.log(nala.raza)

Object Prototype

En JavaScript, los objetos tienen una propiedad oculta especial [[Prototype]] (como se menciona en la especificación); que puede ser null, o hacer referencia a otro objeto llamado “prototipo”:

Prototype

Cuando leemos una propiedad de object, si JavaScript no la encuentra allí la toma automáticamente del prototipo. En programación esto se llama “herencia prototípica”. Pronto estudiaremos muchos ejemplos de esta herencia y otras características interesantes del lenguaje que se basan en ella.

La propiedad [[Prototype]] es interna y está oculta, pero hay muchas formas de configurarla.

Una de ellas es usar el nombre especial __proto__, así:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};
 
rabbit.__proto__ = animal; // establece rabbit.[[Prototype]] = animal

Si buscamos una propiedad en rabbit y no se encuentra, JavaScript la toma automáticamente de animal.

Por ejemplo:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};
 
rabbit.__proto__ = animal; // (*)
 
// Ahora podemos encontrar ambas propiedades en conejo:
console.log("🚀 ~ rabbit.eats: " + rabbit.eats) // ** true
console.log("🚀 ~ rabbit.jumps: " + rabbit.jumps) // true

Aquí, la línea (*) establece que animal es el prototipo de rabbit.

Luego, cuando alert intenta leer la propiedad rabbit.eats (**), no la encuentra en rabbit, por lo que JavaScript sigue la referencia [[Prototype]] y la encuentra en animal (busca de abajo hacia arriba):

Protoype Ejemplo

Aquí podemos decir que "animal es el prototipo de rabbit" o que "rabbit hereda prototípicamente de animal".

Entonces, si animal tiene muchas propiedades y métodos útiles, estos estarán automáticamente disponibles en rabbit. Dichas propiedades se denominan “heredadas”.

Si tenemos un método en animal, se puede llamar en rabbit:

let animal = {
  eats: true,
  walk() {
    alert("Animal da un paseo");
  }
};
 
let rabbit = {
  jumps: true,
  __proto__: animal
};
 
// walk es tomado del prototipo
rabbit.walk(); // Animal da un paseo

El método se toma automáticamente del prototipo, así:

protype Ejemplo 2

La cadena prototipo puede ser más larga:

let animal = {
  eats: true,
  walk() {
    alert("Animal da un paseo");
  }
};
 
let rabbit = {
  jumps: true,
  __proto__: animal
};
 
let longEar = {
  earLength: 10,
  __proto__: rabbit
};
 
// walk se toma de la cadena prototipo
longEar.walk(); // Animal da un paseo
alert(longEar.jumps); // verdadero (desde rabbit)

Protoype Ejemplo 3

Ahora, si leemos algo de longEar y falta, JavaScript lo buscará en rabbit, y luego en animal.

Solo hay dos limitaciones:

  1. No puede haber referencias circulares. JavaScript arrojará un error si intentamos asignar __proto__ en círculo.
  2. El valor de __proto__ puede ser un objeto o null. Otros tipos son ignorados.

También puede ser obvio, pero aún así: solo puede haber un [[Prototype]]. Un objeto no puede heredar desde dos.

📝

`__proto__` es un getter/setter histórico para `[[Prototype]]`

Es un error común de principiantes no saber la diferencia entre ambos.

Tenga en cuenta que __proto__ no es lo mismo que la propiedad interna [[Prototype]]. En su lugar, __proto__ es un getter/setter para [[Prototype]].

La propiedad __proto__ es algo antigua y existe por razones históricas, por lo que los navegadores y entornos del lado del servidor continúan soportándola, así que es bastante seguro su uso. Según la especificación, solamente los navegadores están obligados a continuar soportándola. Desde JavaScript Moderno se recomienda el uso de las funciones Object.getPrototypeOf y Object.setPrototypeOf para obtener y establecer el prototipo.

Objetos globales

El término "objetos globales" (u objetos incorporados estándar) aquí no debe confundirse con el objeto global. Aquí, los objetos globales se refieren a objetos en el ámbito global. Se puede acceder al objeto global en sí usando el operador this en el ámbito global (pero solo si no se usa el modo estricto ECMAScript 5, en ese caso devuelve undefined). De hecho, el alcance global consiste en las propiedades del objeto global, incluidas las propiedades heredadas, si las hay.

Propiedades de valor

Estas propiedades globales devuelven un valor simple; ellos no tienen propiedades o métodos.

Propiedades de función

Estas funciones globales -funciones llamadas globalmente en lugar de un objeto- devuelven directamente sus resultados a la persona que llama.

Objetos fundamentales

Estos son los objetos fundamentales y básicos sobre los que se basan todos los demás objetos. Esto incluye objetos que representan objetos generales, funciones y errores.

Números y fechas

Estos son los objetos base que representan números, fechas y cálculos matemáticos.

Procesamiento de texto

Estos objetos representan cadenas y soporte para manipularlos.

Colecciones indexadas

Estos objetos representan colecciones de datos que están ordenadas por un valor de índice. Esto incluye matrices (tipadas) y construcciones tipo array.

Colecciones con clave

Estos objetos representan colecciones que usan claves; estos contienen elementos que son iterables en el orden de inserción.

Datos estructurados

Estos objetos representan e interactúan con los búferes de datos estructurados y los datos codificados utilizando la notación de objetos JavaScript (JSON del inglés JavaScript Object Notation).

Objetos de abstracción de control

Reflexion

Internacionalización

Adiciones al núcleo de ECMAScript para funcionalidades sensibles al lenguaje.