1. Code
  2. JavaScript

Lo esencial para escribir JavaScript de alta calidad

Scroll to top

Spanish (Español) translation by Steven (you can also view the original English article)

Dos veces al mes, revisamos algunas de las publicaciones favoritas de nuestros lectores a lo largo de la historia de Tuts+. Este tutorial se publicó por primera vez en octubre de 2010.

El brillante Stoyan Stefanov, en la promoción de su libro, "JavaScript Patterns", tuvo la amabilidad de contribuir con un extracto del libro para nuestros lectores, que detalla los elementos esenciales para escribir JavaScript de alta calidad, como evitar los globales, usar declaraciones únicas de variables, longitud de almacenamiento en caché previo en bucles, siguiendo convenciones de codificación y más.

Este extracto también incluye algunos hábitos no necesariamente relacionados con el código en sí, sino más sobre el proceso general de creación de código, incluida la escritura de documentación de API, la realización de revisiones por pares y la ejecución de JSLint. Estos hábitos y prácticas recomendadas pueden ayudarte a escribir un código mejor, más comprensible y fácil de mantener, un código del que enorgullecerse (y ser capaz de descifrar) cuando lo revises meses y años después.


Escribiendo código mantenible

Los errores de software son costosos de corregir. Y su costo aumenta con el tiempo, especialmente si los errores se infiltran en el producto lanzado al público. Es mejor si puedes corregir un error de inmediato, tan pronto como lo encuentres; aquí es cuando el problema que resuelve tu código todavía está fresco en tu cabeza. De lo contrario, pasas a otras tareas y te olvidas por completo de ese código en particular. Revisar el código después de un tiempo requiere:

  • Tiempo para volver a aprender y entender el problema.
  • Tiempo para entender el código que se supone que resuelve el problema.

Otro problema, específico para proyectos o empresas más grandes, es que la persona que finalmente corrige el error no es la misma persona que creó el error (y tampoco la misma persona que encontró el error). Por lo tanto, es fundamental reducir el tiempo que lleva comprender el código, ya sea escrito por ti mismo hace algún tiempo o por otro desarrollador del equipo. Es fundamental tanto para el resultado final (ingresos comerciales) como para la felicidad del desarrollador, porque todos preferimos desarrollar algo nuevo y emocionante en lugar de pasar horas y días manteniendo el código heredado.

Otro hecho de la vida relacionado con el desarrollo de software en general es que normalmente se dedica más tiempo a leer código que a escribirlo. En momentos en los que estás concentrado y profundamente metido en un problema, puedes sentarte y en una tarde crear una cantidad considerable de código.

El código probablemente funcionará en ese momento, pero a medida que la aplicación madura, suceden muchas otras cosas que requieren que tu código sea enviado a revisión, revisado y ajustado. Por ejemplo:

  • Los errores están sin cubrir.
  • Se agregan nuevas características a la aplicación.
  • La aplicación necesita trabajar en nuevos entornos (por ejemplo, los nuevos navegadores que aparecen en el mercado).
  • El código se reutiliza.
  • El código se reescribe completamente desde cero o es migrado a otra arquitectura o incluso a otro lenguaje.

Como resultado de los cambios, las pocas horas de trabajo dedicadas a escribir el código terminan inicialmente en semanas de trabajo dedicadas a leerlo. Es por eso que la creación de código mantenible es fundamental para el éxito de una aplicación.

El código mantenible significa que el código:

  • Es legible
  • Es consistente
  • Es predecible
  • Parece como si estuviera escrito por la misma persona
  • Está documentado

Minimizar los globales

JavaScript usa funciones para administrar el alcance. Una variable declarada dentro de una función es local a esa función y no está disponible fuera de la función. Por otro lado, las variables globales son aquellas declaradas fuera de cualquier función o simplemente se utilizan sin ser declaradas.

Cada entorno de JavaScript tiene un objeto global accesible cuando lo usas fuera de cualquier función. Cada variable global que crees se convierte en una propiedad del objeto global. En los navegadores, por conveniencia, existe una propiedad adicional del objeto global llamada ventana que (normalmente) apunta al objeto global en sí. El siguiente fragmento de código muestra cómo crear y acceder a una variable global en un entorno de navegador:

1
myglobal = "hello"; // antipattern 

2
console.log(myglobal); // "hello" 

3
console.log(window.myglobal); // "hello" 

4
console.log(window["myglobal"]); // "hello" 

5
console.log(this.myglobal); // "hello"

El problema con los globales

El problema con las variables globales es que se comparten entre todo el código de tu aplicación JavaScript o página web. Viven en el mismo espacio de nombres global y siempre existe la posibilidad de colisiones de nombres, cuando dos partes separadas de una aplicación definen variables globales con el mismo nombre pero con diferentes propósitos.

También es común que las páginas web incluyan código no escrito por los desarrolladores de la página, por ejemplo:

  • Una biblioteca JavaScript de terceros.
  • Scripts de un socio publicitario.
  • Código de terceros en un script de seguimiento y análisis de usuarios.
  • Diferentes tipos de widgets, insignias y botones.

Supongamos que uno de los scripts de terceros define una variable global, llamada, por ejemplo, "result". Luego, más adelante, en una de tus funciones, defines otra variable global llamada "result". El resultado de eso es que la última variable de resultado sobrescribe las anteriores, y el script de terceros puede simplemente dejar de funcionar.

