Una nueva introducción a JavaScript


Tabla de contenidos:

Presentación

¿Por qué una nueva introducción? Porque JavaScript es, con toda la razón, el lenguaje de programación peor entendido del mundo. Aunque a menudo ridiculizado como un juguete, por debajo de su engañosa simplicidad, se encuentran algunas características de un lenguaje innovador, que ahora es utilizado por un número increible de aplicaciones JavaScript de alto perfil, lo que demuestra que un conocimiento más profundo de esta tecnología es una habilidad importante para cualquier desarrollador web.

Resulta útil tener una primera idea de la historia de este lenguaje. JavaScript fue creado en 1995 por Brendan Eich, un ingeniero de Netscape, y lanzado por primera vez con Netscape 2 a principios de 1996. En un principio iba a llamarse LiveScript, pero se le cambió el nombre en una decisión de marketing nefasta para tratar de capitalizar la popularidad del lenguaje Java de Sun Microsystems, a pesar de que los dos tenían muy poco en común. Esta ha sido una fuente de confusión desde entonces.

Microsoft lanzó una versión bastante compatible del lenguaje llamado JScript con IE tres meses más tarde. Netscape en 1997 presentó el lenguaje a Ecma International , un organismo europeo de normalización, lo que dio como resultado la primera edición del estándar ECMAScript. En 1999 el estándar recibió una actualización importante con ECMAScript 3ª edición, y se ha mantenido bastante estable desde entonces. La cuarta edición fue abandonada, debido a las diferencias políticas sobre la complejidad del lenguaje. Muchas partes de la cuarta edición que fueron la base para la nueva edición de ECMAScript 5 se publicaron en diciembre de 2009.

Esta estabilidad es una gran noticia para los desarrolladores, puesto que han concedido más tiempo para que se pongan al día las distintas implementaciones. Me voy a centrar casi exclusivamente en el dialecto de la edición 3. Por familiaridad con este término, voy a seguir empleando "JavaScript".

A diferencia de muchos lenguajes de programación, el lenguaje JavaScript no tiene ningún concepto de entrada o salida. Está diseñado para ejecutarse como un lenguaje de script en un entorno de host y depende de dicho entorno de host el proporcionar mecanismos para comunicarse con el mundo exterior. El entorno de host más común es el navegador, pero los intérpretes de JavaScript también se pueden encontrar en Adobe Acrobat, Photoshop, el motor de los widgets de Yahoo!, e incluso entornos del lado del servidor.

Información general

JavaScript es un lenguaje dinámico orientado a objetos, dispone de tipos y operadores, objetos básicos y métodos. Su sintaxis proviene de los lenguajes Java y C, por lo que muchas estructuras de estos lenguajes se aplican también a JavaScript. Una de las principales diferencias es que JavaScript no tiene clases; la funcionalidad de las clases se consigue mediante prototipos de objetos. La otra gran diferencia es que las funciones son objetos, lo que le confiere a estas la capacidad de contener código ejecutable y moverlo de un lado a otro como cualquier otro objeto.

Vamos a empezar mirando el componente básico de cualquier lenguaje: los tipos. Los programas JavaScript manipulan los valores y dichos valores pertenecen todos a un tipo. Los tipos de JavaScript son los siguientes:

... ah, y Undefined (no definido) y Null (nulo), que son un poco extraños, y las Matrices , que son un tipo especial de objeto. Y las Fechas y Expresiones regulares, que son objetos que vienen de regalo. Y para ser técnicamente precisos, las funciones son un tipo especial de objeto. Así que el diagrama de tipos tiene más este aspecto:

  • Número
  • Cadena de caracteres
  • Booleano
  • Objeto:

    • Función
    • Matriz
    • Fecha
    • RegExp
  • Null
  • Undefined

Y también hay incluidos algunos tipos de Error. Sin embargo, las cosas se facilitan mucho si nos guiamos por el primer diagrama.

Números

Los números en JavaScript son, de acuerdo con la especificación, "valores de IEEE 754 con formato de 64 bits de doble precisión". Esto tiene algunas consecuencias interesantes. No existe el entero en JavaScript, por lo que tienes que prestar un poco de atención con tu aritmética si estás acostumbrado a las matemáticas en C o Java. Ten cuidado con cosas como:

0.1 + 0.2 = 0.30000000000000004

Son compatibles los operadores numéricos estándares , incluyendo la adición, la sustracción, la aritmética modular, etc. También hay un objeto integrado que me olvidé de mencionar antes llamado Math para manejar funciones matemáticas más avanzadas y constantes:

Math.sin(3.5);
var d = Math.PI * r * r;

Puedes convertir una cadena de caracteres a un entero utilizando la función integrada parseInt(). Admite  la base de conversión como un segundo argumento opcional, que deberías proporcionar siempre:

> parseInt("123", 10)
  123
> parseInt("010", 10)
  10

Si no proporcionas la base, puedes conseguir resultados que te sorprendan:

> parseInt("010")
  8

