Mostrando las entradas con la etiqueta javascript. Mostrar todas las entradas
Mostrando las entradas con la etiqueta javascript. Mostrar todas las entradas

Programar Javascript eficientemente

Código sin errores

Puede parecer una obviedad, pero muchos programadores noveles cometen grandes errores de sintaxis en su código sin darse cuenta y, puesto que los intérpretes de javascript tienen cierta tolerancia a dichos errores, no se dan cuenta.

Dichos errores, aunque no frenen la ejecución en según que intérpretes, drenan en gran medida el rendimiento, y pueden convertir el algoritmo más eficiente del mundo en el código más lento jamás ejecutado.

// Errores comunes
var a = 5 // Omitir ;
b = 6; // No declarar variables apropiadamente
function c(){
return this; // this fuera de objetos (devolverá window)
}

Y por último un caso un poco complicado de entender.

var d = [ 1, 2, 3, 4, 5, 6 ], e = 0;
for( var f in d ){ e += f; }

La sintaxis for( x in y ) ejecuta el código dentro del bucle asignando a x todos y cada uno de los atributos de y. Es decir, en el caso anterior, no sólo recorrería todos los elementos del Array, sino sus métodos tales como splice, slice... o sus atributos como length. O al menos es así en intérpretes que respetan el estándar, no obstante la mayoría tolera este error y oculta deliberadamente las propiedades extras.

Para comprobar los errores del código recomiendo usar jslint.com.

Estructuras en tiempo de compilación

Muchos intérpretes de javascript precompilan el código para mejorar su eficiencia. No obstante, y dada la enorme flexibilidad de javascript, podemos declarar clases mediante prototipado en tiempo de ejecución, en cuyo caso la mejora del rendimiento será mucho menor.

// Declarada en tiempo de compilación
var objeto = {
foo : function( o ){ return o+1; },
bar : function( o ){ return 0; }
};

// Declarada en tiempo de ejecución
var objeto = ( function(){
var r = {};
r.foo = function( o ){ return o + 1; };
r.bar = function( o ){ return 0; };
return r;
}() );
Evitar ejecuciones repetitivas en los bucles

Esto es algo muy común a todos los lenguajes de programación, el evitar llamadas extras innecesarias. Recordemos que el código de dentro de un bucle se ejecutará en todas las iteraciones, de modo que si podemos reducir llamadas extras desde su interior aumentaremos el rendimiento notablemente.

function foo(){return 5;}
// Estos bucles ejecutarán una función en cada iteración.
var i = 0;
while( i < foo()){
i++;
}
for( var j = 0; j < foo(); j++ ){
i++;
}

// Cuando en realidad podrían reducir su ejecución a una única vez
var vfoo = foo(), i = 0;
while( i < vfoo ){
i++;
}
for( var f = 0; j < vfoo; j++ ){
i++;
}

Esta norma incluye una de las penalizaciones de rendimiento más comunes.

var a=[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], b=0;

// Este bucle obtiene, en cada iteración, al atributo length del Array
for( var i = 0; i < a.length; i++ ){
b += a[ i ];
}

// Cuando debería acceder a él una única vez
for( var i = 0, l = a.length; i < l; i++ ){
b += a[ i ];
}

Tened en cuenta que en este último caso, al estar tanto el Array como el bucle en el mismo ámbito de variables, la mejora no es perceptible, pero cuando el Array se encuentra en un ámbito más global, o en un objeto, la mejora es sustancial.

Evita los dinosaurios

Hay miles de frameworks pululando por la red, muchos de ellos demasiado grandes y pesados. Si bien es cierto que es un absurdo el reinventar la rueda con cada nuevo proyecto, también lo es el cargar un framework enorme para sólo utilizar determinada funcionalidad.

Siempre es preferible reutilizar porciones de código conforme se vayan necesitando o, si finalmente optamos por un framework, cerciorarse de que es modular, y evitar ciertas características extremadamente ineficientes que muchos incluyen.

Espero no haberme olvidado de demasiadas cosas, aunque probablemente mantenga el texto actualizado.

