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)
- Math es un objeto incorporado que proporciona operaciones matemáticas.
- 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.
- 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:
- Hay muchas cosas que uno querría hacer con los tipos primitivos, como un
string
o unnumber
. Sería grandioso accederlas usando métodos. - Los Primitivos deben ser tan rápidos y livianos como sea posible.
La solución es algo enrevesada, pero aquí está:
- Los primitivos son aún primitivos. Con un valor único, como es deseable.
- El lenguaje permite el acceso a métodos y propiedades de
strings
,numbers
,booleans
ysymbols
. - 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: String
, Number
, Boolean
, Symbol
y BigInt
. Así, proveen diferentes sets de métodos.
📝
Ejemplo
let str = "Hola";
alert( str.toUpperCase() ); // HOLA
Lo que ocurre es lo siguiente:
- El
string
"str" es primitivo. Al momento de acceder a su propiedad, un objeto especial es creado, uno que conoce el valor delstring
y tiene métodos útiles comotoUpperCase()
. - Ese método se ejecuta y devuelve un nuevo
string
(mostrado conalert
). - 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.
- Recomendar el uso de
const
parastrings
inmutables. - Evitar el uso excesivo de concatenaciones con el operador
+
cuando las plantillas literales son más claras. - Elegir correctamente entre comillas simples, dobles y plantillas literales según el 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
- 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
- El interprete de JS puede manejar enteros precisos hasta un máximo de 53 bits (
let x = 15; // Entero
let y = 10000000; // Entero grande, pero preciso al estar dentro del rango de 53 bits
- Coma flotante (Decimales):
- Todos los números con fracciones, como
1.4
o4.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 a0.3
debido a la forma en que se representan los decimales en binario. Puedes ver el siguiente video donde se explica.
- Todos los números con fracciones, como
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:
NaN
(Not-a-Number).Infinity
(Infinito).-Infinity
(-Infinito).
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:
Infinity
:- Dividir un numero positivo por 0.
- Un numero muy grande que supera los limites de la representación numérica en JS.
-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 (eso es 9007199254740991
), o menor que para negativos.
Para ser realmente precisos, el tipo de dato “number”
puede almacenar enteros muy grandes (hasta ), pero fuera del rango de enteros seguros 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 puede almacenarse en el tipo de dato “number”.
Para la mayoría de los propósitos, el rango 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”:
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):
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í:
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)
Ahora, si leemos algo de longEar
y falta, JavaScript lo buscará en rabbit
, y luego en animal
.
Solo hay dos limitaciones:
- No puede haber referencias circulares. JavaScript arrojará un error si intentamos asignar
__proto__
en círculo. - El valor de
__proto__
puede ser un objeto onull
. 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.
Infinity
NaN
undefined
null
globalThis
Propiedades de función
Estas funciones globales -funciones llamadas globalmente en lugar de un objeto- devuelven directamente sus resultados a la persona que llama.
eval()
isFinite()
isNaN()
parseFloat()
parseInt()
decodeURI()
decodeURIComponent()
encodeURI()
encodeURIComponent()
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.
Object
Function
Boolean
Symbol
Error
EvalError
InternalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
Números y fechas
Estos son los objetos base que representan números, fechas y cálculos matemáticos.
Number
BigInt
Math
Date
Procesamiento de texto
Estos objetos representan cadenas y soporte para manipularlos.
String
RegExp
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.
Array
- Int8Array
- Uint8Array
- Uint8ClampedArray
- Int16Array
- Uint16Array
- Int32Array
- Uint32Array
- Float32Array
- Float64Array
- BigInt64Array
- BigUint64Array
Colecciones con clave
Estos objetos representan colecciones que usan claves; estos contienen elementos que son iterables en el orden de inserción.
Map
Set
WeakMap
WeakSet
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).
ArrayBuffer
DataView
JSON
Objetos de abstracción de control
Promise
Generator
GeneratorFunction
Reflexion
- Reflect
- Proxy
Internacionalización
Adiciones al núcleo de ECMAScript para funcionalidades sensibles al lenguaje.
Intl
Intl.Collator
Intl.DateTimeFormat
Intl.NumberFormat