Eso sucede porque la función parseInt decidió tratar la cadena de caracteres como octal debido la presencia del 0 a la izquierda.

Si deseas convertir un número binario en un número entero, solo tienes que cambiar la base:

> parseInt("11", 2)
  3

Del mismo modo, puedes analizar los números de punto flotante mediante la función integrada parseFloat() que siempre utiliza la base 10, a diferencia de su prima parseInt().

También puedes usar el operador unario "+" para convertir valores a números:

> + "42"
  42

Un valor especial llamado NaN abreviatura de "Not a Number" (No es un Número) se devuelve si la cadena de caracteres es no numérica:

> parseInt("hello", 10)
  NaN

NaN es tóxico: si lo proporcionas como entrada a cualquier operación matemática el resultado también será NaN :

> NaN + 5
  NaN

Puedes probar NaN utilizando la función integrada isNaN():

> isNaN(NaN)
  true

JavaScript también tiene los valores especiales Infinity y -Infinity :

> 1 / 0 
  Infinity 
> -1 / 0
  -Infinity

Nota: las funciones parseInt() y parseFloat() analizan una cadena de caracteres hasta que llegan a un carácter que no es válido para el formato de número especificado, entonces devuelve el número analizado hasta ese momento. Sin embargo, el operador "+" simplemente convierte la cadena de caracteres a NaN si hay algún carácter no válido en el mismo. Simplemente trata de analizar tú mismo en la consola la cadena de caracteres "10.2abc" con cada método y vas a entender mejor las diferencias.

Cadenas de caracteres (strings)

Las cadenas o "strings" en JavaScript son secuencias de caracteres. Más exactamente, son secuencias de caracteres Unicode, en las que cada carácter está representado por un número de 16 bits. Esto debería ser una buena noticia para cualquier persona que haya tenido que hacer frente a la internacionalización.

Si quieres representar un solo carácter, simplemente utiliza una cadena de caracteres de longitud 1.

Para encontrar la longitud de una cadena de caracteres, accede a su propiedad length:

> "hello".length 5

¡Es nuestro primer contacto con los objetos JavaScript! ¿He mencionado que las cadenas de caracteres son objetos también? Y tienen métodos:

> "hello".charAt(0)
  h
> "hello, world".replace("hello", "goodbye")
  goodbye, world
> "hello".toUpperCase()
  HELLO

Otros tipos

JavaScript distingue entre null , que es un objeto de tipo 'objeto' que indica la ausencia deliberada de valor, y undefined , que es un objeto de tipo "sin definir" que indica un valor sin inicializar, es decir, un valor que ni siquiera ha sido asignado todavía. Hablaremos acerca de las variables más tarde, pero en JavaScript es posible declarar una variable sin asignar un valor a la misma. Si lo haces, el tipo de variable es undefined .

JavaScript tiene un tipo booleano, con posibles valores true y false (los cuales son palabras clave). Cualquier valor puede ser convertido a un valor booleano de acuerdo a las siguientes reglas:

  1. false , 0 , la cadena de caracteres vacía ( "" ), NaN , null , y undefined todos pasan a ser false
  2. el resto de los otros valores pasan a ser true

Puedes realizar esta conversión explícitamente con la función Boolean():

> Boolean("")
  false
> Boolean(234)
  true

Sin embargo, esto rara vez es necesario, ya que JavaScript de forma automática llevará a cabo esta conversión cuando se espere un valor booleano, como en una instrucción if (véase más adelante). Por esta razón, a veces hablamos simplemente de "valores verdaderos" y "valores falsos", es decir los valores que se convierten en true (verdadero) y false (falso), respectivamente, cuando se convierten a booleanos . Alternativamente, estos valores pueden llamarse "verdadistas" y "falsistas", respectivamente.

Las operaciones booleanas como && (y lógico), || (o lógico) y ! (no-lógico) son compatibles, véase más adelante.

Variables

Las variables nuevas en JavaScript se declaran con la palabra var:

var a;                // el valor es undefined
var nombre = "Simon";   // el valor es Simon

Si se declara una variable sin asignarle un valor, su tipo es undefined (no definido).

Una diferencia importante con otros lenguajes como Java es que, en JavaScript, los bloques no tienen ámbito, sólo las funciones lo tienen. Así que si una variable se define utilizando var en una instrucción compuesta (por ejemplo dentro de una estructura de control if), será visible para toda la función.

Operadores

Los operadores numéricos de JavaScript son + , - , * , / y % (que es el operador de resto). Los valores son asignados con = y también hay asignación de instrucciones compuestas, tales como += y -= . Estos se expanden a x = x operator y .

x += 5
x = x + 5

Puedes utilizar ++ y -- para aumentar o disminuir respectivamente. Estos pueden ser usados ​​como operadores de prefijo o sufijo.

El operador + también hace concatenación de cadenas de caracteres:

> "hola" + " mundo"
  hola mundo

Si añades una cadena de caracteres a un número (u otro valor) todo se convierte primero en una cadena de caracteres. Esto podría sorprenderte:

> "3" + 4 + 5
  345
> 3 + 4 + "5"
  75

Agregar una cadena de caracteres vacía a algo es una manera útil de convertirla.

Las comparaciones en JavaScript se pueden hacer usando < , > , <= y >= . Esto funciona tanto para cadenas de caracteres como para los números. La igualdad es un poco menos sencilla. El operador de comparación (o doble igual) realiza conversión de tipos si se dan diferentes tipos, a veces con resultados interesantes:

> "perro" == "perro"
  true
> 1 == true
  true

Para evitar la conversión de tipos, utiliza el operador triple igual:

> 1 === true
  false
> true === true
  true

Existen también operadores = y !==.

JavaScript también tiene operaciones bit a bit . Si deseas utilizarlas, ahí las tienes.

Estructuras de control

JavaScript tiene un conjunto de estructuras de control similar a otros lenguajes de la familia C. Las instrucciones condicionales se implementan con if y else y se pueden encadenar varias si lo deseas:

var nombre = "gatitos";

if (nombre == "cachorros") {
    nombre += "!";

} else if (nombre == "gatitos") {
    nombre += "!!";

} else {
    nombre = "!" + nombre;
}

// El resultado es gatitos!!

JavaScript tiene bucles while y do-while. El primero es bueno para hacer bucles básicos, el segundo para bucles cuyo cuerpo deseas asegurarte que se ejecute al menos una vez:

while (true) {
    // ¡un bucle infinito!
}

var input;

do {
    input = get_input();
} while (inputIsNotValid(input));

El bucle for de JavaScript es el mismo que en C y Java: permite proporcionar la información de control de tu bucle en una sola línea.

for (var i = 0; i < 5; i++) {
    // Se ejecutará 5 veces
}

Los operadores && y || utilizan la lógica de cortocircuito, lo que significa que la ejecución de su segundo operando depende del primero. Esto es útil para el control de objetos nulos antes de acceder a sus atributos:

var name = o && o.getName();

O para los valores de configuración predeterminados:

var name = otherName || "default";

JavaScript tiene un operador ternario para expresiones condicionales:

var allowed = (age > 18) ? "yes" : "no";

La instrucción switch se puede utilizar para ejecutar distintas bifurcaciones según el valor de un número o una cadena de caracteres:

switch(accion) {
    case 'dibujar': 
        funcionDibuja();
        break;
    case 'comer':
        funcionCome();
        break;
    default:
        noHacerNada();
}

Si no agregas una instrucción break, la ejecución "pasará a través" de la siguiente bifurcación. Esto es algo que casi nunca quieres hacer; de hecho, vale la pena etiquetar específicamente este "paso a través" deliberado con un comentario si de verdad es lo que quieres, para ayudar en la depuración:

switch (a) {
    case 1: 
        // paso a través
    case 2:
        funcionComer();
        break;
    default:
        noHacerNada();
}

La cláusula default es opcional. Además puedes tener expresiones tanto en la parte de switch como en las del case si lo deseas; las comparaciones entre las dos se realizan con el operador ===:

switch(1 + 3) {
    case 2 + 2:
        funcionBien();
        break;
    default:
        nuncaSucede();
}

Objetos

Los objetos de JavaScript son simplemente colecciones de pares nombre-valor. Como tales, son similares a:

  • Diccionarios en Python
  • Los valores hash en Perl y Ruby
  • Tablas hash en C y C + +
  • HashMaps en Java
  • Matrices asociativas en PHP

El hecho de que esta estructura de datos sea tan ampliamente utilizada es una prueba de su versatilidad. Puesto que todo (eje de tipo básico) en JavaScript es un objeto, esto implica una gran cantidad de búsquedas en las tablas hash pero. ¡Afortunadamente son muy rápidas!

El nombre es una cadena de caracteres de JavaScript, mientras que el valor puede ser cualquier valor JavaScript, incluyendo más objetos, lo que te permite construir estructuras de datos de complejidad arbitraria.

Hay dos formas básicas para crear un objeto vacío:

var obj = new Objeto();

Y:

var obj = {};

Estos son semánticamente equivalentes, y el segundo se llama sintaxis literal del objeto y resulta más cómodo. Esta sintaxis también es el núcleo del formato JSON y debe elegirse preferiblemente en todo momento.

Una vez creado, se puede acceder a las propiedades de un objeto de dos maneras:

obj.nombre = "Simon";
var nombre = obj.nombre;

Y ...

obj["nombre"] = "Simon";
var nombre = obj["nombre"];

También son semánticamente equivalentes. El segundo método tiene la ventaja de que el nombre de la propiedad se presenta como una cadena de caracteres, lo que significa que se puede calcular en tiempo de ejecución, aunque el uso de este método impide que se apliquen algunas optimizaciones de motores y compresores de JavaScript. También se puede utilizar para configurar y obtener propiedades con nombres que son palabras reservadas:

obj.for = "Simon"; // Error de sintaxis, porque 'for' es una palabra reservada 
obj["for"] = "Simon"; // funciona bien

La sintaxis literal de objeto se puede utilizar para inicializar un objeto en su totalidad:

var obj = {
         nombre: "Carlos",
         "for": "Futbol",
         detalles: {
               color: "naranja",
               altura: 12
         }
    }

El acceso de atributos se puede encadenar:

> obj.detalles.color;
  naranja;
> obj["detalles"]["altura"];
  12;

Matrices

Las matrices en JavaScript son en realidad un tipo especial de objeto. Funcionan de manera muy similar a los objetos ordinarios (a las propiedades numéricas naturalmente, sólo se puede acceder usando sintaxis [] ) pero tienen una propiedad mágica llamada length que retorna un valor enterosiempre uno más el índice mayor de la matriz.

Antes se creaban matrices de la siguiente manera:

> var a = new Array();
> a[0] = "perro";
> a[1] = "gato";
> a[2] = "vaca";
> a.length;
  3

Una notación más cómoda es utilizar una declaración literal de matriz:

> var a = ["perro", "gato", "vaca"];
> a.length;
  3

Dejar una coma al final de un literal de matriz presenta un comportamiento inconsistente entre los distintos navegadores, así que no lo hagas.

Ten en cuenta que array.length no es necesariamente el número de elementos de la matriz. Observa lo siguiente:

> var a = ["perro", "gato", "vaca"];
> a[100] = "zorro";
> a.length;
  101

Recuerda: la longitud de la matriz es uno más que el índice mayor.

Si se consulta un índice de matriz no existente, se obtiene undefined :

> typeof(a[90]);
  undefined

Si tienes en cuenta lo anterior, puedes iterar por una matriz usando lo siguiente:

for (var i = 0; i < a.length; i++) {
    // Hacer algo con una [i]
}

Esto es ligeramente ineficiente puesto que se busca la propiedad de longitud una vez cada bucle. Una forma de mejorarlo es la siguiente:

for (var i = 0, len = a.length; i < len; i++) {
    // Hacer algo con una [i]
}

Una expresión simplificada aún mejor es la siguiente:

for (var i = 0, item; item = a[i++];) {
    // Hacer algo con el elemento
}

Aquí estamos inicializando dos variables. La asignación en la parte media del bucle for también se evalúa para comprobar su veracidad y, si tiene éxito, el bucle continúa. Dado que i se incrementa cada vez, a los elementos de la matriz se les asignará un elemento en orden secuencial. El bucle se detiene cuando se encuentra un elemento "falsista" (como undefined ).

Ten en cuenta que este truco sólo debe utilizarse para las matrices que sabes que no contienen valores "falsistas" (matrices de objetos o nodos DOM, por ejemplo). Si estás iterando a través de datos numéricos que pueden incluir un 0 o una cadena de caracteres de datos que, a su vez, pueden incluir "" (cadena de caracteres vacía), deberías utilizar en su lugar la expresión simplificada i, j.

Otra forma de iterar es utilizar el bucle for...in. Ten en cuenta que si alguien agrega nuevas propiedades a Array.prototype, también serán iteradas por este bucle:

for (var i in a) {
    // Hacer algo con a[i]
}

Si deseas agregar un elemento a una matriz, la forma más segura de hacerlo es así:

a[a.length] = item; // equivalente a a.push(item);

Dado que a.length es uno más que el índice mayor, puedes estar seguro que la asignación la vas a realizar a una posición vacía, osea al final de la matriz.

Las matrices vienen con una serie de métodos:

a.toString(), a.toLocaleString(), a.concat(item, ..), a.join(sep),
a.pop(), a.push(item, ..), a.reverse(), a.shift(), a.slice(start, end),
a.sort(cmpfn), a.splice(start, delcount, [item]..), a.unshift([item]..)
  • concat: devuelve una nueva matriz con los elementos agregados a ella
  • pop: elimina y devuelve el último elemento
  • push: agrega uno o más elementos al final (como nuestra expresión simplificada (o "azúcar sintáctica") ar[length])
  • slice: devuelve un submatriz
  • sort: tiene una función de comparación opcional
  • splice: permite modificar una matriz suprimiendo una parte de la misma y reemplazándola con elementos
  • unshift: antepone elementos al inicio de la matriz

Funciones

Junto con los objetos, las funciones son el componente central en la comprensión de JavaScript. La función más básica no podría ser más simple:

function sumar(x, y) {
    var total = x + y;
    return total;
}

Esto muestra todo lo que hay que saber sobre las funciones básicas. Una función de JavaScript puede tener 0 o más parámetros con nombre. El cuerpo de la función puede contener tantas instrucciones como quieras y puede declarar sus propias variables que son locales a esa función. Se puede utilizar la instrucción return para devolver un valor en cualquier momento, finalizando la función. Si no se utiliza ninguna instrucción return (o un "return" sin valor) JavaScript devuelve undefined .