Javascript: Localizar nodos del DOM

Javascript dispone de tres funciones para localizar nodos.

getElementById(id)
Recibe el id del elemento que se quiere encontrar, y lo retorna.
getElementsByName(name)
Recibe un nombre, y retorna una lista de nodos con ese nombre en su atributo name.
getElementsByTagName(tagName)
Recibe el nombre de nodo (p.ej: input, div, img...), y retorna una lista de nodos de ese tipo.

Éstas son las estándares, pero muchos frameworks poseen los suyos propios (algunos extremadamente ineficientes) además de muchos otros algoritmos que rondan por foros y blogs más visitados que éste.

Pero ¿qué ocurre si quiero filtrar sus resultados según el valor de algún otro de sus atributos? Sencillo: buscamos usando alguno de los estándares y filtramos.

Podéis bajar el mismo código (0.5KiB) o la versión minificada (0.3KiB) de las descargas del blog en Google Code.

function require(a,o){
/* Filter a list of DOM nodes by theirs attributes
* a: hash with attribute name as key, and attributes value (or cmp function) as value
* b: list of DOM nodes
*/
var tr=[];
for(var j in a){if(typeof(a[j])!="function"){
a[j]=(function(a){return function(v){return v==a;}}(a[j]));}}
for(var i=0,l=o.length,j,k;i<l;i++){
k=1;
for(var j in a){
k=(typeof(o[i][j])!="undefined")&&(a[j](o[i][j]));}
if(k){tr[tr.length]=o[i];}}
return tr;
}

Tengo la buena costumbre de comentar el código en inglés, pero por si acaso explico, recibe un objeto hash (o diccionario) con los atributos a buscar y sus valores (o funciones de comparación), y una lista de nodos del DOM.

Siempre habrá que pasarle una lista de nodos puesto que se trata simplemente de un filtro. Pero puesto que los atributos posibles dependen directamente del tipo de nodo, usarlo en conjunción con getElementsByTagName es bastante trivial.

Usarlo es muy sencillo y potente, como ejemplo vamos a buscar todos los botones de envío de formulario.

var submits=require(
{type:"submit"},
document.getElementsByTagName("input"));

O buscamos los cuadros de selección que estén activados.

var checkedboxes=require(
{checked:"checked"},
document.getElementsByTagName("checkbox"));

Y también permite especificar filtros personalizados.

var divs=require(
{className:function(v){
return v.search("test")>-1;}},
document.getElementsByTagName("div"));

Como veis, es un método muy sencillo a la par que útil.

Utilidades imprescindibles para desarrollo web

A la hora de meteros en el mundo del desarrollo web, muchos optan por grandes entornos de programación, otros nos contentamos con simples editores con resaltado de sintaxis, pero todos necesitamos de herramientas para comprobar errores en el código final, tanto en el CSS, XHTML e incluso Javascript que generen nuestros proyectos, y ahí entran en juego las siguientes utilidades.

Web developer toolbar (Firefox)
Web developer toolbar es una extensión que agrega una barra a Mozilla Firefox, con la que se puede validar el código, desactivar ciertas funcionalidades del navegador, facilitar el ver los elementos de la página... Muy útil para comprobar la calidad del resultado, la accesibilidad, y que el Javascript no es obstrusivo.
FireBug (Firefox)
La más famosa utilidad de este tipo. Permite analizar el código tanto HTML como CSS y editarlos en tiempo real, y hacer debug de Javascript usando puntos de interrupción, observando variables, etc. Además de poder ver y analizar, en tiempo real, las peticiones al servidor. Es un devorador recursos y hace algunas cosas de forma poco ortodoxa, pero es un imprescindible.
Developer tools (incluido en navegadores webkit)
Dependiendo del navegador puede funcionar bien o no, pero pasa a ser, junto con FireBug, una de las herramientas más potentes, con análisis y edición del marcado y sintaxis de HTML y CSS, además de un completo debugger para Javascript (aunque yo sigo prefiriendo el de FireBug). También dispone de análisis de peticiones a servidor, cookies, etc.