Por lo tanto, es importante ser un buen vecino de los otros scripts que pueden estar en la misma página y usar la menor cantidad posible de variables globales. Más adelante en el libro aprenderás sobre estrategias para minimizar el número de globales, como el patrón de espacio de nombres o las funciones inmediatas autoejecutables, pero el patrón más importante para tener menos globales es siempre usar var para declarar variables.

Es sorprendentemente fácil crear globales involuntariamente debido a dos características de JavaScript. Primero, puedes usar variables sin siquiera declararlas. Y en segundo lugar, JavaScript tiene la noción de globales implícitas, lo que significa que cualquier variable que no declares se convierte en una propiedad del objeto global (y es accesible como una variable global declarada correctamente). Considera el siguiente ejemplo:

1
function sum(x, y) { 
2
   // antipattern: implied global 

3
   result = x + y; 
4
   return result;
5
}

En este código, result se usa sin ser declarado. El código funciona bien, pero después de llamar a la función, terminas con un resultado de una variable más en el espacio de nombres global que puede ser una fuente de problemas.

La regla general es declarar siempre las variables con var, como se demuestra en la versión mejorada de la función sum():

1
function sum(x, y) {
2
   var result = x + y;
3
   return result;
4
}

Otro antipatrón que crea globales implícitos es encadenar asignaciones como parte de una declaración var. En el siguiente fragmento, a es local pero b se vuelve global, lo que probablemente no sea lo que pretendías hacer:

1
// antipattern, do not use 

2
function foo() {
3
   var a = b = 0;
4
   // ...

5
}

Si te preguntas por qué sucede eso, se debe a la evaluación de derecha a izquierda. Primero, se evalúa la expresión b = 0 y en este caso b no se declara. El valor de retorno de esta expresión es 0 y se asigna a la nueva variable local declarada con var a. En otras palabras, es como si hubieras escrito:

1
var a = (b = 0);

Si ya has declarado las variables, encadenar asignaciones está bien y no crea globales inesperadas. Por ejemplo:

1
function foo() { 
2
   var a, b;
3
   // ... a = b = 0; // both local

4
}

Otra razón más para evitar los globales es la portabilidad. Si deseas que tu código se ejecute en diferentes entornos (hosts), es peligroso usar globales porque puedes sobrescribir accidentalmente un objeto host que no existe en tu entorno original (por lo que pensaste que el nombre era seguro de usar) pero sí en otros entornos.


Efectos secundarios al olvidar "var"

Hay una pequeña diferencia entre los globales implícitos y los definidos explícitamente: la diferencia está en la capacidad de anular la definición de estas variables mediante el operador de eliminación:

  • Los globales creados con "var" (los creados en el programa fuera de cualquier función) no se pueden eliminar.
  • Los globales implícitos creados sin "var" (independientemente de si se crearon dentro de funciones) se pueden eliminar.

Esto muestra que las globales implícitas no son técnicamente variables reales, pero son propiedades del objeto global. Las propiedades se pueden eliminar con el operador de eliminación, mientras que las variables no se pueden:

1
// define three globals 

2
var global_var = 1; 
3
global_novar = 2; // antipattern 

4
(function () {
5
   global_fromfunc = 3; // antipattern 

6
}());
7
8
// attempt to delete 

9
delete global_var; // false 

10
delete global_novar; // true 

11
delete global_fromfunc; // true

12
13
// test the deletion 

14
typeof global_var; // "number" 

15
typeof global_novar; // "undefined" 

16
typeof global_fromfunc; // "undefined"

En el modo estricto de ES5, las asignaciones a variables no declaradas (como los dos antipatrones en el fragmento anterior) arrojarán un error.


Acceder al objeto global

En los navegadores, se puede acceder al objeto global desde cualquier parte del código a través de la propiedad de window (a menos que hayas hecho algo especial e inesperado, como declarar una variable local llamada window). Pero en otros entornos, esta propiedad de conveniencia puede llamarse de otra manera (o incluso no estar disponible para el programador). Si necesitas acceder al objeto global sin codificar window del identificador, puedes hacer lo siguiente desde cualquier nivel de alcance de función anidada:

1
var global = (function () { 
2
   return this;
3
}());

De esta manera siempre puedes obtener el objeto global, porque dentro de las funciones que fueron invocadas como funciones (es decir, no como constructoras con new), esto siempre debería apuntar al objeto global. En realidad, este ya no es el caso en ECMAScript 5 en modo estricto, por lo que debes adoptar un patrón diferente cuando tu código está en modo estricto. Por ejemplo, si estás desarrollando una librería, puedes envolver el código de tu librería en una función inmediata y luego, desde el alcance global, pasar una referencia a esto como un parámetro para su función inmediata.


Patrón "var" único

Usar una sola instrucción var en la parte superior de tus funciones es un patrón útil para adoptar. Tiene los siguientes beneficios:

  • Proporciona un único lugar para buscar todas las variables locales que necesita la función.
  • Evita errores lógicos cuando se utiliza una variable antes de que se defina.
  • Te ayuda a recordar declarar variables y, por lo tanto, minimizar los globales.
  • Es menos código (para escribir y transferir por cable)

El patrón de var única se ve así:

1
function func() { 
2
   var a = 1,
3
       b = 2, 
4
       sum = a + b, 
5
       myobject = {}, 
6
       i, 
7
       j;
8
   // function body...

9
}

Utilizas una instrucción var y declaras múltiples variables delimitadas por comas. Es una buena práctica inicializar también la variable con un valor inicial en el momento en que la declaras. Esto puede evitar errores lógicos (todas las variables declaradas y no inicializadas se inicializan con el valor undefined) y también mejorar la legibilidad del código. Cuando observes el código más adelante, podrás hacerte una idea sobre el uso previsto de una variable en función de su valor inicial; por ejemplo, ¿se suponía que era un objeto o un número entero?

