- Repasar conceptos de JavaScript que lo hacen diferente de otros lenguajes para dejar una base firme de conocimientos y evitar caer en problemas típicos que aparecen al llegar desde otros lenguajes estructurados o con un paradigma clásico de orientación a objetos.
- Presentar y comprender patrones usados muy frecuentemente en JavaScript para poder resolver problemas típicos sin rápidamente y de una manera óptima y elegante.
JavaScript es un lenguaje de programación que tiene las siguientes características principales:
- imperativo: su sintaxis deriva de la línea del lenguaje C y C++
- funcional: las funciones se pueden manipular como cualquier otra entidad dentro de un programa, incluso pueden ser pasadas a funciones y devueltas como resultado de la ejecución de una función
- dinámico: existen 6 tipos de datos que se chequean y convierten dinámicamente: Boolean, Number, String, Object, undefined y null
- orientado a objetos: los objetos son uno de los tipos de dato fundamentales
- herencia basada en prototipos: cada objeto tiene referencias a un objeto base o prototipo del cual hereda propiedades
El modelo de programación, haber estado diseñado originalmente para correr en navegadores, es asíncrono, con un solo hilo de ejecución y orientado a eventos.
El tipo booleano está compuesto por los valores true
y false
. Hay que tener presente que, al ser un lenguaje dinámico, los tipos se convierten automáticamente según las operaciones involucradas.
Cuando una expresión debe resolverse al tipo Boolean
, los valores 0
, ""
, null
y undefined
van a convertirse en false
. Cualquier otro valor, incluso {}
y []
serán convertidos a true
.
Todos los números son representados internamente como de punto flotante de doble precisión. Por eso:
0.1 + 0.2 // devuelve 0.30000000000000004
Todas las cadenas de texto son de este tipo. No existe el concepto de "caracter" de otros lenguajes.
Todos los objetos son conjuntos asociativos. Incluso las funciones son objetos.
Es el valor de las propiedades no definidas de un objeto. También es lo que devuelve una función que no devuelve nada explícitamente.
Es un objeto que representa al objeto nulo. Es distinto de {}
, que es un objeto sin propiedades.
A diferencia de los modelos de objetos tradicionales con clases, instancias y herencia simple o múltiple a través de clases o interfaces, en JavaSctipt todos los objetos son instancias y tienen una referencia a un objeto base, llamado "prototipo", del cual heredan propiedades.
Es un estilo de programación en el cual se basa en el uso de funciones para transformar entradas en salidas, a diferencia de los estilos imperativos tradicionales de lenguajes estructurados en donde el modelo de programación se basa en cambios de estado del propio proceso.
var usuarios = ["Juan", "Pedro", "Héctor"];
// estilo imperativo
// el estado del proceso se mantiene en la variable i
for (var i = 0; i < usuarios.length; i++) {
procesar(usuarios[i]);
}
// estilo funcional
usuarios.forEach(procesar);
Los objetos globales String
y Array
tienen muchas funciones que permiten utilizar este estilo.
Dado un array de números cualquiera, por ejemplo [1, 4, 2, 0, 6, 2]
, desarrollar una función que devuelva la cantidad de veces que un elemento es 0. Se la debería usar así:
var lista = [1, 4, 2, 0, 6, 2];
contarCeros(lista); // devuelve 1
No usar for dentro de la función sino las funciones propias del objeto Array.
Extender la función anterior para que evalúe cualquier condición. Por ejemplo:
function esCero(valor) {
return valor === 0;
}
contarSi(esCero, lista); // devuelve 1
Reimplementar contarCeros
utilizando contarSi
.
Resolver el ejercicio anterior sin utilizar Array.filter
.
Las variables tienen alcance dentro de la función que son declaradas, salvo las variables que se crean en el contexto global.
var variableGlobal = "global";
function verificarAlcance() {
var variableLocal = "local";
console.log(variableGlobal); // global
console.log(variableLocal); // local
}
verificarAlcance();
console.log(variableGlobal); // global
console.log(variableLocal); // ReferenceError!
Otra particularidad a tener en cuenta es el hoisting (izado) de las variables, que significa que son creadas (no inicializadas) al comienzo de la ejecución de la función, más allá de que estén declaradas en cualquier lado del cuerpo de la misma.
function pruebaHoisting() {
console.log(nombre); // undefined
var nombre = "Juan";
console.log(nombre); // Juan
}
pruebaHoisting();
Se pueden crear funciones dentro de funciones y estas funciones internas tienen acceso a las variables de la función externa, aún luego de haber salido de dicha función.
function crearPrefijo(prefijo) {
var str = prefijo + " ";
return function (nombre) {
return str + nombre;
}
}
var prefijarDr = crearPrefijo("Dr.");
prefijarDr("Gonzalez"); // Dr. Gonzalez
var prefijarSr = crearPrefijo("Sr.");
prefijarSr("Pérez"); // Dr. Pérez
Estas funciones internas mantienen una referencia a un objeto donde están declaradas todas las variables de la función externa, llamada clausura o closure. A su vez, la función externa también mantiene una referencia similar hasta llegar al objeto global, generándose una cadena de clausuras o closure chain.
ECMAScript 5, inicialmente 3.1, tuvo foco en seguridad y mejora de las librerías base, siempre pensando en mantener compatibilidad. Los puntos más destacados son:
- Nuevas funciones del objeto
Object
:create()
,defineProperty()
- Nuevas funciones de strings:
trim()
- Nuevas funciones de arrays:
filter()
,forEach()
,indexOf()
,map()
,reduce()
- Nuevas funciones del objeto
Date
:now()
- Nuevas funciones del objeto
JSON
:parse()
,stringify()
- Métodos de acceso (get y set)
- Modo estricto o strict mode
En el modo estricto está prohibida la creación de variables globales sin el uso de var
, la duplicación de propiedades en la definición de un objeto o parámetros de función, extiende la lista de palabras reservadas que no se pueden usar como propiedades o nombres de función, entre otros cambios.
function nombreCompleto(prefijo, nombre, nombre) {
"use strict";
return prefijo + " " + nombre + " " + apellido; // SyntaxError!
}
nombreCompleto("Dr.", "Juan", "Pérez");
Es muy utilizado en JavaScript y permite aislar estados y funciones del resto del código. La construcción más sencilla se basa en funciones anónimas que se ejecutan inmediátamente (IIEF, Immediately-Invoked Function Expression) que devuelven un objeto conteniendo la interfaz pública del módulo.
var modulo = (function () {
// variables privadas
var clavePublica = "secreto normal";
var clavePrivada = "secreto de Estado";
// interfaz pública
return {
obtenerClave: function () {
return clavePublica;
}
};
})();
modulo.obtenerClave(); // secreto normal
Crear un módulo que contenga en un array privado una lista de nomrbes de usuario. Este módulo debe exponer solo una función para obtener la lista:
var usuarios = (function () {
// ...
})();
usuarios.obtenerTodos(); // ["Juan", "Ana", "Pedro"]
Extender el módulo con una función para agregar usuarios y otra para obtener la cantidad de usuarios.
Extender el módulo para poder eliminar usuarios de la lista.
En el modelo de programación orientada a eventos y asíncrona, es necesario el uso de callbacks, funciones que son llamadas una vez que la tarea asíncrona es completada:
// simula una consulta asíncrona a la base de datos
function obtenerUsuarios(callback) {
setTimeout(function () {
var usuarios = ["Juan", "Ana", "Pedro"];
callback(usuarios);
}, 250); // demora 250 ms
}
obtenerUsuarios(function (usuarios) {
console.log(usuarios); // ["Juan", "Ana", "Pedro"]
});
Cuando se trabaja con Node.js, la convención es que las funciones callback siempre reciben como primer parámetro una variable que representa el estado de error de la operación:
obtenerUsuariosNode(function (err, usuarios) {
if (err) {
console.log(err.message);
return;
}
console.log(usuarios); // ["Juan", "Ana", "Pedro"]
});
Cuando es necesario encadenar operaciones asíncronas, el código puede volverse difícil de mantener:
// agregar un permiso de "borrar" a usuarios
obtenerUsuarios(function (usuarios) {
usuarios.forEach(function (usuario) {
obtenerRoles(usuario, function (roles) {
grabarRoles(usuario, roles.push("borrar"));
});
});
});
Asimismo, puede volverse muy complicado el manejor de errores en pasos intermedios.
// agregar un permiso de "borrar" a usuarios considerando errores
obtenerUsuarios(function (err, usuarios) {
if (err) {
console.log(err.message);
return;
}
usuarios.forEach(function (err, usuario) {
if (err) {
console.log(err.message);
return;
}
obtenerRoles(usuario, function (err, roles) {
if (err) {
console.log(err.message);
return;
}
grabarRoles(usuario, roles.push("borrar"), function (err) {
if (err) {
console.log(err.message);
return;
}
});
});
});
});
Un patrón de desarrollo que facilita la forma en la que se escribe el código asíncrono y, al mismo tiempo, el manejo de errores es el uso de promesas. Una promesa es un objeto que representa una ejecución de una operación asíncrona y puede estar en uno de tres estados: "pendiente", "completa" o "rechazada". En estos últimos dos casos, se ejecutará una función de continuación definida durante la creación de la promesa.
// simula una consulta asíncrona a la base de datos usando promesas
function obtenerUsuarios() {
return new Promise(function (fulfill) {
setTimeout(function () {
var usuarios = ["Juan", "Ana", "Pedro"];
fulfill(usuarios);
}, 250); // demora 250 ms
});
}
obtenerUsuarios().then(function (usuarios) {
console.log(usuarios); // ["Juan", "Ana", "Pedro"]
});
Todas las promesas tienen una propiedad then
que recibe la función que se va a ejecutar al resolverse dicha promesa. Incluso es posible encadenar promesas:
function aMayusculas(palabra) {
return palabra.toUpperCase();
}
obtenerUsuarios().then(function (usuarios) {
return usuarios.map(aMayusculas);
}).then(function (usuarios) {
console.log(usuarios); // ["JUAN", "ANA", "PEDRO"]
});
Incluso es posible definir funciones a realizarse sobre promesas que ya han sido resueltas anteriormente:
function aMinusculas(palabra) {
return palabra.toLowerCase();
}
var promesa = obtenerUsuarios().then(function (usuarios) {
console.log("promesa resuelta!");
return usuarios.map(aMinusculas);
});
promesa.then(function (usuarios) {
return usuarios.map(aMinusculas);
}).then(function (usuarios) {
console.log(usuarios); // ["JUAN", "ANA", "PEDRO"]
});
El ejemplo anterior de roles y permisos, de estar implementado con promesas, se podría escribir así:
// agregar un permiso de "borrar" a usuarios con promesas
obtenerUsuarios.then(function (usuarios) {
usuarios.forEach(function (usuario) {
obtenerRoles(usuario).then(function (roles) {
grabarRoles(usuario, roles.push("borrar"));
});
});
});
El manejo de errores se realiza pasando a then
una segunda función que se ejecutará en caso de error. En ese caso, la cadena de promesas se corta.
// agregar un permiso de "borrar" a usuarios con promesas
obtenerUsuarios.then(function (usuarios) {
// procesarlos
}, function (err) {
// manejar el error
});
Es conveniente revisar la compatibilidad de los diferentes navegadores o entornos como Node.js respecto del soporte nativo de promesas. En caso de querer dar soporte en ambientes que todavía no las tienen implementadas, es posible utilizar librerías como bluebird, Q, rsvp.js, etc. las que implementan la especificación Promises/A+, una de las más aceptadas.
Las siguientes funciones simulan operaciones asíncronas de acceso a datos de un sistema:
function obtenerUsuario(nombre) {
return new Promise(function (fulfill, reject) {
if (nombre === "Juan") {
fulfill({
rol: "usuario"
});
} else if (nombre === "Pedro") {
fulfill({
rol: "invitado"
});
} else {
reject(new Error("Usuario desconocido"));
}
});
}
function registrarAcceso(nombre, rol) {
return new Promise(function (fulfill, reject) {
console.log("Usuario " + nombre + " ingresa como " + rol);
fulfill();
})
}
function solicitarRegistro() {
console.log("Debe registrarse");
}
function notificarError(mensaje) {
console.log("Error: " + mensaje);
}
Realizar una función login()
que reciba un nombre de usuario, obtenga sus datos y registre su acceso. Utilizar las funciones de solicitud de registro y notificacion de errores para los casos que sea necesario.
function login(nombre) {
// ...
}
login("Pedro");
// Usuario Pedro ingresa como invitado
login("Juan");
// Usuario Juan ingresa como usuario
login("Pablo");
// Error: Usuario desconocido
// Debe registrarse
Extender la función login
para que pueda encadenarse una operación adicional en la cadena de promesas, llamando a la siguiente función en caso de un ingreso exitoso:
function registrarEstadísticas(datos) {
console.log("Estadísticas agregadas: " + datos.nombre + ", " + datos.rol + ", " + Date(datos.hora));
}
function login(nombre) {
// ...
}
login("Pedro").then(registrarEstadísticas)
// Usuario Pedro ingresa como invitado
// Estadísticas agregadas: Pedro, invitado, Sep 05 2015
Si este ejercicio se realiza en un browser, utilizar Google Chromeu otro que posea soporte nativo de promesas. Si se realiza en Node.js, incluir algún módulo de promesas como los nombrados anteriormente.
- Entrada de JavaScript en Wikipedia.
- Video presentación de Douglas Crockford: The JavaScript Programming Language
- Métodos del objeto String y Array en MDN
- Functional Programming en Eloquent JavaScript
- Video de Mark Miller: Changes to JavaScript, Part 1: EcmaScript 5
- Speaking JavaScript Chapter 25. New in ECMAScript 5
- Modo estricto en MDN
- Tablas de compatibilidad de ES5 y ES2015
- Implementaciones de Promises/A+.
- Taller javascripting de Nodeshool.
- Taller Scope, Chains & Closures de Nodeshool.