Además, no puedo dejar de recomendar las siguientes utilidades web para ayudaros a trabajar con javascript.

Javascript Shell y Javascript Development Environment
Un intérprete interactivo para javascript, cuyo bookmarklet lo abre utilizando la página actual como ámbito, y un editor de Javascript, algo limitado, pero que permite ejecutar directamente el código editado en el intérprete del navegador.
JSLint
Utilidad que valida la calidad del código javascript, comprobación de errores, e incluye un modo de calidad de la sintaxis.

Eventos Javascript multinavegador

Editado 15.03.2010: He eliminado el soporte específico a navegadores no estándares. No obstante, en dichos navegadores seguirá funcionando gracias al código general para navegadores antiguos. Además, se ha añadido una función para facilitar el añadir eventos que sólo se ejecuten una vez (ver más abajo). Las descargas están actualizadas.

Siempre que me dedico a diseñar interfaces asistidas con javascript, me encuentro con el mismo problema: ¿cómo manejar los eventos eficientemente y mantener la compatibilidad multinavegador?

La solución sencilla, y la más compatible1 (DOM 0), consiste en los atributos de evento, como onclick u onmouseover, pero tiene serias limitaciones: sólo admite una función (aunque podemos concatenar varias), y si optamos por concatenar funciones ni podríamos eliminar alguna, aparte de que la cancelación de eventos con return se vuelve una pesadilla.

Sin embargo, los navegadores estándares (motores mozilla, webkit, khtml...) disponen de elemento.addEventListener( nombre_evento, función, capturar ), que simplifica mucho el proceso de asignar varias funciones y elegir si capturan el evento por completo con un booleano, junto con elemento.removeEventListener( nombre_evento, función, capturaba ) para borrar funciones asignadas al elemento.

Por otro lado, navegadores no estándares2 (motor trident) recurren a elemento.attachEvent( onevento, función ), que funciona como addEventListener pero con nombres de eventos de HTML, y no permite establecer si se captura el evento o no, y para eliminar la función del evento tenemos elemento.detachEvent( onevento, función).

Crear una función para simplificar el proceso de asignación y desasignación de funciones a eventos, que sirva para los tres casos anteriores, nos lleva a renunciar a la posibilidad de definir si el evento se captura al momento de asignarlo, pero aún queda la posibilidad de capturar el evento dentro de la función asignada.

Pero de nuevo, existe una forma estándar y varias no estándar de capturar el evento, además de que los navegadores más antiguos sólo lo detendrán si la función asignada retorna false. En este último caso tendremos la fortuna3 de que dichos navegadores tampoco aceptan las formas modernas de asignación.

Teniendo todo esto en cuenta, he malgastado unas preciosas horas de mi vida en diseñar tres útiles funciones auxiliares (que empezaré a usar en todos lados). Podéis descargar este mismo código (3.2 KiB) a o la versión minificada (1 KiB).