También puedes hacer algún trabajo real en el momento de la declaración, como el caso con sum = a + b en el código anterior. Otro ejemplo es cuando se trabaja con referencias al DOM (Document Object Model). Puedes asignar referencias DOM a variables locales junto con la declaración única, como lo demuestra el siguiente código:

1
function updateElement() { 
2
   var el = document.getElementById("result"),
3
       style = el.style; 
4
   // do something with el and style...

5
}

Elevación: un problema con vars dispersas

JavaScript te permite tener múltiples declaraciones var en cualquier lugar de una función, y todas actúan como si las variables fueran declaradas en la parte superior de la función. Este comportamiento se conoce como elevación. Esto puede llevar a errores lógicos cuando usas una variable y luego la declaras nuevamente en la función. Para JavaScript, siempre que una variable esté en el mismo ámbito (misma función), se considera declarada, incluso cuando se usa antes de la declaración var. Échale un vistazo a éste ejemplo:

1
// antipattern 

2
myname = "global"; // global variable 

3
function func() {
4
    alert(myname); // "undefined" 

5
    var myname = "local"; 
6
    alert(myname); // "local"

7
} 
8
func();

En este ejemplo, puedes esperar que la primera alert() muestre "global" y la segunda muestre "local". Es una expectativa razonable porque, en el momento de la primera alerta, myname no se declaró y, por lo tanto, la función probablemente debería "ver" el myname global. Pero no es así como funciona. La primera alerta dirá "undefined" porque myname se considera declarado como una variable local de la función. (Aunque la declaración viene después). Todas las declaraciones de variables se elevan a la parte superior de la función. Por lo tanto, para evitar este tipo de confusión, es mejor declarar por adelantado todas las variables que pretendas utilizar.

El fragmento de código anterior se comportará como si se hubiera implementado así:

1
myname = "global"; // global variable 

2
function func() {
3
   var myname; // same as -> var myname = undefined;

4
   alert(myname); // "undefined"

5
   myname = "local";
6
   alert(myname); // "local" 

7
}
8
func();

Para completar, mencionemos que, en realidad, a nivel de implementación, las cosas son un poco más complejas. Hay dos etapas del manejo del código, donde las variables, las declaraciones de funciones y los parámetros formales se crean en la primera etapa, que es la etapa de analizar e ingresar al contexto. En la segunda etapa, se crea la etapa de ejecución del código en tiempo de ejecución, las expresiones de función y los identificadores no calificados (variables no declaradas). Pero para fines prácticos, podemos adoptar el concepto de izado, que en realidad no está definido por el estándar ECMAScript, pero se usa comúnmente para describir el comportamiento.


Bucles de tipo for

En bucles for, iteras sobre matrices u objetos similares a matrices, como argumentos y objetos de tipo HTMLCollection. El patrón habitual de bucle for se ve así:

1
// sub-optimal loop 

2
for (var i = 0; i < myarray.length; i++) {
3
   // do something with myarray[i]

4
}

Un problema con este patrón es que se accede a la longitud de la matriz en cada iteración del ciclo. Esto puede ralentizar tu código, especialmente cuando myarray no es una matriz sino un objeto de tipo HTMLCollection.

HTMLCollections son objetos devueltos por métodos DOM como:

  • document.getElementsByName()
  • document.getElementsByClassName()
  • document.getElementsByTagName()

También hay una serie de otros elementos HTMLCollections, que se introdujeron antes del estándar DOM y todavía se utilizan en la actualidad. Incluyen (entre otros):

  • document.images: Todos los elementos de IMG de la página.
  • document.links : Todos los elementos "A" (o los enlaces).
  • document.forms : Todos los formularios.
  • document.forms[0].elements: Todos los campos del primer formulario de la página.

El problema con las colecciones es que son consultas en vivo contra el documento subyacente (la página HTML). Esto significa que cada vez que accedes a la longitud (length) de cualquier colección, estás consultando el DOM en vivo, y las operaciones del DOM son costosas en general.

Es por eso que un mejor patrón para bucles for es almacenar en caché la longitud de la matriz (o colección) sobre la que estás iterando, como se muestra en el siguiente ejemplo:

1
for (var i = 0, max = myarray.length; i < max; i++) {
2
   // do something with myarray[i] 

3
}

De esta manera, obtienes el valor de longitud solo una vez y lo usa durante todo el ciclo.

El almacenamiento en caché de la longitud cuando se itera sobre HTMLCollections es más rápido en todos los navegadores, en cualquier lugar entre dos veces más rápido (Safari 3) y 190 veces (IE7).

Ten en cuenta que cuando tienes la intención explícita de modificar la colección en el ciclo (por ejemplo, agregando más elementos DOM), probablemente desees que la longitud se actualice y no sea constante.

Siguiendo el patrón var único, también puedes sacar el elemento var del ciclo y hacer que el ciclo sea algo así:

1
function looper() { 
2
   var i = 0,
3
        max, 
4
        myarray = [];
5
   // ...

6
   for (i = 0, max = myarray.length; i < max; i++) {
7
      // do something with myarray[i]

8
   }
9
}

Este patrón tiene el beneficio de la coherencia porque se adhiere al patrón var único. Un inconveniente es que hace un poco más difícil copiar y pegar bucles completos mientras se refactoriza el código. Por ejemplo, si estás copiando el bucle de una función a otra, debes asegurarte de transferir i y max a la nueva función (y probablemente eliminarlos de la función original si ya no se necesitan allí).

