El Jardín de JavaScript es una guía de documentación acerca de las
partes más peculiares de este lenguaje de programación. Brinda consejos para evitar
los errores más comunes y sutiles, así como problemas de rendimiento y de malas
prácticas que los programadores menos experimentados en JavaScript pueden resolver
en sus esfuerzos por profundizar en el lenguaje.
El Jardín de JavaScript no prentende enseñar JavaScript.
Se recomienda un conocimiento sobre el lenguaje para entender los temas tratados en
esta guía. Con el fin de aprender los conceptos básicos del lenguaje, por favor
diríjase a la excelente guía de los desarrolladores de Mozilla.
Los autores
Esta guía es el trabajo de dos encantadores usuarios del foro Stack Overflow,
Ivo Wetzel (Escrito) y Zhang Yi Jiang (Diseño).
El Jardín de JavaScript es publicado bajo la licencia MIT y es hospedado en
GitHub. Si encuentra algún error o errata por favor publique una incidencia o
envie un pull request a nuestro repositorio. También nos puede encontrar en la
sala de chat de JavaScript en Stack Overflow.
Objetos
Uso de objetos y propiedades
Todo en JavaScript actúa como un objeto, con las dos únicas excepciones de
null y undefined.
Un error muy común es el uso de literales númericos como objetos.
Esto se debe a un error en el parser de JavaScript que intenta analizar la
notación de puntos como un literal de punto flotante.
2.toString(); // lanza SyntaxError
Existe un par de soluciones que pueden utilizarse para hacer que los
literales númericos actúen como objetos.
2..toString(); // el segundo punto es reconocido correctamente
2 .toString(); // observe el espacio a la izquierda del punto
(2).toString(); // el número 2 se evalúa primero
Objetos como un tipo de datos
Los objetos en JavaScript también pueden ser utilizados como una Tabla Hash o conocido como Hashmap en inglés, consisten
principalmente en nombres de propiedades, y asignándoles valores a éstas.
El uso de un objeto literal - con notación {} - puede crear un
objeto plano. Este nuevo objeto heredado desde Object.prototype
no posee propiedades propias definidas.
var foo = {}; // un nuevo objeto vacío
// un nuevo objeto con la propiedad llamada 'test' con el valor 12
var bar = {test: 12};
Acceso a las propiedades
Se puede acceder a las propiedades de un objeto de dos maneras, ya sea a través de la
notación de punto o desde la notación de corchetes.
var foo = {name: 'kitten'}
foo.name; // kitten
foo['name']; // kitten
var get = 'name';
foo[get]; // kitten
foo.1234; // SyntaxError
foo['1234']; // ¡funciona!
Ambas notaciones son idénticas en su funcionamiento, la única diferencia es la
notación de corchetes permite el ajuste dinámico de las propiedades, así como
el uso de propiedades que de otro modo daría lugar a error de sintaxis.
Eliminando propiedades
La única manera de eliminar una propiedad desde un objeto es usando el
operador delete; establecer la propiedad a undefined o null solamente
elimina el valor asociado a la propiedad, pero no la key (valor clave).
var obj = {
bar: 1,
foo: 2,
baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;
for(var i in obj) {
if (obj.hasOwnProperty(i)) {
console.log(i, '' + obj[i]);
}
}
Los resultados de la salida son bar undefined y foo null - sólo baz ha
sido removido y por lo tanto no aparece en la salida.
Notación de Keys
var test = {
'case': 'Soy una palabra clave y debo ser anotado como string',
delete: 'Soy una palabra clave también' // lanza SyntaxError
};
Las propiedades de los objetos puede ser simbolizados como caracteres planos y como strings. Debido
a otro mal diseño del parser de JavaScript, lo anterior es una excepción
de SyntaxError antes de ECMAScript 5.
Este error se produce porque delete es una keyword; por lo tanto, debe ser
anotado como un string literal para asegurarse que será interpretado correctamente
por diversos motores de JavaScript.
Prototipo
JavaScript no posee en sus características un sistema clásico de herencia, sino que
utiliza un prototipo para esto.
Si bien a menudo se considera uno de los puntos débiles de JavaScript, el
modelo de herencia prototipado es de hecho más poderoso que el modelo clásico.
Por ejemplo, es bastante trivial construir un modelo clásico a partir del modelo prototipado,
mientras que al contrario es una tarea mucho más difícil.
Debido al hecho que JavaScript es básicamente el único lenguaje que utiliza
ampliamente la herencia prototipada, se necesita algo de tiempo para adaptarse a
las diferencias entre los dos modelos.
La primera gran diferencia es que la herencia en JavaScript se realiza usando
llamadas de cadenas de prototipo (prototype chains).
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
// Asigna el prototipo de Bar como una nueva instancia de Foo
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';
// Asegura que el constructor sea Bar
Bar.prototype.constructor = Bar;
var test = new Bar() // crea una nueva instancia de Bar
// Resultado de cadena de prototipos (prototype chain)
test [instance of Bar]
Bar.prototype [instance of Foo]
{ foo: 'Hello World', value: 42 }
Foo.prototype
{ method: ... }
Object.prototype
{ toString: ... /* etc. */ }
En el código anterior, el objeto test hereda de Bar.prototype y Foo.prototype;
por lo tanto, tendrá acceso a la función method que se ha definido en Foo.
También se tendrá acceso a a la propiedad value de la única instancia de Foo
que compone su prototipo. Es importante tomar en cuenta que new Bar()no creará una nueva
instancia de Foo, pero retornará lo asignado en su prototipo; de este modo, todas las instancias
de Bar tendrán que compartir el mismovalor de la propiedad.
Búsqueda de propiedades
Cuando se accede a las propiedades de un objeto, JavaScript recorre la cadena de
prototipo hacia arriba hasta encontrar la propiedad con el nombre solicitado.
Cuando se llega al final de la cadena - concretamente Object.prototype - y aún
no se ha encontrado la propiedad especificada, se retornará un valor
undefined en su lugar.
La propiedad prototype
Aunque la propiedad prototype es usada por el lenguaje para construir la cadena
de prototipos, es posible asignar cualquier valor. Aunque los tipos primitivos
serán ignorados cuando se asigne en prototype.
function Foo() {}
Foo.prototype = 1; // no tendrá efecto
La asignación de objetos, como se muestra en el ejemplo anterior, funcionará, y permitirá
la creación dinámica de cadena de prototipos.
Rendimiento
El tiempo tomado en la búsqueda de propiedades es alta y la cadena de prototipo puede
presentar un impacto negativo crítico en el rendimiento en partes del código. Además,
si ha tratado de acceder a propiedades que no existen, esto provoca que se recorra la cadena de prototipo completa.
Además, al recorrer en iteración las propiedades de un objeto
, cada propiedad encontrada en la cadena de prototipo será enumerada.
Extensión de prototipos nativos
Una mala característica que se suele utilizar para extender Object.prototype o cualquier
otro prototipo construido.
Esta técnica es conocida en inglés como monkey patching y rompe la encapsulación del código.
Si bien es utilizado en frameworks como Prototype, todavía no existen buenas razones para adoptarlo o integrarlo
como tipos de dato o como funcionalidad no estándar.
La única razón coherente para extender un prototipo es para adaptarle nuevas
características de los motores JavaScript más modernos; por ejemplo,
Array.forEach.
En conclusión
Se debe entender por completo el módelo de herencia prototipado antes de
escribir código complejo que lo utilice. Además, observe la longitud de la
cadena de prototipo y modifíquela si es necesario para evitar posibles problemas de
rendimiento. Con relación a los prototipos nativos, estos nunca deben ser extendidos a
menos que sea para mantener la compatibilidad con nuevas características de JavaScript.
hasOwnProperty
Con el fin de comprobar si un objeto posee una propiedad definida en sí mismo y no
en algún lugar de su cadena de prototipo, es necesario utilizar
el método hasOwnProperty ya que todos los objetos herendan de Object.prototype.
hasOwnProperty es la única utilidad en JavaScript que se ocupa de las propiedades
y no las salta en la cadena de prototipo.
// Envenenamiento en Object.prototype
Object.prototype.bar = 1;
var foo = {goo: undefined};
foo.bar; // 1
'bar' in foo; // true
foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true
Sólo hasOwnProperty retornará el resultado correcto y esperado, esto es
ensencial cuando se repite una iteración en las propiedades de cualquier objeto. No hay
otra maner de excluir las propiedades que no están definidas en el mismo objeto, pero
en alguna parte de su cadena de prototipo si.
hasOwnProperty como propiedad
JavaScript no protege el nombre de la propiedad hasOwnProperty; de este modo, si existe
la posibilidad de que un objeto tenga una propiedad con el mismo nombre, es necesario utilizar
hasOwnProperty como propiedad externa con el fin de obtener resultados correctos.
var foo = {
hasOwnProperty: function() {
return false;
},
bar: 'Here be dragons'
};
foo.hasOwnProperty('bar'); // siempre devolverá false
// Utilice otro objeto con hasOwnProperty y llamelo con 'this' para asignarlo a foo
({}).hasOwnProperty.call(foo, 'bar'); // true
En conclusión
Cuando se necesite comprobar la existencia de una propiedad en un objeto, hasOwnProperty es
el único método para hacerlo. También se recomienda el uso de hasOwnProperty como
parte de un bucle for in, esto evitará errores desde
extenciones de prototipos nativos.
El bucle for in
Al igual que el operador in, el bucle for in también recorre sobre la
cadena de prototipo cuando este se repite en una iteración en las propiedades de un objeto.
// Envenenamiento en Object.prototype
Object.prototype.bar = 1;
var foo = {moo: 2};
for(var i in foo) {
console.log(i); // Imprime ambos bar y moo
}
Dado que no es posible cambiar el comportamiento del bucle for in en sí mismo, es
necesario filtrar las propiedades internas no deseadas dentro del bucle,
esto se hace mediante el uso del método hasOwnProperty del
Object.prototype.
Usando hasOwnProperty para filtrado
// Aún es el foo del código de arriba
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
Está versión es la única forma correcta de uso. Esto se debe sólo al uso de
hasOwnProperty que imprimirá moo. Cuando hasOwnProperty se omita, el código es
propenso a errores en los casos de prototipos nativos - ej. Object.prototype -
se ha extendedido.
Uno de los frameworks más usado que implementa estas funcionalidades es Prototype. Cuando el
framework es incluido, el bucle for in que no utilicen hasOwnProperty no podrá garantizar que
se interrumpa.
En conclusión
Se recomienda utilizar siempre el uso de hasOwnProperty. Nunca debe suponer ningún entorno donde el código se ejecute, o si los prototipos
nativos han sido extendidos o no.
Funciones
La declaración de funciones y expresiones
Las funciones en JavaScript son funciones de primera clase (first class functions).
Esto significa que se pueden tratar como objetos. Un uso común de esta característica es pasar de
una función anónima a otra, posiblemente una función asíncrona. Esto se conoce como callback.
La declaración function
function foo() {}
La función anterior se carga así mismo antes de iniciar la ejecución del
programa; por lo tanto, está disponible en todo el scope (ámbito) de la aplicación
donde se ha definido, aunque hubiera sido llamado antes de definirse en el código.
foo(); // Funciona porque foo ha sido creado antes que este código se ejecute
function foo() {}
La expresión function
var foo = function() {};
Este ejemplo asigna una función sin nombre y anónima a la variable foo.
Debido a la declaración de var, que carga el nombre de la variable foo antes
de la ejecución real del inicio del código, foo ya estará definidido cuando se
ejecute el script.
Pero se asigna sólo si ocurre en tiempo de ejecución, el valor de foo de forma
predetermina es undefined antes de que el código se ejecute.
Expresión nombre de función
Otro caso especial de asignación de nombre de funciones.
var foo = function bar() {
bar(); // Funciona
}
bar(); // ReferenceError
Aquí bar no está disponible en el ámbito externo (scope), ya que la función sólo es
asignada a foo; Sin embargo, dentro de bar si está disponible. Esto se debe a la forma
en como trabaja la resolución de nombres en JavaScript, el nombre de
la función esta siempre disponible en el ámbito local de la propia función.
Cómo trabaja this
JavaScript tiene un concepto diferente sobre el nombre especial this referido a la
mayoría de lenguajes de programación. Hay exactamente cinco formas distintas en donde
es posible ver el valor de this dentro de lo posible en el lenguaje.
El ámbito global (Global Scope)
this;
Cuando se utiliza this en el ámbito global, simplemente se refiere al objeto global.
Llamar a una función
foo();
Aquí this se refiere al objeto global.
Llamar a un método
test.foo();
En este ejemplo this se referiere a test.
Llamar a un constructor
new foo();
Llamar a una función que esta precedida por la palabra clave new actúa como
un constructor. Dentro de la función, this se refiere
al Objetorecién creado.
Ajuste explícito de this
function foo(a, b, c) {}
var bar = {};
foo.apply(bar, [1, 2, 3]); // array que se apilará
foo.call(bar, 1, 2, 3); // resultados a = 1, b = 2, c = 3
Cuando se utiliza los métodos call o apply en Function.prototype, el valor de
this dentro de la función llamada se ajustará explícitamente al primer argumento
correspondiente a la llamada de la función.
Como resultado, el ejemplo anterior sobre los casos de métodos estos no se aplican, y this
dentro de foo puede establecerse en bar.
Errores comunes
Si bien en la mayoría de los casos esto tiene sentido, el primero puede cosiderarse como otro
mal diseño del lenguaje, ya que nunca tiene un uso práctico.
Foo.method = function() {
function test() {
// this es establecido como un objeto global
}
test();
};
Un error común es que this dentro de test haga referencia a Foo, mientras que en
realidad esto no es así.
Con el fin de acceder a Foo desde dentro de test es necesario crear una variable local
dentro del método para referirse a Foo.
Foo.method = function() {
var that = this;
function test() {
// Use that instead of this here
}
test();
};
that es justo un nombre normal, pero es comúnmente usado para referenciar a this
de forma externa. En combinación con closures, esto puede ser
también usado para pasar this como valor.
Asignación de métodos
Otra cosa que no funciona en JavaScript son los alias en las funciones, es decir,
asignar un método a una variable.
var test = someObject.methodTest;
test();
Debido al primer caso, test actúa como una función de llamada; por lo que
this dentro de este no estará referido a someObject.
Mientras que la unión de this puede parecer una mala idea en un principio, esto es en
realidad lo que hace trabajar a la herencia de prototipo.
function Foo() {}
Foo.prototype.method = function() {};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();
Cuando los métodos son llamados desde una instancia de Bar, this se referirá a una
instancia.
Closures y referencias
Una de las características más poderosas de JavaScript es la disponibilidad de closures (cerraduras),
esto significa que los ámbitos siempre podrán ser accedidos por ámbitos externos donde
fueron definidos. Dado que sólo el alcance es único en JavaScript en el
ámbito de la función, todas las funciones, por omisión, actúan como closures.
Emulando variables privadas
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
En este caso, Counter retorna dos closures. La función increment y la
función get. Ambas funciones mantienen el ámbito de la referencia de Counter y, por lo tanto, siempre accede a la variable count que fue definido
en el ámbito.
¿Por qué las variables privadas trabajan?
Dado que no es posible referenciar o asignar ámbitos en JavaScript, no hay
manera de acceder a la variable count desde fuera. Sólo existe una forma para
interactuar con estos vía los dos closures.
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
El código anterior no ha cambiado la variable count en el ámbito de Counter,
desde foo.hack no es definido en ese ámbito. En su lugar se creará - o
se anulará - la variable globalcount.
Closures dentro de bucles
Un error frecuente en el uso de closures dentro de bucles, es como si se tratará
de copiar el valor del índice de la variable del bucle.
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
El código anterior no tendrá como salida los números del 0 al 9, sino
simplementemente se imprimirá el número 10 diez veces.
La función anónima hace referencia a i y se llama a
console.log, el bucle for ya ha terminado y finalizo el valor de
i a 10.
Con el fin de obtener el comportamiento deseado, es necesario crear una copia
del valor de i.
Evitando el problema de referencia
Con el fin de copiar el valor de la variable índice del bucle, lo mejor es utilizar
un contenedor anónimo.
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
La función anónima externa llamará inmediatamente a i como su primer
argumento y recibirá la copia del valor de i como parámetro de e.
La función anónima que se pasa a setTimeout ahora es una referencia a
e, cuyo valor no han sido cambiados por el bucle.
No hay otra manera de lograr esto; se debe retornar una función desde
el contenedor anónimo, que tendrá el mismo comportamiento que el código
anterior.
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
El objeto arguments
Cada ámbito de la función de JavaScript puede acceder a la variable especial arguments.
Está variable contiene una lista de todos los argumentos que se pasan a la función.
El objeto argumentsno es un Array. Si bien cuenta con la semántica
de un array - concretamente la propiedad length - no hereda de
Array.prototype y es de hecho un Objeto.
Debido a esto, no es posible usar los métodos estándar de los arrays como push,
pop o slice en arguments. Mientras que la iteración es un simple bucle for que
funciona muy bien, esto se convierte necesariamente en un Array real con el
fin de utilizar los métodos de un Array.
Conversión de un Array
El siguiente código devuelve un nuevo Array que contiene todos los elementos del
objeto arguments.
Array.prototype.slice.call(arguments);
Esta conversión es lenta, no es recomendable usarlo en puntos criticos que
afecten el rendimiento del código.
Pasar Argumentos
El siguiente método es recomendado para pasar argumentos desde una función a
otra.
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// do stuff here
}
Otro truco es utilizar tanto call y apply juntos para crear contenedores rápidos y
consolidados.
function Foo() {}
Foo.prototype.method = function(a, b, c) {
console.log(this, a, b, c);
};
// Crea una versión sin consolidar de "method"
// Se toma los parámetros: this, arg1, arg2...argN
Foo.method = function() {
// Resultado: Foo.prototype.method.call(this, arg1, arg2... argN)
Function.call.apply(Foo.prototype.method, arguments);
};
Los parámetros formales y argumentos de índices
El objeto arguments crea las funciones de getter y setter para sus
propiedades, así como parámetros formales de la función.
Como resultado, se ha cambiado el valor formal del parámetro también se cambio el
valor de la propiedad correspondiente del objeto arguments, y al revés.
function foo(a, b, c) {
arguments[0] = 2;
a; // 2
b = 4;
arguments[1]; // 4
var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);
Mitos y verdades sobre el rendimiento
El objeto arguments es siempre creado con las dos únicas excepciones cuando es
el caso en que declarado como un nombre dentro de la función o uno de los
parámetros formales. No importa si se utiliza o no.
Ambos getters y setters son siempre creados; por lo tanto, con que casi no se
tiene un impacto en el rendimiento en todo, especialemente no en el código real donde no
es más que un simple acceso a las propiedades del objeto arguments.
Sin embargo, hay casos en que se reducirá drásticamente el rendimiento en los motores
modernos de JavaScript. Este es el caso del uso de arguments.callee.
function foo() {
arguments.callee; // realiza algo con la función del objeto
arguments.callee.caller; // y llama a la función del objeto
}
function bigLoop() {
for(var i = 0; i < 100000; i++) {
foo(); // Debería ser normalmente entre líneas...
}
}
El código anterior, foo no puede estar sujeto a la expansión en línea ya que se
necesita saber acerca de sí mismo y la llamada. Esto no sólo denota los posibles beneficios
de rendimiento que surgen con la expansión en línea, ya que también interrumpe la encapsulación
ya que la función ahora puede ser dependiente de un contexto específico de llamada.
Es muy recomendablenunca hacer uso de arguments.callee o de cualquier
de sus propiedades.
Constructores
Los constructores en JavaScript todavía son diferentes a los de otros lenguajes.
Cualquier llamada que es precedida por la palabra new actua como un constructor.
Dentro del constructor - la función llama - el valor de this se refiere a un
Objeto recién creado. El prototipo de este nuevo
objeto se establece en el prototipo de la funcióno que es invocado como el
constructor.
Si la función que se llama no tiene una sentencia return explícita, entonces
implícitamente devuelve el valor de this - el nuevo objeto.
function Foo() {
this.bla = 1;
}
Foo.prototype.test = function() {
console.log(this.bla);
};
var test = new Foo();
La llamada de Foo por encima del constructor y establece el prototipo del objeto
recién creado a Foo.prototype.
En caso explícito de la sentencia return de la función devuelva el valor especificado
que la declaración, pero sólo si el valor devuelto es un Object.
function Bar() {
return 2;
}
new Bar(); // a new object
function Test() {
this.value = 2;
return {
foo: 1
};
}
new Test(); // the returned object
Cuando una nueva keyword es omitidad, la función no devuelve un nuevo objeto.
function Foo() {
this.bla = 1; // se establece en el objeto global
}
Foo(); // undefined
Auqnue el ejemplo anterior puede parecer que trabaja en algunos casos, debido
a los trabajos de this en JavaScript, que usará el
objeto global como valor de this.
Fábricas
Con el fin de ser capaz de omitir un nuevo keyword, la función del tiene
explícitamente devolver un valor.
function Bar() {
var value = 1;
return {
method: function() {
return value;
}
}
}
Bar.prototype = {
foo: function() {}
};
new Bar();
Bar();
Ambos llamadas a Bar devuelven exactamente lo mismo, un reciente objeto creado que
tiene como propiedad llamada el method, esto es un
Closure.
También hay que notar que la llamada new Bar()no afecta al prototipo
del objeto devuelto. Mientras que el prototipo se establece en el objeto recién creado,
Bar nunca devuelve un nuevo objeto.
En el ejemplo anterior, no hay diferencia funcional entre usar y no usar
el keyword new.
Creación de nuevos objetos vía Factorias
Una recomendación a menudo es no utilizar new ya que su uso puede
conducir a errores.
Con el fin de crear un nuevo objeto, uno bien debe utilizar una fábrica y un
constructor para crear un nuevo objeto dentro de la fábrica.
Aunque lo anterior es robuesto frente a la keyword new y, ciertamente hace
que el uso de variables privadas sea fácil, esto viene con
algunas desventajas.
Se utiliza más memoria, ya que los objetos creados no comparten los métodos de
un prototipo.
Con el fin de heredar de una fábrica se necesita copiar todos los métodos a otro
objeto o poner todo en un prototipo de nuevo objeto.
La eliminación de una cadena de prototipo sólo por dejar la keyword new de
alguna manera va en contra del espíritu del lenguaje.
En conclusión
Mientras que se omite el keyword new podría dar a errores, no es ciertamente
una razón para abandonar el uso de prototipos por completo. Al final todo se reduce a
la solución que se adapta mejor a las necesidades de la aplicación, especialmente si es
importante elegir un estilo específico en la creación de objetos
y resistirse.
Ámbitos y Namespaces
A pesar que JavaScript tiene una muy buena sintaxis de dos llaves para los bloques,
está no es compatible con el soporte de ámbito de bloques; por lo que todo se deja
al lenguaje con el ámbito de la función.
function test() { // un ámbito
for(var i = 0; i < 10; i++) { // no es un ámbito
// cuenta
}
console.log(i); // 10
}
Tampoco hay distintos namespaces en JavaScript, lo que significa que todo se define
en un namespace global y compartido.
Cada vez que una variable es referenciada, JavaScript recorre hacia arriba a través de todos
los ámbitos hasta encontrarlo. En este caso que llegue al ámbito global y todavía no ha
encontrado el nombre solicitado, se generará un error ReferenceError.
El terror de las variables globales
// script A
foo = '42';
// script B
var foo = '42'
Estos dos scripts no tienen el mismo efecto. El script A define una variable
llamada foo en el ámbito global y el script B define foo en el
actual ámbito.
Una vez más, esto no tiene el mismo efecto para todo, no usar var puede tener
mayor implicación.
// ámbito global
var foo = 42;
function test() {
// ámbito local
foo = 21;
}
test();
foo; // 21
Dejando de lador la sentencia var dentro de la función test sobre escribiría el
valor de foo. Si bien al principio puede parecer un gran cambio, se tiene
miles de líneas de código en JavaScript y no se usaría var introduciendose en un
horrible y difícil detección de errores.
// ámbito global
var items = [/* some list */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// ámbito de subLoop
for(i = 0; i < 10; i++) { // falta la sentencia var
// ¡realizar cosas asombrosas!
}
}
El bucle externo terminará después de la primera llamada a subLoop, desde subLoop
sobreescribe el valor global de i. Usando var para el segundo bucle for se hace
fácil evitar este error. La sentencia var no debe nunca dejarse a menos que
el efecto deseado es afectado por el ámbito exteriror.
Variables locales
La única fuente para las variables locales en JavaScript son los parámetros de la
función y variables que fueron declaradas vía la sentencia
var.
// ámbito global
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// ámbito local de la función test
i = 5;
var foo = 3;
bar = 4;
}
test(10);
Mientras foo y i son variables locales dentro del ámbitor de la función test,
ela asignación de bar sobreescribe la variable global con el mismo nombre.
Hoisting
La declaración de hoists en JavaScript. Esto significa que tanto la declaración de var y
la función declarada se translada a la parte superior de su ámbito que lo contiene.
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
El código anterior transforma antes de ejecutarse. JavaScript mueve
la declaracione var aspi como las declaraciones de la función a la parte superior a
lo más cercano del ámbito circundante.
// declaraciones var movidas aquí
var bar, someValue; // por omisión 'undefined'
// la función declarada es movida aquí también
function test(data) {
var goo, i, e; // se pierde el ámbito del bloque movido aquí
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // falla con TypeError desde bar sigue en 'undefined'
someValue = 42; // las asignaciones no se ven afectadas por hoisting
bar = function() {};
test();
La falta de alcance del bloque no sólo moverá la declaración var fuera de los bucles y
su contenido, sino también hará que los resultados de ciertos constructores if
no sean intuitivas.
En el código original la declaración de if si parecía modificar la variable
globalgoo, mientras actualmente este modifica la variable local - después hoisting
ha sido aplicado.
Sin el conocimiento acerca de hoisting, a continuación el código puede parecer
un ReferenceError.
// comprueba si SomeImportantThing ha iniciado
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
Pero, por supuesto, lo anterior funciona debido a que la declaración var es movida
a la parte superior del ámbito global.
var SomeImportantThing;
// otro código podría iniciar SomeImportantThing aqui, o no
// asegúrese de que está ahí
if (!SomeImportantThing) {
SomeImportantThing = {};
}
Name Resolution Order
All scopes in JavaScript, including the global scope, have the special name
this, defined in them, which refers to the current object.
Function scopes also have the name arguments, defined in
them, which contains the arguments that were passed to a function.
For example, when trying to access a variable named foo inside the scope of a
function, JavaScript will lookup the name in the following order:
In case there is a var foo statement in the current scope, use that.
If one of the function parameters is named foo, use that.
If the function itself is called foo, use that.
Go to the next outer scope, and start with #1 again.
Namespaces
A common problem of having only one global namespace is the likeliness of running
into problems where variable names clash. In JavaScript, this problem can
easily be avoided with the help of anonymous wrappers.
(function() {
// a self contained "namespace"
window.foo = function() {
// an exposed closure
};
})(); // execute the function immediately
Unnamed functions are considered expressions; so in order to
being callable, they must first be evaluated.
( // evaluate the function inside the paranthesis
function() {}
) // and return the function object
() // call the result of the evaluation
There are other ways for evaluating and calling the function expression; which,
while different in syntax, do behave the exact same way.
// Two other ways
+function(){}();
(function(){}());
In Conclusion
It is recommended to always use an anonymous wrapper for encapsulating code in
its own namespace. This does not only protect code against name clashes, but it
also allows for better modularization of programs.
Additionally, the use of global variables is considered bad practice. Any
use of them indicates badly written code that is prone to errors and hard to maintain.
Arrays
Iteración de un Array y sus propiedades
A pesar que los arrays en JavaScript son objetos, no existe un buena razón para
usarlo en un bucle for para una interación de este. De
hecho, hay un número de buenas razones contra el uso de for in en arrays.
Dado que el bucle for in enumera todas las propiedades que están en una cadena
de prototipo y la única manera para excluir estas propiedades es el uso de
hasOwnProperty, ya que es veinte veces más
lento que un bucle for normal.
Iteración
Con el fin de obtener el mejor rendimiento cuando se repite la interación de arrays,
es lo mejor hacer uso del clásico bucle for.
var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
console.log(list[i]);
}
Hay una captura adicional en el ejemplo anterior, que es el almacenamiento de la
caché de longitud del array vía l = list.length.
Aunque la propiedad length es definida en el mismo array, todavía posee una sobrecarga
para realizar la búsqueda en cada interación del bucle. Y mientras que los últimos
motores de JavaScript pueden aplicar optimizaciones en este caso, no hay manera
de saber si el ćodigo se ejecutará en uno de estos nuevos motores nuevos o no.
De hecho, dejando de lado el almacenamiento en caché puede resultar que el bucle
inicie sólo la mitad de rápido que con la longitud de la caché.
La propiedad length
Mientras que getter de la propiedad length simplemente retorne el número de
elementos son contenidos en un array, el setter puede ser usado para
truncar el array.
La asignación de un menor número de longitud trunca al array, pero incrementando la
longitud no tiene ningún efecto sobre el array.
En conclusión
Para obtener el mejor rendimiento es recomendable siempre usar el bucle for
y alamacenar en caché la propiedad length. El uso del bucle for in en un array
es señal de un código mal escrito propenso a errores y un mal desempeño.
El constructor Array
Desde el constructor Array es ambiguo en la forma en que ocupa sus párametros,
es recomendable siempre el uso de arrays literales - la notación [] -
cuando se crean nuevos arrays.
En casos cuando sólo hay un argumento pasado al constructor del Array,
y que el argumento es un Número, el contructor devolverá un array disperso
con la propiedad length establecida al valor del argumento. Esto debe señalarse
que la propiedad lengthsólo del nuevo array se establecerá de esa manera,
los índices reales de la matriz no se iniciará.
var arr = new Array(3);
arr[1]; // undefined
1 in arr; // falso, el índice no se ha establecido
El comportamiento de poder establecer la longitud de un array inicial sólo es útil
en algunos casos array, como la repetición de una cadena, en la que se evita el uso
del código de bucle for.
new Array(count + 1).join(stringToRepeat);
En conclusión
El uso de un constructor Array debe ser devuelto como sea posible.
Los literales son definitivamente preferidos. Estos son más cortos y tienen una
sintaxis más limpia; por lo tanto, también se incrementa la legibilidad del código.
Tipos
Igualdad y Comparación
JavaScript posee 2 maneras diferentes para comparar valores entre objetos para comprobar igualdad.
El Operador de Igualdad
El operador de igualdad consiste en 2 signos es igual: ==
JavaScript utiliza tipado débil. Esto significa que el operador de igualdad
obliga una conversión de tipos para poder compararlos.
La tabla anterior muestra los resultados de la conversión de tipos, éste es el motivo principal
de por qué el uso de == es ampliamente considerado una mala práctica. Introduce errores
difíciles de identificar debido a la complejidad de sus reglas de conversión.
Además, existe un impacto en el rendimiento cuando entra en juego la conversión de tipos;
por ejemplo, una cadena debe ser convertida a número antes de poder ser comparada
con otro número.
El Operador de Igualdad Estricto
El operador de igualdad estricto consiste en tres signos es igual: ===:
Funciona exactamente igual que el operador de igualdad, excepto que el operador de igualdad
estricto no utiliza conversión de tipos entre sus operandos.
Los resultados anteriores son mucho más claros y permiten una detección de errores temprana.
Esto permite un código más sólido en cierto grado y también mejora el rendimiento
en el caso que los operandos sean de tipos diferentes.
Comparando Objetos
Aunque == como === son considerados operadores de igualdad, se comportan
de maneras diferentes cuando al menos uno de sus operandos es Object.
{} === {}; // false
new String('foo') === 'foo'; // false
new Number(10) === 10; // false
var foo = {};
foo === foo; // true
En este caso, los dos operadores comparan por referencia y no por igualdad; esto es,
comparan por la misma instancia del objeto, parecido
al operador is en Python y la comparación entre punteros en C.
En Conclusión
Es altamente recomendable usar sólo el operador de igualdad estricta. En los casos
donde los tipos de datos necesitan ser convertidos, debe hacerse explícitamente
y no dejárselo a las complicadas reglas de conversión del lenguaje.
The typeof Operator
The typeof operator (together with
instanceof) is probably the biggest
design flaw of JavaScript, as it is near of being completely broken.
Although instanceof still has its limited uses, typeof really has only one
practical use case, which does not happen to be checking the type of an
object.
The JavaScript Type Table
Value Class Type
-------------------------------------
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function in Nitro/V8)
new RegExp("meow") RegExp object (function in Nitro/V8)
{} Object object
new Object() Object object
In the above table, Type refers to the value that the typeof operator returns.
As can be clearly seen, this value is anything but consistent.
The Class refers to the value of the internal [[Class]] property of an object.
In order to retrieve the value of [[Class]], one has to make use of the
toString method of Object.prototype.
The Class of an Object
The specification gives exactly one way of accessing the [[Class]] value,
with the use of Object.prototype.toString.
In the above example, Object.prototype.toString gets called with the value of
this being set to the object whose [[Class]] value should be
retrieved.
Testing for Undefined Variables
typeof foo !== 'undefined'
The above will check whether foo was actually declared or not; just
referencing it would result in a ReferenceError. This is the only thing
typeof is actually useful for.
In Conclusion
In order to check the type of an object, it is highly recommended to use
Object.prototype.toString because this is the only reliable way of doing so.
As shown in the above type table, some return values of typeof are not defined
in the specification; thus, they can differ across various implementations.
Unless checking whether a variable is defined, typeof should be avoided at
all costs.
The instanceof Operator
The instanceof operator compares the constructors of its two operands. It is
only useful when comparing custom made objects. Used on built-in types, it is
nearly as useless as the typeof operator.
Comparing Custom Objects
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();
new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true
// This just sets Bar.prototype to the function object Foo
// But not to an actual instance of Foo
Bar.prototype = Foo;
new Bar() instanceof Foo; // false
One important thing to note here is that instanceof does not work on objects
that originate from different JavaScript contexts (e.g. different documents
in a web browser), since their constructors will not be the exact same object.
In Conclusion
The instanceof operator should only be used when dealing with custom made
objects that originate from the same JavaScript context. Just like the
typeof operator, every other use of it should be avoided.
Type Casting
JavaScript is a weakly typed language, so it will apply type coercionwherever possible.
// These are true
new Number(10) == 10; // Number.toString() is converted
// back to a number
10 == '10'; // Strings gets converted to Number
10 == '+10 '; // More string madness
10 == '010'; // And more
isNaN(null) == false; // null converts to 0
// which of course is not NaN
// These are false
10 == 010;
10 == '-10';
In order to avoid the above, use of the strict equal operator
is highly recommended. Although this avoids a lot of common pitfalls, there
are still many further issues that arise from JavaScript's weak typing system.
Constructors of Built-In Types
The constructors of the built in types like Number and String behave
differently when being used with the new keyword and without it.
new Number(10) === 10; // False, Object and Number
Number(10) === 10; // True, Number and Number
new Number(10) + 0 === 10; // True, due to implicit conversion
Using a built-in type like Number as a constructor will create a new Number
object, but leaving out the new keyword will make the Number function behave
like a converter.
In addition, having literals or non-object values in there will result in even
more type coercion.
The best option is to cast to one of the three possible types explicitly.
Casting to a String
'' + 10 === '10'; // true
By prepending an empty string, a value can easily be casted to a string.
Casting to a Number
+'10' === 10; // true
Using the unary plus operator, it is possible to cast to a number.
Casting to a Boolean
By using the not operator twice, a value can be converted a boolean.
La función eval ejecuta un string como código JavaScript en el ámbito local.
var foo = 1;
function test() {
var foo = 2;
eval('foo = 3');
return foo;
}
test(); // 3
foo; // 1
Pero eval sólo ejecutará en ámbito local cuando es llamado directamentey
el nombre de la función llamada es eval.
var foo = 1;
function test() {
var foo = 2;
var bar = eval;
bar('foo = 3');
return foo;
}
test(); // 2
foo; // 3
El uso de eval debe evitarse a toda costa. El 99.9% de su "uso" puede
lograrse sin su uso..
eval disfrazado
Las funciones de tiempo de esperasetTimeout y setInterval pueden
tomar un string como primer argumento. En este caso, el string siempre se ejecutará en
el ámbito global ya que eval no ha sido llamado directamente.
Problemas de seguridad
eval es también un problema de seguridad ya que ejecuta cualquier código enviado,
y nunca debe usarse con strings que no se conozcan o tengan un origen no confiable.
En conclusión
eval nunca debe ser usado, cualquier código que haga uso del mismo debe ser cuestionado
en su funcionamiento, rendimiento y seguridad. En caso de que se necesite trabajar con
eval, el diseño ha de ser cuestionado y no debe utilizarse en primer lugar, se
debe usar un mejor diseño, que no requiera el uso de eval.
undefined y null
JavaScript tiene dos valores distintos para nothing, el más útil de estos dos
es undefined.
El valor undefined
undefined es un tipo de dato con exactamente el mismo valor: undefined.
El lenguaje también define una variable global que tiene el valor de undefined,
Esta variable es también llamada undefined. Sin embargo, esta variable no es una
constante, ni es una palabra reservada del lenguaje. Esto significa que el valor
puede ser sobreescrito fácilmente.
Algunos ejemplos cuando el valor retorna undefined:
Acceso a la variable global (sin modificar) undefined.
Retorna implícitamente las funciones que no posean la sentencia return.
Sentencia return que no retorna nada de forma explicíta.
Búsquedas de propiedades inexistentes.
Párametros de la función que no tienen ningún valor explicíto pasado.
Cualquier valor que se estable en undefined.
Manejar los cambios en el valor deChanges undefined
Dado que la variable undefined sólo tiene una copia del value de undefined, assigna un nuevo valor que no cambie el valor del
tipoundefined.
Aún con el fin de comparar con el valor de undefined es necesario
recuperar el valor de undefined primero.
Con el fin de proteger una posible sobreescritura en la variable undefined,
una técnica común es agregar un párametro adicional a un
wrapper anónimo, que consiga ningún párametro que se le pase.
var undefined = 123;
(function(something, foo, undefined) {
// undefined en el ámbito local
// ahora hace referencia al valor
})('Hello World', 42);
Otra forma de lograrlo un mismo efecto es declarar dentro un
wrapper.
var undefined = 123;
(function(something, foo) {
var undefined;
...
})('Hello World', 42);
La única diferencia, es que está versión es de 4 bytes más que utiliza, y en
caso se comprima y no hay otra declaración de 'var' dentro del
wrapper anónimo.
Uso de null
Mientras que undefined en el contexto del lenguaje JavaScript es muy usado
en el sentido tradicional como null, el actual null (ambos literal y de un tipo)
es más o menos que otro tipo de datos.
Es utilizado en algunos detalles internos de JavaScript (como declarar al final de un
cadena de prototipo estableciendo Foo.prototype = null), pero en casi todos los
casos, puede ser reemplazado por undefined.
Automatic Semicolon Insertion
Although JavaScript has C style syntax, it does not enforce the use of
semicolons in the source code, so it is possible to omit them.
JavaScript is not a semicolon-less language. In fact, it needs the
semicolons in order to understand the source code. Therefore, the JavaScript
parser automatically inserts them whenever it encounters a parse
error due to a missing semicolon.
Chances are very high that log does not return a function; therefore,
the above will yield a TypeError stating that undefined is not a function.
In Conclusion
It is highly recommended to never omit semicolons; it is also advocated to
keep braces on the same line with their corresponding statements and to never omit
them for one single-line if / else statements. Both of these measures will
not only improve the consistency of the code, but they will also prevent the
JavaScript parser from changing its behavior.
Otros
setTimeout and setInterval
Since JavaScript is asynchronous, it is possible to schedule the execution of a
function by using the setTimeout and setInterval functions.
function foo() {}
var id = setTimeout(foo, 1000); // returns a Number > 0
When setTimeout gets called, it will return the ID of the timeout and schedule
foo to run in approximately one thousand milliseconds in the future.
foo will then get executed exactly once.
Depending on the timer resolution of the JavaScript engine that is running the
code, as well as the fact that JavaScript is single threaded and other code that
gets executed might block the thread, it is by no means a safe bet that one
will get the exact delay that was specified in the setTimeout call.
The function that was passed as the first parameter will get called by the
global object, which means that this inside the called function
refers to that very object.
function Foo() {
this.value = 42;
this.method = function() {
// this refers to the global object
console.log(this.value); // will log undefined
};
setTimeout(this.method, 500);
}
new Foo();
Stacking Calls with setInterval
While setTimeout only runs the function once, setInterval - as the name
suggests - will execute the function everyX milliseconds, but its use is
discouraged.
When code that is being executed blocks the timeout call, setInterval will
still issue more calls to the specified function. This can, especially with small
intervals, result in function calls stacking up.
function foo(){
// something that blocks for 1 second
}
setInterval(foo, 100);
In the above code, foo will get called once and will then block for one second.
While foo blocks the code, setInterval will still schedule further calls to
it. Now, when foo has finished, there will already be ten further calls to
it waiting for execution.
Dealing with Possible Blocking Code
The easiest solution, as well as most controllable solution, is to use setTimeout within
the function itself.
function foo(){
// something that blocks for 1 second
setTimeout(foo, 100);
}
foo();
Not only does this encapsulate the setTimeout call, but it also prevents the
stacking of calls and it gives additional control. foo itself can now decide
whether it wants to run again or not.
Manually Clearing Timeouts
Clearing timeouts and intervals works by passing the respective ID to
clearTimeout or clearInterval, depending which set function was used in
the first place.
var id = setTimeout(foo, 1000);
clearTimeout(id);
Clearing all timeouts
Because there is no built-in method for clearing all timeouts and/or intervals,
it is necessary to use brute force in order to achieve this functionality.
// clear "all" timeouts
for(var i = 1; i < 1000; i++) {
clearTimeout(i);
}
There might still be timeouts that are unaffected by this arbitrary number;
therefore, is is instead recommended to keep track of all the timeout IDs, so
they can be cleared specifically.
Hidden use of eval
setTimeout and setInterval can also take a string as their first parameter.
This feature should never be used because it internally makes use of eval.
function foo() {
// will get called
}
function bar() {
function foo() {
// never gets called
}
setTimeout('foo()', 1000);
}
bar();
Since eval is not getting called directly in this case, the string
passed to setTimeout will get executed in the global scope; thus, it will
not use the local variable foo from the scope of bar.
It is further recommended to not use a string for passing arguments to the
function that will get called by either of the timeout functions.
function foo(a, b, c) {}
// NEVER use this
setTimeout('foo(1,2, 3)', 1000)
// Instead use an anonymous function
setTimeout(function() {
foo(1, 2, 3);
}, 1000)
In Conclusion
Never should a string be used as the parameter of setTimeout or
setInterval. It is a clear sign of really bad code, when arguments need
to be supplied to the function that gets called. An anonymous function should
be passed that then takes care of the actual call.
Furthermore, the use of setInterval should be avoided because its scheduler is not
blocked by executing JavaScript.