/* Mi función auxiliar preferida, recibe un nombre de propiedad
* y un objeto, y retorna true o false dependiendo de si existe en
* el objeto o no. Si no se recibe un objeto se asume window */
function def(p,o){return (typeof((o||window)[p])!="undefined");}
function attEvent(o,e,f){
/* Función de asignación de evento, recibe objeto, nombre de evento
* (p.ej. click, no onclick) y función de callback */
var w3="addEventListener",a="attachedEvents",cp="cancel",nf,of,nr;
if(def(w3,o)){o[w3](e,f,false);} // Estándar
else{ // Funcionalidad para navegadores antiguos
if(!def(a,o)){o[a]={};} // Objeto auxiliar
if(!def(e,o[a])){o[a][e]={};} // Objeto evento
if(!def(f,o[a][e])){ // No agregar dos veces la misma función
nf=function(){
/* Detección de funciones bloqueadas por detEvent:
* si ha sido bloqueada, no se ejecuta la función */
if(o[a][e][f]){f.apply(this,arguments);}
};
nr=function(v){
// Retorna false si se ha ejecutado stopEvent
return !(def(cp,(v||window.event))&&(v[cp]==true));
};
if(o["on"+e]){
/* Si hay funciones previamente asignadas, asignamos
* una función que las ejecutará junto con la nueva */
of=o["on"+e]; // Funciones previas
o["on"+e]=function(){
// Ejecutamos:
of.apply(this,arguments); // 1. funciones previas
nf.apply(this,arguments); // 2. nueva función
// Retornamos false si se ha ejecutado stopEvent
return nr.apply(this,arguments);
};
}
else{o["on"+e]=nf;} // Agregar la primera función
}
/* Asignamos 1 en la función para el evento, si el valor
* pasa a ser 0 (por detEvent), la función no se ejecutará */
o[a][e][f]=1;
}
}
function attEventOnce(o,e,f){
attEvent(o,e,function(){
f.apply(this,arguments);
detEvent(this,e,arguments.callee);
});
}
function detEvent(o,e,f){
/* Función de desasignación de evento, recibe objeto, nombre de
* evento, y función a eliminar (o bloquear en el último caso) */
var w3="removeEventListener",a="attachedEvents";
if(def(w3,o)){o[w3](e,f,false);} // Estándar
else if(def(a,o)&&def(e,o[a])&&def(f,o[a][e])){
// Modo para navegadores antiguos (no borra, sólo desactiva)
o[a][e][f]=0;
}
}
function stopEvent(e){
/* Función de detención de eventos, recibe la instancia del evento
* por parámetro. */
var w3="stopPropagation",mz="preventDefault",e=(e||window.event);
/* Para navegadores que cumplen los estándares */
if(def(w3,e)){e[w3]();}
/* Evitamos la acción por defecto (mozilla) */
if(def(mz,e)){e[mz]();}
/* Para navegadores antiguos y no estándares: establecemos cancel y
* cancelBubble a true, returnValue a false, y retornamos false
* por si queremos usarlo en eventos asignados directamente. */
return e.returnValue=!(e.cancel=e.cancelBubble=true);
}

Usando estos métodos, agregar varias funciones al mismo evento sería bastante simple.

attEvent(window,"load",function(){alert("Primera vez.");});
attEvent(window,"load",function(){alert("Segunda vez.");});

O incluso podríamos asignar una función que se desasignara a sí misma y detuviera el evento cuando se ejecutase.

attEvent(document,"click",function(e){
alert("¡Cliclí por primera vez, capturado!");
// Desasignamos la propia función (callee) del elemento (this)
detEvent(this,"click", arguments.callee);
// Detenemos el evento
stopEvent(e);
});
Editado 15.03.2010: Ahora existe la posibilidad, a petición popular, de añadir eventos de un sólo uso de forma más sencilla: con attEventOnce.

attEventOnce(document,"click",function(){
alert("¡Cliclí una única vez (sin capturar)!");
});

Sí, ya sé que frameworks mastodónticos, como jQuery o Mootools, ya han ideado formas de hacer todo esto, listas para usar, pero ¿qué tiene eso de divertido?


  1. Hay que decirlo: Chrome "se olvida" de los onsubmit de los formularios.
  2. Es tan obvio cuál que, tal cual, obviaré su nombre.
  3. Muy relativo todo, eso sí.

Créditos:

Parser xHTML para Javascript

Basándome en el intérprete de HTML para javascript de John Resig, y aplicándole un par de cambios para hacerlo más adecuado para tratar con XHTML, he acabado haciendo una clase muy útil que a mas de uno os gustará.

Está bajo la Mozilla Public License por exigencia del código empleado.