Un último ajuste al ciclo sería sustituir i++ con cualquiera de estas expresiones:

1
i=i+ 1 
2
i += 1

JSLint te solicita que lo hagas; la razón es que ++ y -- promueven "excesiva astucia". Si no estás de acuerdo con esto, puedes establecer la opción JSLint plusplus en false. (Es cierto de forma predeterminada).

Dos variaciones del patrón "for" introducen algunas microoptimizaciones porque:

  • Utiliza una variable menos (sin max.)
  • Cuenta regresiva hasta 0, que suele ser más rápido porque es más eficiente compararlo con 0 que con la longitud de la matriz o con cualquier otra cosa que no sea 0.

El primer patrón modificado es:

1
var i, myarray = []; 
2
for (i = myarray.length; i--;) {
3
   // do something with myarray[i]

4
}

Y el segundo usa un bucle while:

1
var myarray = [],
2
    i = myarray.length; 
3
while (i--) {
4
   // do something with myarray[i]

5
}

Estas son microoptimizaciones y solo se notarán en operaciones críticas para el rendimiento. Además, JSLint se quejará del uso de i--.


Bucles for-in

Los bucles for-in deben usarse para iterar sobre objetos que no son de matriz. El bucle con for-in también se denomina enumeration.

Técnicamente, también puedes usar for-in para recorrer matrices (porque en JavaScript las matrices son objetos), pero no se recomienda. Puede dar lugar a errores lógicos si el objeto de matriz ya se ha aumentado con una funcionalidad personalizada. Además, el orden (la secuencia) de enumerar las propiedades no está garantizado en un for-in. Por lo tanto, es preferible utilizar bucles for normales con matrices y bucles for-in para objetos.

Es importante utilizar el método hasOwnProperty() al iterar sobre las propiedades del objeto para filtrar las propiedades que descienden de la cadena del prototipo.

Considera el siguiente ejemplo:

1
// the object 

2
var man = {
3
   hands: 2, 
4
   legs: 2, 
5
   heads: 1
6
};
7
8
// somewhere else in the code 

9
// a method was added to all objects 

10
if (typeof Object.prototype.clone === "undefined") {
11
   Object.prototype.clone = function () {};
12
}

En este ejemplo tenemos un objeto simple llamado man definido con un objeto literal. En algún lugar antes o después de que se definiera man, el prototipo de Object se amplió con un método útil llamado clone(). La cadena de prototipos está activa, lo que significa que todos los objetos obtienen acceso automáticamente al nuevo método. Para evitar que aparezca el método clone() al enumerar man, debes llamar a hasOwnProperty() para filtrar las propiedades del prototipo. No hacer el filtrado puede resultar en la aparición de la función clone(), que es un comportamiento no deseado en la mayoría de los escenarios:

1
// 1. 

2
// for-in loop 

3
for (var i in man) {
4
   if (man.hasOwnProperty(i)) { // filter

5
      console.log(i, ":", man[i]);
6
   }
7
} 
8
/* result in the console 

9
hands : 2 

10
legs : 2 

11
heads : 1 

12
*/
13
// 2. 

14
// antipattern: 

15
// for-in loop without checking hasOwnProperty() 

16
for (var i in man) {
17
   console.log(i, ":", man[i]);
18
} 
19
/* 

20
result in the console 

21
hands : 2 

22
legs : 2 

23
heads : 1 

24
clone: function() 

25
*/

Otro patrón para usar hasOwnProperty() es llamar a ese método fuera del Object.prototype, así:

1
for (var i in man) { 
2
   if (Object.prototype.hasOwnProperty.call(man, i)) { // filter

3
      console.log(i, ":", man[i]);
4
   }
5
}

El beneficio es que puedes evitar las colisiones de nombres en caso de que el objeto man haya redefinido hasOwnProperty. Además, para evitar las búsquedas de propiedades largas hasta el Object, puedes usar una variable local para "almacenarlo en caché":

1
var i, hasOwn = Object.prototype.hasOwnProperty;
2
for (i in man) {
3
    if (hasOwn.call(man, i)) { // filter

4
        console.log(i, ":", man[i]);
5
    }
6
}

Estrictamente hablando, no usar hasOwnProperty() no es un error. Dependiendo de la tarea y la confianza que tengas en el código, puedes omitirlo y acelerar ligeramente los ciclos. Pero cuando no estás seguro sobre el contenido del objeto (y su cadena de prototipo), estás más seguro simplemente agregando la verificación hasOwnProperty().

Una variación de formato (que no pasa JSLint) omite una llave y coloca el if en la misma línea. El beneficio es que la declaración de bucle se lee más como un pensamiento completo (“para cada elemento que tenga una propiedad X propia, haz algo con X”). También hay menos indentación antes de llegar al propósito principal del bucle:

1
// Warning: doesn't pass JSLint 

2
var i, hasOwn = Object.prototype.hasOwnProperty;
3
for (i in man) if (hasOwn.call(man, i)) { // filter

4
    console.log(i, ":", man[i]);
5
}

(No) aumento de prototipos integrados

Aumentar la propiedad de prototipo de las funciones de constructor es una forma poderosa de agregar funcionalidad, pero a veces puede ser demasiado poderosa.

Es tentador aumentar los prototipos de constructores integrados como Object(), Array() o Function(), pero puede dañar seriamente la capacidad de mantenimiento, ya que hará que tu código sea menos predecible. Otros desarrolladores que usen tu código probablemente esperarán que los métodos de JavaScript integrados funcionen de manera consistente y no esperarán tus adiciones.