Los parámetros con nombre resultan ser más directrices que cualquier otra cosa. Puedes llamar a una función sin  pasar los parámetros que espera, en cuyo caso se establecerán en undefined.

> sumar();
  NaN // No se puede realizar la suma en undefined

También puedes probar con más argumentos de los que la función espera:

> sumar(2, 3, 4);
  5 // sumó los dos primeros, el 4 fue ignorado

Eso puede parecer un poco tonto, pero las funciones tienen acceso a una variable adicional dentro de su cuerpo que se llama arguments, que es un objeto similar a una matriz que contiene todos los valores pasados ​​a la función. Vamos a re-escribir la función sumar para que tome tantos valores como queramos:

function sumar() {
    var valores = 0;
    for (var i = 0, j = arguments.length; i < j; i++) {
      valores += arguments[i];
    }
    return valores;
}

> sumar(2, 3, 4, 5);
  14

Sin embargo, esto no es más útil que escribir 2 + 3 + 4 + 5. Vamos a crear una función de promedio:

function promedio() {
    var suma = 0;
        for (var i = 0, j = arguments.length; i < j; i++) {
            suma += arguments[i];
        }
    return suma / arguments.length;
}

> promedio(2, 3, 4, 5);
  3.5

Esto es bastante útil, pero introduce un nuevo problema. La función promedio() toma una lista separada por comas de los argumentos, ¿pero qué ocurre si quieres hallar el promedio de una matriz? Simplemente puedes reescribir la función de la siguiente manera:

function promedioArray(arr) {
    var suma = 0;
    for (var i = 0, j = arr.length; i < j; i++) {
        suma += arr[i];
    }
    return suma / arr.length;
}

> promedioArray([2, 3, 4, 5]);
  3.5

Pero sería bueno poder re-utilizar la función que habíamos creado. Afortunadamente, JavaScript te permite llamar a una función y llamarla con un conjunto arbitrario de argumentos, utilizando el método apply() de cualquier objeto de función.

> promedio.apply(null, [2, 3, 4, 5]);
  3.5

El segundo argumento para apply() es la matriz que se va a utilizar como argumentos; el primero se discutirá más adelante. Esto pone de relieve el hecho de que las funciones son objetos también.

Funciones Anónimas

JavaScript permite crear funciones anónimas.

var promedio = function() {
    var suma = 0;
    for (var i = 0, j = arguments.length; i < j; i++) {
        suma += arguments[i];
    }
    return suma / arguments.length;
}

Esto es semánticamente equivalente a la forma de la función promedio(). Esto es extremadamente potente, ya que permite poner la definición completa de la función en cualquier parte donde normalmente pondrías una expresión. Esto permite todo tipo de trucos ingeniosos. He aquí una manera de "esconder" algunas variables locales — como bloquear alcance en C:

> var a = 1;
> var b = 2;
> (function() {
      var b = 3;
      a += b;
  })();

> a
  4 
> b
  2

JavaScript te permite llamar a funciones de manera recurrente. Esto es particularmente útil para tratar con estructuras de árbol, como las que tienes en el navegador DOM .

function countChars(elm) {
    if (elm.nodeType == 3) {  // TEXT_NODE
        return elm.nodeValue.length;
    }

    var count = 0;
    for (var i = 0, child; child = elm.childNodes[i]; i++) {
        count += countChars(child);
    }
    return count;
}

Esto pone de relieve un problema potencial con funciones anónimas: ¿cómo llamarlas de forma recursiva si no tienen un nombre? JavaScript nos permite nombrar expresiones de función para ello. Puedes utilizar nombre IIFEs (expresiones de función invocada inmediatamente) como se indica a continuación:

var charsInBody = (function counter(elm) {
    if (elm.nodeType == 3) { // TEXT_NODE
        return elm.nodeValue.length;
    }
    var count = 0;
    for (var i = 0, child; child = elm.childNodes[i]; i++) {
        count += counter(child);
    }
    return count;
})(document.body);

El nombre proporcionado a una función anónima como la anterior está (o al menos debería estar) sólo disponible para el propio ámbito de la función. Esto permite que el motor realice más optimizaciones y que el código sea más legible.

Objetos personalizados

Nota: para una discusión más detallada de la programación orientada a objetos en JavaScript, consulta Introducción a JavaScript orientado a objetos .

En la programación orientada a objetos clásica, los objetos son recopilaciones de datos y métodos que operan sobre dichos datos. JavaScript es un lenguaje basado en prototipos que no contiene una palabra clave class para definirla, como podemos encontrar en otros lenguajes como por ejemplo en  C++ ó Java. (Esto es a veces confuso para los programadores acostumbrados a los lenguajes con una declaración de clase.) En su lugar, JavaScript utiliza a las funciones como clases. Vamos a considerar un objeto crearPersona con campos de nombre y apellido. Hay dos formas en el que se podría mostrar el nombre: como "nombre apellido" o como "apellido, nombre". Utilizando las funciones y los objetos que hemos discutido anteriormente, esta es una forma de hacerlo:

function crearPersona(nombre, apellido) {
    return {
        nombre: nombre,
        apellido: apellido
    }
}
function nombreCompleto(persona) {
    return persona.nombre + ' ' + persona.apellido;
}
function invertirNombreCompleto(persona) {
    return persona.apellido + ', ' + persona.nombre
}

> s = crearPersona("Simon", "Willison");
> nombreCompleto(s);
  Simon Willison

> invertirNombreCompleto(s);
  Willison, Simon

Esto funciona, pero es bastante feo. Obtendrás decenas de funciones en el espacio de nombre global. Lo que realmente necesitamos es una manera de asociar una función a un objeto. Dado que las funciones son objetos, esto es fácil:

function crearPersona(nombre, apellido) {
    return {
        nombre: nombre,
        apellido: apellido,
        nombreCompleto: function() {
            return this.nombre + ' ' + this.apellido;
        },
        invertirNombreCompleto: function() {
            return this.apellido + ', ' + this.nombre;
        }
    }
}

> s = crearPersona("Simon", "Willison")
> s.nombreCompleto();
  Simon Willison

> s.invertirNombreCompleto();
  Willison, Simon

Aquí hay algo que no hemos visto: la palabra clave this. Utilizada dentro de la función, this  se refiere al objeto actual. Lo que realmente significa es la forma específica de como se llama a la función. Si la llamamos utilizando la notación de puntos o notación de corchetes sobre un objeto, ese objeto se convierte en this. Si la notación de puntos no fue utilizada para la llamada, this se refiere al objeto global. Esto constituye una causa frecuente de errores. Por ejemplo,

> s = crearPersona("Simon", "Willison");
> var nombreCompleto = s.nombreCompleto;
> nombreCompleto();
  undefined undefined

Cuando llamamos a nombreCompleto() , this está ligado al objeto global. Dado que no hay ninguna variable global llamada nombre o apellido tenemos undefined para cada uno.

Podemos aprovechar la palabra clave this para mejorar nuestra función crearPersona:

function Persona(nombre, apellido) {
    this.nombre = nombre;
    this.apellido = apellido;
    this.nombreCompleto = function() {
        return this.nombre + ' ' + this.apellido;
  }
    this.invertirNombreCompleto = function() {
        return this.apellido + ', ' + this.nombre;
  }
}
var s = new Persona("Simon", "Willison");

Hemos introducido otra palabra clave: 'new'. new está relacionada estrechamente con this. Lo que hace es crear un objeto vacío completamente nuevo y luego llama a la función que especificamos, con this estableciendo a ese nuevo objeto. Las funciones que están diseñadas para ser llamadas por new se llaman funciones constructoras. Lo que se suele hacer habitualmente es aprovechar estas funciones como un recordatorio para llamarlas con new .

Nuestro objeto persona están mejorando, pero todavía hay algunos detalles feos ahi. Cada vez que creamos un objeto persona, creamos también dos nuevos objetos de función dentro de él, ¿no sería mejor si este código fuese compartido?

function nombreCompleto() {
    return this.nombre + ' ' + this.apellido;
}
function invertirNombreCompleto() {
    return this.apellido + ', ' + this.nombre;
}
function Persona(nombre, apellido) {
    this.nombre = nombre;
    this.apellido = apellido;
    this.nombre = nombreCompleto;
    this.invertirNombre = invertirNombreCompleto;
}

Esto está mejor: estamos creando los métodos de la función sólo una vez y les estamos asignando referencias dentro del constructor. ¿Podemos hacerlo mejor? La respuesta es que sí.

function Persona(nombre, apellido) {
    this.nombre = nombre;
    this.apellido = apellido;
}
Persona.prototype.nombreCompleto = function() {
    return this.nombre + ' ' + this.apellido;
}
Persona.prototype.invertirNombreCompleto = function() {
    return this.apellido + ', ' + this.nombre;
}

Persona.prototype es un objeto compartido para todas las instancias de Persona . Forma parte de una cadena de búsqueda (que tiene un nombre especial, "cadena prototipo"): cada vez que intentes acceder a una propiedad de Persona que no está establecida, JavaScript comprobará Persona.prototype para ver si esa propiedad existe. Como resultado, cualquier cosa asignada a Persona.prototype está disponible para todas las instancias de este constructor a través del objeto this.

Esta es una herramienta con un potencial increíble. JavaScript permite modificar un determinado prototipo en cualquier momento en tu programa, lo que significa que puedes añadir métodos adicionales para los objetos existentes en tiempo de ejecución:

> s = new Persona("Simon", "Willison");
> s.nombreCaps();
  TypeError on line 1: s.nombreCaps is not a function
> Persona.prototype.nombreCaps = function() {
    return this.nombre.toUpperCase();
}
> s.nombreCaps();
  SIMON

Curiosamente, también puedes agregar cosas al prototipo de objetos JavaScript integrados. Vamos a añadir un método a String que devuelve esa cadena de código a la inversa:

> var s = "Simon";
> s.reversed();
  TypeError on line 1: s.reversed is not a function
> String.prototype.reversed = function() {
    var r = "";
    for (var i = this.length - 1; i >= 0; i--) {
        r += this[i];
    }
    return r;
}
> s.reversed();
  nomiS

¡Nuestro nuevo método funciona incluso en los literales de las cadenas de código!

> "This can now be reversed".reversed()
  desrever eb won nac sihT

Como mencioné antes, el prototipo forma parte de una cadena. La raíz de esa cadena es Object.prototype , cuyos métodos incluyen toString(), que es el método que se llama cuando se intenta representar un objeto como una cadena de caracteres. Esto es útil para la depuración de nuestros objetos Person:

> var s = new Persona("Simon", "Willison");
> s
  [Object Object]
> Persona.prototype.toString = function() {
      return '<Persona: ' + this.fullName() + '>';
  }
> s
  <Persona: Simon Willison>

¿Recuerdas cómo promedio.apply() tuvo un primer argumento nulo? Podemos volver a eso de nuevo ahora. El primer argumento para apply() es el objeto que debe ser tratado como this. Por ejemplo, aquí hay una aplicación trivial de new:

function trivialNew(constructor) {
    var o = {};
    // Create an object constructor.apply(o, arguments);
    return o;
}

Esto no es una réplica exacta de new , ya que no establece la cadena de prototipo. apply() es difícil de ilustrar, no es algo que se use muy a menudo, pero resulta útil conocerlo.

apply() tiene una función hermana llamada call , que a su vez te permite configurar this, pero tiene una lista de argumentos ampliada en lugar de una matriz.

function apellidoCaps() {
    return this.apellido.toUpperCase();
}
var s = new Persona("Simon", "Willison");
apellidoCaps.call(s);

// Es lo mismo que: 
s.apellidoCaps = apellidoCaps;
s.apellidoCaps();

Funciones internas

JavaScript permite declarar una función dentro de otra. Ya vimos esto antes con la función crearPersona(). Un detalle importante con las funciones anidadas es que en JavaScript se pueden acceder a las variables en el ámbito de su función primaria:

function unMejorEjemplo() {
    var a = 1;
    function unaFuncionMas() {
        return a + 1;
    }
    return unaFuncionMas();
}

Esto proporciona una gran utilidad a la hora de escritura código, además es más fácil de mantener. Si una función depende de una o dos funciones que no resultan útiles a otras partes de tu código, puedes anidar las funciones útiles dentro de la función que va a ser  llamada desde otra parte. Esto mantiene el número de funciones que están en el ámbito global hacia abajo, lo cual siempre está bien.

Este es también un gran contador de la atracción por las variables globales. Al escribir código complejo, a menudo resulta tentador utilizar variables globales para compartir valores entre las funciones múltiples, lo que conduce a crear código que es difícil de mantener. Las funciones anidadas pueden compartir variables en sus funciones primarias, por lo que puedes utilizar ese mecanismo para emparejar funciones cuando tenga sentido sin contaminar el espacio de nombres global ('globales locales' si lo prefieres). Se recomienda utilizar esta técnica con precaución, pero es una habilidad que resulta útil tener.

Clausura

Para una mayor comprensión de este tema puedes revisar la definición de closures en wikipedia. Esto nos lleva a una de las abstracciones con más posibilidades que JavaScript tiene para ofrecer, pero también la que puede dar lugar a más confusión. ¿Qué es lo que hace?

function makeAdder(a) {
    return function(b) {
        return a + b;
    }
}

x = makeAdder(5);
y = makeAdder(20);
x(6)
?
y(7)
?

El nombre de la función makeAdder explica lo que hace: crear nuevas funciones de agregado ("adder"), que cuando son llamadas con un argumento se agregan al argumento con el que fueron creadas.

Lo que está pasando aquí es más o menos lo mismo que sucedía con las funciones internas anteriormente: una función definida dentro de otra función tiene acceso a las variables de la función externa. La única diferencia aquí es que la función externa ha vuelto y el sentido común, por lo tanto, parece dictar que sus variables locales ya no existen. Pero todavía existen, de lo contrario las funciones de agregado no podrían funcionar. Es más, hay dos "copias" de las variables locales de makeAdder: una en la que a es 5 y otra en la que a es 20. Así, el resultado de las llamadas de función es el siguiente:

x (6) // devuelve 11
y (7) // devuelve 27

Esto es lo que está sucediendo realmente. Cada vez que JavaScript ejecuta una función, se crea un objeto 'ámbito' para contener las variables locales creadas dentro de dicha función. Se inicializa con cualquier variable aprobada como parámetros de función. Esto es similar al objeto global en el que viven todas las variables y funciones globales, pero con un par de diferencias importantes: en primer lugar, se crea un objeto de ámbito totalmente nuevo cada vez que una función comienza a ejecutarse y en segundo lugar, a diferencia del objeto global (que en los navegadores es accesible como ventana),a estos objetos de ámbito no se pueden acceder directamente desde tu código JavaScript. No hay ningún mecanismo para iterar sobre las propiedades del objeto de ámbito actual, por ejemplo.

