Tabla de contenidos
- 1. Presentación
- 2. Información general
- 3. Números
- 4. Cadenas de caracteres (strings)
- 5. Otros tipos
- 6. Variables
- 7. Operadores
- 8. Estructuras de control
- 9. Objetos
- 10. Matrices
- 11. Funciones
- 12. Objetos personalizados
- 13. Funciones internas
- 14. Cierres
- 14.1. Pérdidas de memoria
- 15. Información del documento original
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 lenguaje innovadoras. 2005 ha visto el lanzamiento de una serie de aplicaciones de 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 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 1997. El estándar recibió una actualización importante como ECMAScript 3ª edición en 1999, 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 procede 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 función 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 algunos tipos de error incluidos. 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 utiliza la base 10 siempre, 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
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 devuelven 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:
false,0, la cadena de caracteres vacía (""),NaN,null, yundefinedtodos pasan a serfalse- 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; var name = "simon";
Si se declara una variable sin asignar ningún valor a la misma, 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:
> "hello" + " world" hello world
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 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:
> "dog" == "dog" 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 quieres:
var name = "kittens"; if (name == "puppies") { name += "!"; } else if (name == "kittens") { name += "!!"; } else { name = "!" + name; } name == "kittens!!"
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 ejecuta 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(action) { case 'draw': drawit (); break; case 'eat': eatit (); break; default: donothing(); }
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: / / fallthrough case 2: eatit (); break; default: donothing (); }
La cláusula "default" es opcional. Puedes tener expresiones tanto en la parte de "switch" como en las de "case" si quieres; las comparaciones entre las dos se realizan con el operador ===:
switch(1 + 3) { case 2 + 2: yay(); break; default: neverhappens (); }
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 (tipos de barra principales) en JavaScript es un objeto, cualquier programa JavaScript, naturalmente, implica una gran cantidad de búsquedas en tablas hash. ¡Afortunadamente son muy rápidas!
La parte "name" (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 Object ();
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 es también el núcleo del formato JSON y debe elegirse preferiblemente en todo momento.
Una vez creado, a las propiedades de un objeto se puede acceder de dos maneras:
obj.name = "Simón" var name = obj.name;
Y ...
obj["name"] = "Simon"; var name = obj["name"];
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"; // Syntax error, because 'for' is a reserved word obj["for"] = "Simon"; // works fine
La sintaxis literal de objeto se puede utilizar para inicializar un objeto en su totalidad:
var obj = { name: "Carrot", "for": "Max", details: { color: "orange", size: 12 } }
El acceso de atributos se puede encadenar:
> obj.details.color orange > obj["details"]["size"] 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 ". Es siempre uno más que el índice de la matriz.
Antes se creaban matrices de la siguiente manera:
> var a = new Array(); > a[0] = "dog"; > a[1] = "cat"; > a[2] = "hen"; > a.length 3
Una notación más cómoda es utilizar una declaración literal de matriz:
> var a = ["dog", "cat", "hen"]; > 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 = ["dog", "cat", "hen"]; > a[100] = "fox"; > 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 la expresión simplificada i, j en su lugar.
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 de que vas a asignar a una posición vacía 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]..)
concatdevuelve una nueva matriz con los elementos agregados a ellapopelimina y devuelve el último elementopushagrega uno o más elementos al final (como nuestra expresión simplificada (o "azúcar sintáctica")ar[length])slicedevuelve un submatrizsorttiene una función de comparación opcionalsplicepermite modificar una matriz suprimiendo una parte de la misma y reemplazándola con elementosunshiftantepone 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 add(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 a sus propias variables cuáles son locales a esa función. La instrucción return se puede utilizar para devolver un valor en cualquier momento, finalizando la función. Si no se utiliza ninguna instrucción "return" (ni una "return" vacía 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 que pase los parámetros que espera, en cuyo caso se establecerán en undefined .
> add() NaN / / No se puede realizar adición en undefined
También puedes aprobar más argumentos que la función espera:
> add(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 (argumentos) , que es un objeto similar a una matriz que contiene todos los valores pasados a la función. Vamos a reescribir la función de sumar ("add") para que tome tantos valores como queramos:
function add() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum; } > add(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 avg() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum / arguments.length; } > avg(2, 3, 4, 5) 3.5
Esto es bastante útil, pero introduce un nuevo problema. La función avg() 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 avgArray(arr) { var sum = 0; for (var i = 0, j = arr.length; i < j; i++) { sum += arr[i]; } return sum / arr.length; } > avgArray([2, 3, 4, 5]) 3.5
Pero sería bueno poder volver a utilizar la función que ya hemos 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.
> avg.apply(null, [2, 3, 4, 5]) 3.5
El segundo argumento para apply() (aplicar) 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.
JavaScript permite crear funciones anónimas.
var avg = function() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum / arguments.length; }
Este es semánticamente equivalente al formulario function avg(). Es muy potente, ya que te permite poner una definición de la función completa en cualquier lugar que normalmente pondría una expresión y da pie a todo tipo de trucos ingeniosos. Esta es una forma de "ocultar" algunas de las variables locales, como ámbito de bloque en C:
> var a = 1; > var b = 2; > (function() { var b = 3; a += b; })(); > a 4 > b 2
JavaScript te permite llamar a funciones recurrentemente. 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 los llamas recurrentemente si carecen de nombre? La respuesta recae en el objeto . El uso de arguments, que además de actuar como una lista de argumentos también proporciona una propiedad denominada arguments.calleearguments.callee es depreciado e incluso rechazado en modo estricto. En su lugar, debes utilizar "funciones anónimas con nombre" de la siguiente manera:
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
En la programación orientada a objetos clásisica, los objetos son recopilaciones de datos y métodos que operan sobre dichos datos. JavaScript es un lenguaje basado en prototipos que no contiene ninguna instrucción de clase,como se encuentra, por ejemplo, en C + + o Java. (Esto es a veces confuso para los programadores acostumbrados a los lenguajes con una instrucción de clase.) En su lugar, JavaScript utiliza funciones como clases. Vamos a considerar un objeto persona con campos de nombre y apellido. Hay dos formas en que se podría mostrar el nombre: como "nombre apellido" ("first last") o como "apellido, nombre" ("last, first"). Utilizando las funciones y los objetos que hemos discutido anteriormente, esta es una forma de hacerlo:
function makePerson(first, last) { return { first: first, last: last } } function personFullName(person) { return person.first + ' ' + person.last; } function personFullNameReversed(person) { return person.last + ', ' + person.first } > s = makePerson("Simon", "Willison"); > personFullName(s) Simon Willison > personFullNameReversed(s) Willison, Simon
Esto funciona, pero es bastante feo. Acabas con decenas de funciones en el espacio de nombres global. Lo que realmente necesitamos es una manera de asociar una función a un objeto. Dado que las funciones son objetos, es fácil:
function makePerson(first, last) { return { first: first, last: last, fullName: function() { return this.first + ' ' + this.last; }, fullNameReversed: function() { return this.last + ', ' + this.first; } } } > s = makePerson("Simon", "Willison") > s.fullName() Simon Willison > s.fullNameReversed() Willison, Simon
Hay algo aquí que no hemos visto antes: la palabra clave ' this '. ' this ' se utiliza dentro de una función y se refiere al objeto actual. Lo que significa realmente es especificado por la forma en que llamaste a esa función. Si la llamaste con 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 = makePerson("Simon", "Willison") > var fullName = s.fullName; > fullName() undefined undefined
Cuando llamamos a fullName() , ' this 'está ligado al objeto global. Dado que no hay ninguna variable global llamada first o last tenemos undefined para cada uno.
Podemos aprovechar la palabra clave ' this ' para mejorar nuestra función makePerson:
function Person(first, last) { this.first = first; this.last = last; this.fullName = function() { return this.first + ' ' + this.last; } this.fullNameReversed = function() { return this.last + ', ' + this.first; } } var s = new Person("Simon", "Willison");
Hemos introducido otra palabra clave: 'new'. new está relacionada estrechamente con ' this '. Lo que hace es que crea un objeto vacío completamente nuevo y luego llama a la función especificada, con 'this' establecido a ese nuevo objeto. Las funciones que están diseñadas para ser llamadas por 'new' se llaman funciones constructor. Lo que se suele hacer habitualmente es aprovechar estas funciones como un recordatorio para llamarlas con new .
Nuestros objetos persona están mejorando, pero todavía hay algunos bordes feos en ellos. 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 personFullName() { return this.first + ' ' + this.last; } function personFullNameReversed() { return this.last + ', ' + this.first; } function Person(first, last) { this.first = first; this.last = last; this.fullName = personFullName; this.fullNameReversed = personFullNameReversed; }
Eso está mejor: estamos creando las funciones método sólo una vez y les estamos asignando referencias dentro del constructor. ¿Podemos hacerlo mejor? La respuesta es que sí.
function Person(first, last) { this.first = first; this.last = last; } Person.prototype.fullName = function() { return this.first + ' ' + this.last; } Person.prototype.fullNameReversed = function() { return this.last + ', ' + this.first; }
Person.prototype es un objeto compartido por todos los ejemplos de Person . Forma parte de una cadena de búsqueda (que tiene un nombre especial ," cadena prototipo"): cada vez que se intenta acceder a una propiedad de Person que no está establecida, JavaScript comprobará Person.prototype para ver si esa propiedad existe. Como resultado, cualquier cosa asignada a Person.prototype está disponible para todos los ejemplos de ese 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 Person("Simon", "Willison"); > s.firstNameCaps(); TypeError on line 1: s.firstNameCaps is not a function > Person.prototype.firstNameCaps = function() { return this.first.toUpperCase() } > s.firstNameCaps() 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 Person("Simon", "Willison"); > s [Object Object] > Person.prototype.toString = function() { return '<Person: ' + this.fullName() + '>'; } > s <Person: Simon Willison>
¿Te acuerdas de cómo avg.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 de nuevo te permite configurar ' this ', pero tiene una lista de argumentos ampliada en lugar de una matriz.
function lastNameCaps() { return this.last.toUpperCase(); } var s = new Person("Simon", "Willison"); lastNameCaps.call(s); / / Es lo mismo que: s.lastNameCaps = lastNameCaps; s.lastNameCaps();
Funciones internas
Están permitidas las declaraciones de función de JavaScript dentro de otras funciones. Hemos visto esto ya antes, con una función anterior makePerson(). Un detalle importante de funciones anidadas en JavaScript es que pueden acceder a las variables en el ámbito de su función primaria:
function betterExampleNeeded() { var a = 1; function oneMoreThanA() { return a + 1; } return oneMoreThanA(); }
Esto proporciona una gran utilidad en la escritura de código más fácil de mantener. Si una función se basa en una o dos funciones que no resultan útiles a otra parte de tu código, puedes anidar dichas funciones de utilidad dentro de la función que sea 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.
Cierres
Esto nos lleva a una de las abstracciones con más posibilidades que JavaScript tiene que 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 ).
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 .