Además, las propiedades que agregas al prototipo pueden aparecer en bucles que no usan hasOwnProperty(), por lo que pueden crear confusión.

Por lo tanto, es mejor si no aumentas los prototipos integrados. Puedes hacer una excepción a la regla solo cuando se cumplan todas estas condiciones:

  • Se espera que las futuras versiones de ECMAScript o implementaciones de JavaScript implementen esta funcionalidad como un método integrado de manera consistente. Por ejemplo, puedes agregar métodos descritos en ECMAScript 5 mientras esperas que los navegadores se pongan al día. En este caso, solo estás definiendo los métodos útiles con anticipación.
  • Verificas si tu propiedad o método personalizado aún no existe; tal vez ya esté implementado en algún otro lugar del código o ya sea parte del motor JavaScript de uno de los navegadores que admites.
  • Documentas y comunicas claramente el cambio con el equipo.

Si se cumplen estas tres condiciones, puedes continuar con la adición personalizada al prototipo, siguiendo este patrón:

1
if (typeof Object.protoype.myMethod !== "function") { 
2
   Object.protoype.myMethod = function () {
3
      // implementation...

4
   };
5
}

Patrón switch

Puedes mejorar la legibilidad y la robustez de las instrucciones switch siguiendo este patrón:

1
var inspect_me = 0, 
2
    result = '';
3
switch (inspect_me) { 
4
case 0:
5
   result = "zero";
6
   break; 
7
case 1:
8
   result = "one";
9
   break; 
10
default:
11
   result = "unknown";
12
}

Las convenciones de estilo seguidas en este sencillo ejemplo son:

  • Alinear cada case con switch (una excepción a la regla de indentación de llaves).
  • Indenta el código dentro de cada caso.
  • Termina cada case con un break; claro.
  • Evita caídas (cuando omites la ruptura intencionalmente). Si estás absolutamente convencido de que una falla es el mejor enfoque, asegúrate de documentar estos casos, porque pueden parecer errores ante los lectores de tu código.
  • Finaliza el switch con un valor default: para asegurarse de que siempre haya un resultado sensato incluso si ninguno de los casos coincide.

Evitar la conversión de tipos implícitos

JavaScript tipifica implícitamente las variables cuando las comparas. Es por eso que comparaciones como false == 0 o "" == 0 devuelven true.

Para evitar la confusión causada por el encasillado implícito, utiliza siempre los operadores === y !== que verifican tanto los valores como el tipo de expresiones que comparas:

1
var zero = 0; 
2
if (zero === false) {
3
   // not executing because zero is 0, not false

4
}
5
6
// antipattern 

7
if (zero == false) {
8
   // this block is executed...

9
}

Hay otra escuela de pensamiento que se suscribe a la opinión de que es redundante usar === cuando == es suficiente. Por ejemplo, cuando usas typeof, sabes que devuelve una cadena, por lo que no hay razón para usar la igualdad estricta. Sin embargo, JSLint requiere una estricta igualdad; hace que el código parezca coherente y reduce el esfuerzo mental al leer el código. ("¿Es esto == intencional u omisión?")


Evitar eval()

Si detectas el uso de eval() en tu código, recuerda el mantra "eval() es malo". Esta función toma una cadena arbitraria y la ejecuta como código JavaScript. Cuando el código en cuestión se conoce de antemano (no se determina en tiempo de ejecución), no hay razón para usar eval(). Si el código se genera dinámicamente en tiempo de ejecución, a menudo hay una mejor manera de lograr el objetivo sin eval(). Por ejemplo, solo usar la notación de corchetes para acceder a propiedades dinámicas es mejor y más sencillo:

1
// antipattern 

2
var property = "name"; 
3
alert(eval("obj." + property));
4
5
// preferred 

6
var property = "name"; 
7
alert(obj[property]);

El uso de eval() también tiene implicaciones de seguridad, ya que es posible que estés ejecutando código (por ejemplo, procedente de la red) que se ha manipulado. Este es un antipatrón común cuando se trata de una respuesta JSON de una solicitud Ajax. En esos casos, es mejor utilizar los métodos integrados de los navegadores para analizar la respuesta JSON para asegurarte de que es segura y válida. Para los navegadores que no admiten JSON.parse() de forma nativa, puedes utilizar una biblioteca de JSON.org.

También es importante recordar que pasar cadenas a setInterval(), setTimeout() y la Function() constructora es, en su mayor parte, similar al uso de eval() y, por lo tanto, debe evitarse. En segundo plano, JavaScript todavía tiene que evaluar y ejecutar la cadena que se pasa como código de programación:

1
// antipatterns 

2
setTimeout("myFunc()", 1000); 
3
setTimeout("myFunc(1, 2, 3)", 1000);
4
5
// preferred 

6
setTimeout(myFunc, 1000); 
7
setTimeout(function () {
8
   myFunc(1, 2, 3); 
9
}, 1000);

El uso del nuevo constructor Function() es similar a eval() y debe abordarse con cuidado. Podría ser una construcción poderosa, pero a menudo se utiliza mal. Si absolutamente debes utilizar eval(), puedes considerar el uso de new Function() en su lugar. Hay una pequeña ventaja potencial porque el código evaluado en new Function() se ejecutará en un ámbito de función local, por lo que las variables definidas con var en el código que se está evaluando no se convertirán automáticamente en globales. Otra forma de evitar globales automáticos es envolver la llamada eval() en una función inmediata.