Así que cuando se llama a makeAdder, se crea un objeto de ámbito con una propiedad: a , que es el argumento que se pasa a la función makeAdder. makeAdder continuación, devuelve una función de nueva creación. Normalmente el colector de basura de JavaScript limpiaría el objeto de ámbito creado para makeAdder en este momento, pero la función devuelta mantiene una referencia hacia ese objeto de alcance. Como resultado, el objeto de ámbito será basura que no se recogerá hasta que no haya más referencias al objeto de función que makeAdder devolvió.

Los objetos de ámbito forman una cadena que se llama la cadena de ámbito, similares a la cadena de prototipo que utiliza el sistema objeto de JavaScript.

Un cierre es la combinación de una función y el objeto ámbito en el que se creó.

Los cierres te permiten guardar el estado y, como tal, a menudo se pueden utilizar en lugar de objetos.

Pérdidas de memoria

Un desafortunado efecto secundario de los cierres es que hacen trivialmente fácil perder memoria en Internet Explorer. JavaScript es un lenguaje que recoge basura: los objetos se asignan a la memoria cuando son creados y esta memoria es reclamada por el navegador cuando no permanece ninguna referencia a un objeto. Los objetos proporcionados por el entorno de host son gestionados por dicho entorno.

Los hosts del navegador tienen que gestionar un gran número de objetos que representan la página HTML que se presenta: los objetos del DOM. Depende del navegador la gestión de la asignación y la recuperación de estos.

Internet Explorer utiliza su propio sistema de recolección de elementos para ello, aparte del mecanismo utilizado por JavaScript. Es la interacción entre los dos LO que pueden causar pérdidas de memoria.

Una pérdida de memoria en Internet Explorer se produce en el momento en que se forma una referencia circular entre un objeto de JavaScript y un objeto nativo. Presta atención a lo siguiente:

function leakMemory() {
    var el = document.getElementById('el');
    var o = { 'el': el };
    el.o = o;
}

La referencia circular formada anteriormente crea una pérdida de memoria. IE no liberará la memoria utilizada por el y el o hasta que el navegador se reinicie por completo.

El caso anterior puede que pase desapercibido porque las pérdidas de memoria sólo se convierten en una preocupación real en aplicaciones de larga ejecución o en aplicaciones que pierden grandes cantidades de memoria debido a las estructuras de datos de gran tamaño o patrones de pérdidas dentro de los bucles.

Las fugas son rara vez tan obvias puesto que a menudo la estructura de datos perdidos puede tener muchas capas de referencias, oscureciendo la referencia circular.

Los cierres facilitan la creación de una pérdida de memoria sin pretenderlo. Consideremos lo siguiente:

function addHandler() {
    var el = document.getElementById('el');
    el.onclick = function() {
        this.style.backgroundColor = 'red';
    }
}

El código anterior configura el elemento para que se ponga de color rojo cuando se hace clic en él. También crea una pérdida de memoria. ¿Por qué? Porque la referencia a el está capturada accidentalmente en el cierre creado para la función interna anónima. Esto crea una referencia circular entre un objeto JavaScript (la función) y un objeto nativo ( el ).

needsTechnicalReview ();

Existen una serie de soluciones para este problema. La más simple es no utilizar la variable el:

function addHandler(){
    document.getElementById('el').onclick = function(){
        this.style.backgroundColor = 'red';
    }
}

Sorprendentemente, un truco para romper las referencias circulares introducidas por el cierre es añadir otro cierre:

function addHandler() {
    var clickHandler = function() {
            this.style.backgroundColor = 'red';
        };

    (function() {
        var el = document.getElementById('el');
        el.onclick = clickHandler;
    })();
}

La función interna se ejecuta de inmediato y oculta el contenido del cierre creado con clickHandler .

Otro buen truco para evitar cierres es romper las referencias circulares durante el evento window.onunload, aunque muchas bibliotecas de eventos te lo hará. Ten en cuenta que al hacerlo se deshabilita bfcache en Firefox 1.5 , así que no deberías registrar un proceso de escucha unload en Firefox, a menos que tengas otras razones para hacerlo.

 

Información del documento original

  • Autor: Simon Willison
  • Última actualización: 07 de marzo 2006
  • Copyright: © 2006 Simon Willison, cuya colaboración tiene la licencia Reconocimiento-Compartir igual 2.0 de Creative Commons.
  • Más información: para obtener más información acerca de este tutorial (y para los enlaces a las diapositivas originales de la charla), consulta el artículo del weblog Etech de Simon .

Añadir nuevo comentario

CAPTCHA
Esta pregunta es para comprobar si usted es un visitante humano y prevenir envíos de spam automatizado.
4 + 6 =
Resuelva este simple problema matemático y escriba la solución; por ejemplo: Para 1+3, escriba 4.