Métodos:

  • HTMLParser: recibe una cadena con código HTML, y un diccionario (hashtable) con callbacks, al estilo SAX de Java, "start" para el inicio de una etiqueta, "end" para el final de una etiqueta, "chars" para texto y "comment" para comentarios html.
  • escapeXHTML y unescapeXHTML: reciben una cadena de HTML o XHTML y convierte los caracteres no permitidos por el estándar XHTML por sus códigos, y al revés, respectivamente.
  • HTMLtoXML: recibe una cadena de HTML o XHTML, y retorna otra de XHTML, intentando corregir los errores del código como etiquetas sin cerrar.
  • HTMLtoNODE: Recibe una cadena de HTML o XHTML y un nodo, y construye el árbol DOM a partir de la cadena en el nodo dado.
  • HTMLtoDOM: recibe una cadena de HTML o XHTML, y opcionalmente un nodo de tipo documento, y retorna el árbol DOM construído a partir de la cadena.

¿Qué utilidad tiene?

La técnica más usada, a día de hoy, para escribir código HTML en el documento sigue siendo innerHTML. innerHTML nunca fué estándar, es sucio, y tira por suelo la lógica de todo el DOM (Modelo del Objeto Documento, o Document Object Model), que es la forma en la que Javascript de comunica con el intérprete HTML del navegador.

Uno de dichos efectos secundarios es que, mientras construyendo nodos del DOM por javascript cualquier error en la manipulación árbol resulta en un fallo del intérprete, y no llega a reflejarse en el documento, si usamos innerHTML cualquier código malformado puede, y de hecho muy comúnmente sucede, destrozar la página web.

Utilizar un traductor de cadenas de HTML a DOM mediante javascript es una buena solución, y aunque sea menos eficiente que innerHTML, que recordemos está integrado en el navegador, es mucho más fiable y estándar.

Yo lo uso para escribir en el documento todo el HTML recibido con técnicas AJAX, y me ha ahorrado muchos dolores de cabeza con el código malformado.

El código

Podéis descargar este mismo código (6.59 KiB) a o la versión minificada (4.93 KiB).