Considera el siguiente ejemplo. Aquí sólo permanece un como una variable global que contamina el espacio de nombres:

1
console.log(typeof un);	// "undefined" 

2
console.log(typeof deux); // "undefined" 

3
console.log(typeof trois); // "undefined"

4
5
var jsstring = "var un = 1; console.log(un);";
6
eval(jsstring); // logs "1"

7
8
jsstring = "var deux = 2; console.log(deux);"; 
9
new Function(jsstring)(); // logs "2"

10
11
jsstring = "var trois = 3; console.log(trois);"; 
12
(function () {
13
   eval(jsstring); 
14
}()); // logs "3"

15
16
console.log(typeof un); // number

17
console.log(typeof deux); // undefined

18
console.log(typeof trois); // undefined

Otra diferencia entre eval() y el constructor Function es que eval() puede interferir con la cadena de ámbito, mientras que Function es mucho más para un espacio aislado. No importa dónde ejecutes Function, esto solo ve el ámbito global. Por lo que puede hacer menos contaminación a la variable local. En el siguiente ejemplo, eval() puede acceder y modificar una variable en su ámbito externo, mientras que Function no puede (también ten en cuenta que usar Function o new Function es idéntico):

1
(function () { 
2
   var local = 1;
3
   eval("local = 3; console.log(local)"); // logs 3

4
   console.log(local); // logs 3 

5
}());
6
7
(function () { 
8
   var local = 1;
9
   Function("console.log(typeof local);")(); // logs undefined 

10
}());

Conversiones de números con parseInt()

Usando parseInt() puedes obtener un valor numérico de una cadena. La función acepta un segundo parámetro de base, que a menudo se omite pero no debería. Los problemas ocurren cuando la cadena a analizar comienza con 0: por ejemplo, una parte de una fecha ingresada en un campo de formulario. Las cadenas que comienzan con 0 se tratan como números octales (base 8) en ECMAScript 3; sin embargo, esto ha cambiado en ES5. Para evitar inconsistencias y resultados inesperados, siempre especifica el parámetro radix:

1
var month = "06",
2
    year = "09";
3
month = parseInt(month, 10);
4
year = parseInt(year, 10);

En este ejemplo, si omites el parámetro de base como parseInt(year), el valor devuelto será 0, porque "09" asume un número octal (como si hiciera parseInt(year, 8)) y 09 no es un dígito válido en base 8.

Las formas alternativas de convertir una cadena en un número incluyen:

1
+"08" // result is 8 

2
Number("08") // 8

A menudo son más rápidos que parseInt(), porque parseInt(), como su nombre lo indica, analiza y no simplemente convierte. Pero si esperas una entrada como "08 hello", parseInt() devolverá un número, mientras que los demás fallarán con NaN.


Convenciones de codificación

Es importante establecer y seguir las convenciones de codificación; hacen que tu código sea coherente, predecible y mucho más fácil de leer y comprender. Un nuevo desarrollador que se une al equipo puede leer las convenciones y ser productivo mucho antes, entendiendo el código escrito por cualquier otro miembro del equipo.

Se han librado muchas guerras de fuego en reuniones y en listas de correo sobre aspectos específicos de ciertas convenciones de codificación (por ejemplo, la indentación del código: ¿pestañas o espacios?). Entonces, si eres tú quien sugiere la adopción de convenciones en tu organización, debes estar preparado para enfrentar la resistencia y escuchar opiniones diferentes pero igualmente fuertes. Recuerda que es mucho más importante establecer y seguir constantemente una convención, cualquier convención, que cuáles serán los detalles exactos de esa convención.


Indentación

El código sin indentación es imposible de leer. Lo único peor es el código con indentación incoherente, porque parece que está siguiendo una convención, pero puede tener sorpresas confusas en el camino. Es importante estandarizar el uso de la indentación.

Algunos desarrolladores prefieren la indentación con pestañas, porque cualquiera puede modificar su editor para mostrar las pestañas con el número de espacios preferido individualmente. Algunos prefieren espacios, generalmente cuatro. No importa siempre que todos en el equipo sigan la misma convención. Este libro, por ejemplo, usa una indentación de cuatro espacios, que también es la predeterminada en JSLint.

¿Y qué deberías indentar? La regla es simple: cualquier cosa dentro de llaves. Esto significa los cuerpos de funciones, bucles (do, while, for, for-in), ifs, switches y propiedades de tipo Object en la notación literal del objeto. El siguiente código muestra algunos ejemplos del uso de la indentación:

1
function outer(a, b) {
2
    var c = 1,
3
        d = 2,
4
        inner;
5
    if (a > b) {
6
        inner = function () {
7
            return {
8
                r: c - d
9
            };
10
        };
11
    } else {
12
        inner = function () {
13
            return {
14
                r: c + d
15
            };
16
        };
17
    }
18
    return inner;
19
}

Llaves

Las llaves siempre deben utilizarse, incluso en los casos en que son opcionales. Técnicamente, si solo tienes una declaración en un if o un for, no se requieren llaves, pero siempre debes usarlas de todos modos. Hace que el código sea más consistente y más fácil de actualizar.

Imagina que tienes un bucle for con una sola declaración. Puedes omitir las llaves y no habrá error de sintaxis:

1
// bad practice 

2
for (var i = 0; i < 10; i += 1)
3
   alert(i);

Pero, ¿qué pasa si, más adelante, agregas otra línea en el cuerpo del bucle?

1
// bad practice 

2
for (var i = 0; i < 10; i += 1)
3
   alert(i); 
4
   alert(i + " is " + (i % 2 ? "odd" : "even"));

La segunda alerta está fuera del bucle, aunque la indentación puede engañarte. Lo mejor que se puede hacer a largo plazo es utilizar siempre las llaves, incluso para bloques de una línea:

1
// better 

2
for (var i = 0; i < 10; i += 1) {
3
   alert(i);
4
}

De manera similar para las condiciones if:

1
// bad 

2
if (true)
3
   alert(1); 
4
else
5
   alert(2);
6
7
// better 

8
if (true) {
9
   alert(1); 
10
} else {
11
   alert(2);
12
}

Ubicación de la llave de apertura

Los desarrolladores también tienden a tener preferencias sobre dónde debería estar la llave de apertura: ¿en la misma línea o en la siguiente línea?

1
if (true) {
2
   alert("It's TRUE!");
3
}

O:

1
if (true)
2
{
3
   alert("It's TRUE!");
4
}

En este ejemplo específico, es una cuestión de preferencia, pero hay casos en los que el programa puede comportarse de manera diferente dependiendo de dónde esté la llave. Esto se debe al mecanismo de inserción de punto y coma: JavaScript no es exigente cuando eliges no terminar tus líneas correctamente con un punto y coma y lo agrega por ti. Este comportamiento puede causar problemas cuando una función devuelve un objeto literal y la llave de apertura está en la siguiente línea:

1
// warning: unexpected return value 

2
function func() {
3
   return
4
  // unreachable code follows

5
   {
6
      name : "Batman"
7
   }
8
}

Si esperas que esta función devuelva un objeto con una propiedad name, te sorprenderás. Debido a los puntos y comas implícitos, la función devuelve undefined. El código anterior es equivalente a este:

1
// warning: unexpected return value 

2
function func() {
3
   return undefined;
4
  // unreachable code follows

5
   {
6
      name : "Batman"
7
   }
8
}

En conclusión, usa siempre llaves y coloca siempre la de apertura en la misma línea que la declaración anterior:

1
function func() {
2
   return {
3
      name : "Batman"
4
   };
5
}

Una nota sobre el punto y coma: al igual que con las llaves, siempre debes usar punto y coma, incluso cuando están implícitos en los analizadores de JavaScript. Esto no solo promueve la disciplina y un enfoque más riguroso del código, sino que también ayuda a resolver ambigüedades, como mostró el ejemplo anterior.


Espacio en blanco

El uso de espacios en blanco también puede contribuir a mejorar la legibilidad y la coherencia del código. En oraciones escritas en inglés, usas intervalos después de comas y puntos. En JavaScript, sigues la misma lógica y agregas intervalos después de expresiones similares a listas (equivalentes a comas) y declaraciones de finalización (equivalentes a completar un "pensamiento").