/*
* Raw xHTML parser
* A pure JavaScript SAX-like HTML/XHTML parser.
*
* Felipe A. Hernández - http://spayder26.blogspot.com
* John Resig - http://ejohn.org/blog/pure-javascript-html-parser/
* Erik Arvidsson - http://erik.eae.net/archives/2004/11/20/12.18.31/
* Licensed under Mozilla Public License
*/
window.rhp=(function(){
/* Raw xHTML parser */
var $def=function(a,b){return typeof((b||window)[a])!="undefined";},
makeMap=function(a){
var r={},b=a.split(",");
for(var i=0,j=b.length;i<j;i++){r[b[i]]=1;}
return r;
},
startTag=/^<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
endTag=/^<\/(\w+)[^>]*>/,
attr=/(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g,
me={},
esc=[["&","&amp;"],[">","&gt;"],["<","&lt;"],["\"","&quot;"],["'","&apos;"]],
esr=[],unesr=[],esl,
getDoc=function(node){
if(node.nodeType==9){return node;}
return (node.ownerDocument||(node.getOwnerDocument&&node.getOwnerDocument())||document);};
esl=esc.length;
for(var i=0;i<esl;i++){esr[i]=new RegExp(esc[i][0],"g");unesr[i]=new RegExp(esc[i][1],"g");}
var empty=makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed"),
block=makeMap("address,applet,blockquote,button,center,dd,del,dir,div,dl,dt,fieldset,form,frameset,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul"),
inline=makeMap("a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var"),
closeSelf=makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr"),
fillAttrs=makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected"),
special=makeMap("script,style");
me.HTMLParser=function(html,handler){
// SAX-like HTML parser
var stack=[];
var parseEndTag=function(tag,tagName){
var pos,i;
if(!tagName){pos=0;}
else{for(pos=stack.length-1;pos>-1;pos--){
if(stack[pos]==tagName){break;}}}
if(pos>=0){
for(i=stack.length-1;i>=pos;i--){
if(handler.end){handler.end(stack[i]);}}
stack.length=pos;}};
var parseStartTag=function(tag,tagName,rest,unary){
if(block[tagName]){
while(stack.last()&&$def(stack.last(),inline)){
parseEndTag("",stack.last());}}
if(closeSelf[tagName]&&stack.last()==tagName){
parseEndTag("",tagName);}
unary=(empty[tagName]||!!unary);
if(!unary){stack.push(tagName);}
if(handler.start){
var attrs=[];
rest.replace(attr,
function(match,name){
var value=arguments[2]?arguments[2]:(arguments[3]?arguments[3]:(arguments[4]?arguments[4]:(fillAttrs[name]?name:"")));
attrs.push({
name:name,
value:value,
escaped:value.replace(/(^|[^\\])"/g,'$1\\\"')});});
if(handler.start){handler.start(tagName,attrs,unary);}}},
last=html,index,chars,match;
stack.last=function(){return (this.length)?this[this.length-1]:null;};
while(html){
chars=true;
if(!stack.last()||!special[stack.last()]){
if(html.indexOf("<!--")===0){
index=html.indexOf("-->");
if(index>=0){
if(handler.comment){handler.comment(html.substring(4,index));}
html=html.substring(index+3);chars=false;}}
else if(html.indexOf("</")===0){
match=html.match(endTag);
if(match){html=html.substring(match[0].length);
match[0].replace(endTag,parseEndTag);
chars=false;}}
else if(html.indexOf("<")===0){
match=html.match(startTag);
if(match){
html=html.substring(match[0].length);
match[0].replace(startTag,parseStartTag);
chars=false;}}
if(chars){
index=html.indexOf("<");
var text=(index<0)?html:html.substring(0,index);
html=(index<0)?"":html.substring(index);
if(handler.chars){handler.chars(text);}}}
else{
html=html.replace(new RegExp("(.*)<\/"+stack.last()+"[^>]*>"),
(function(){
return function(all,text){
text=text.replace(/<!--(.*?)-->/g,"$1").replace(/<!\[CDATA\[(.*?)\]\]>/g,"$1");
if(handler.chars){handler.chars(text);}
return "";};})());
parseEndTag("",stack.last());}
if(html==last){throw "Parse Error: "+html;}
last=html;}
parseEndTag();};
me.escapeXHTML=function(html){
// Escape XHTML characters
for(var i=0;i<esl;i++){
html=html.replace(esr[i],esc[i][1]);}
return html;};
me.unescapeXHTML=function(html){
// Unescape XHTML characters
for(var i=esl-1;i>-1;i--){
html=html.replace(unesr[i],esc[i][0]);}
return html;};
me.HTMLtoXML=function(html){
// Parses HTML string to XML string (XHTML)
var results="";
me.HTMLParser(html,{
start:function(tag,attrs,unary){
results+="<"+tag.toLowerCase();
for(var i=0,il=attrs.length;i<il;i++){
results+=" "+attrs[i].name.toLowerCase()+'="'+attrs[i].escaped+'"';}
results+=(unary?"/":"")+">";},
end:function(tag){
results+="</"+tag.toLowerCase()+">";},
chars:function(text){results+=text;},
comment:function(text){results+="<!--"+text+"-->";}});
return results;};
me.HTMLtoNODE=function(html,node){
// Writes an HTML string to DOM's node
var elems=[node],doc=getDoc(node),cur=node;
me.HTMLParser(html,{
start:function(tagName,attrs,unary){
var elem=doc.createElement(tagName);
for(var i=0,il=attrs.length;i<il;i++){
elem.setAttribute(attrs[i].name,me.unescapeXHTML(attrs[i].value));}
if(cur&&cur.appendChild){
cur.appendChild(elem);}
if(!unary){elems.push(elem);cur=elem;}},
end:function(tag){
elems.length-=1;
cur=elems[elems.length-1];},
chars:function(text){
cur.appendChild(doc.createTextNode(me.unescapeXHTML(text)));},
comment:function(text){
if(doc.createComment){cur.appendChild(doc.createComment(text));}}});};
me.HTMLtoDOM=function(html,doc){
// HTML string to DOM's document node
if(!doc){doc=document;}
else{doc=getDoc(doc);}
var n=doc.createElement("div");
me.HTMLtoNODE(html,n);
return n.childNodes;};
return me;}());

Créditos