Los buenos lugares para usar un espacio en blanco incluyen:

  • Después del punto y coma que separan las partes de un bucle for: por ejemplo, for (var i. 0; i
  • Al inicializar múltiples variables (i y max) en un bucle for: for (var i = 0, max = 10; i.
  • Después de las comas que delimitan los elementos de la matriz: var a = [1, 2, 3];
  • Después de las comas en las propiedades del objeto y después de los dos puntos que dividen los nombres de las propiedades y
    sus valores: var o = {a: 1, b: 2};.
  • Al delimitar los argumentos de la función: myFunc (a, b, c).
  • Antes de las llaves en las declaraciones de funciones: function myFunc () {}.
  • Después de la función en expresiones de función anónimas: var myFunc = function () {};.

Otro buen uso del espacio en blanco es separar todos los operadores y sus operandos con espacios, lo que básicamente significa usar un espacio antes y después de +, -, *, =,, =, ===,! ==, &&, ||, + =, y así sucesivamente:

1
// generous and consistent spacing 

2
// makes the code easier to read 

3
// allowing it to "breathe" 

4
var d = 0,
5
    a = b + 1;
6
if (a && b && c) {
7
    d = a % c;
8
    a += d;
9
}
10
11
// antipattern 

12
// missing or inconsistent spaces 

13
// make the code confusing 

14
var d = 0,
15
    a = b + 1;
16
if (a && b && c) {
17
    d = a % c;
18
    a += d;
19
}

Y una nota final sobre el espacio en blanco: Espaciado entre llaves. Es bueno usar un espacio:

  • Antes de abrir llaves ({) en funciones, casos if-else, bucles y objetos literales.
  • Entre la llave de cierre (}) y else o while.

Un caso en contra del uso liberal del espacio en blanco podría ser que podrías aumentar el tamaño del archivo, pero
la minificación se ocupa de este problema.

Un aspecto de la legibilidad del código que a menudo se pasa por alto es el uso de espacios en blanco verticales. Puedes utilizar líneas en blanco para separar unidades de código, al igual que los párrafos se utilizan en la literatura para separar ideas.


Convenciones de nombres

Otra forma de hacer que tu código sea más predecible y fácil de mantener es adoptar convenciones de nomenclatura. Eso significa elegir nombres para tus variables y funciones de manera coherente.

A continuación se muestran algunas sugerencias de convenciones de nomenclatura que puedes adoptar tal cual o modificar a tu gusto. Una vez más, tener una convención y seguirla consistentemente es mucho más importante que lo que realmente es esa convención.


Capitalización de constructores

JavaScript no tiene clases pero tiene funciones de constructor invocadas con new:

1
var adam = new Person();

Debido a que los constructores siguen siendo solo funciones, esto ayuda si puedes saber, con solo mirar el nombre de una función, si se suponía que debía comportarse como un constructor o como una función normal.

Nombrar constructores con una primera letra mayúscula proporciona esa pista. El uso de minúsculas para funciones y métodos indica que se supone que no deben llamarse con new:

1
function MyConstructor() {...} 
2
function myFunction() {...}

Separando palabras

Cuando tienes varias palabras en una variable o el nombre de una función, es una buena idea seguir una convención sobre cómo se separarán las palabras. Una convención común es utilizar el llamado camel case. Siguiendo la convención del camel case, escribe las palabras en minúsculas, y solamente en mayúscula la primera letra de cada palabra.

Para tus constructores, puedes usar mayúsculas con camel case, como en MyConstructor(), y para los nombres de funciones y métodos, puedes usar minúsculas con camel case, como en myFunction(), calculateArea() y getFirstName().

¿Y las variables que no son funciones? Los desarrolladores suelen utilizar minúsculas en camel case para los nombres de las variables, pero otra buena idea es utilizar todas las palabras en minúscula delimitadas por un guión bajo: por ejemplo, first_name, favorite_bands y old_company_name. Esta notación te ayuda a distinguir visualmente entre funciones y todos los demás identificadores: primitivas y objetos.

ECMAScript usa mayúsculas y minúsculas tanto para métodos como para propiedades, aunque los nombres de propiedades de varias palabras son raros (propiedades lastIndex e ignoreCase de objetos de expresión regular).


Otros patrones de denominación

A veces, los desarrolladores utilizan una convención de nomenclatura para crear o sustituir características de lenguaje.

Por ejemplo, no hay forma de definir constantes en JavaScript (aunque hay algunas incorporadas como Number.MAX_VALUE), por lo que los desarrolladores han adoptado la convención de usar mayúsculas para nombrar variables que no deberían cambiar valores durante la vida del programa, como:

1
// precious constants, please don't touch 

2
var PI = 3.14,
3
    MAX_WIDTH = 800;

Existe otra convención que compite por el uso de mayúsculas: el uso de letras mayúsculas para los nombres de variables globales. Nombrar globales con mayúsculas puede reforzar la práctica de minimizar su número y hacer que se puedan distinguir fácilmente.

Otro caso de uso de una convención para imitar la funcionalidad es la convención de miembros privados. Aunque puedes implementar la verdadera privacidad en JavaScript, a veces a los desarrolladores les resulta más fácil usar un prefijo de subrayado para indicar un método o propiedad privados. Considera el siguiente ejemplo:

1
var person = {
2
    getName: function () {
3
        return this._getFirst() + ' ' + this._getLast();
4
    },
5
6
    _getFirst: function () {
7
        // ...

8
    },
9
    _getLast: function () {
10
        // ...

11
    }
12
};

En este ejemplo, getName() está destinado a ser un método público, parte de la API estable, mientras que _getFirst() y _getLast() están destinados a ser privados. Siguen siendo métodos públicos normales, pero el uso del prefijo de subrayado advierte a los usuarios del objeto persona que no se garantiza que estos métodos funcionen en la próxima versión y que no se deben utilizar directamente. Ten en cuenta que JSLint se quejará de los prefijos de subrayado, a menos que establezcas la opción nomen: false.

A continuación se muestran algunas variedades de la convención _private:

  • Usar un guión bajo al final para significar privado, como en name_ y getElements_().
  • Usar un prefijo de subrayado para las propiedades de tipo _protected y dos para las propiedades de tipo __private-
  • En Firefox, algunas propiedades internas que técnicamente no forman parte del lenguaje están disponibles y se nombran con un prefijo de dos guiones bajos y un sufijo de dos guiones bajos, como __proto__ y __parent__.

Escribir comentarios

Tienes que comentar tu código, incluso si es poco probable que alguien más que tú lo toque. A menudo, cuando estás metido en un problema, piensas que es obvio lo que hace el código, pero cuando vuelves al código después de una semana, te cuesta recordar cómo funcionó exactamente.

No debes exagerar comentando lo obvio: cada variable o cada línea. Pero normalmente necesitas documentar todas las funciones, sus argumentos y valores de retorno, y también cualquier algoritmo o técnica interesante o inusual. Piensa en los comentarios como sugerencias para los futuros lectores del código; los lectores deben comprender lo que hace tu código sin leer mucho más que solo los comentarios y los nombres de funciones y propiedades. Cuando tienes, por ejemplo, cinco o seis líneas de código que realizan una tarea específica, el lector puede omitir los detalles del código si proporcionas una descripción de una línea que describa el propósito del código y por qué está allí. No existe una regla estricta y rápida o una proporción de comentarios a código; algunos fragmentos de código (piensa en expresiones regulares) pueden requerir más comentarios que código.

El hábito más importante, aunque más difícil de seguir, es mantener los comentarios actualizados, porque los comentarios desactualizados pueden inducir a error y ser mucho peores que ningún comentario.


Sobre el autor

Stoyan Stefanov es un desarrollador web y autor de Yahoo, colaborador y revisor técnico de varios libros de O'Reilly. Habla regularmente sobre temas de desarrollo web en conferencias y en su blog en www.phpied.com. Stoyan es el creador de la herramienta de optimización de imágenes smush.it, colaborador de YUI y arquitecto de la herramienta de optimización del rendimiento de Yahoo, YSlow 2.0.


Comprar el libro


JavaScript PatternsJavaScript PatternsJavaScript Patterns

Este artículo es un extracto de "JavaScript Patterns", de O'Reilly Media.