Estudiando el diseño de jQuery paso a paso

12 Sep 2011

Introducción

Hace algún tiempo repasamos algunos de los patrones de diseño modulares más importantes que podemos encontrar actualmente en Javascript. Vimos el patrón del módulo tradicional, el revelado, el proxy y algunas variaciones.

Todas estas estructuras tienen como fin el agrupar una serie de bloques de código con funcionalidades compartidas permitiendo así su reutilización y portabilidad entre proyectos. Es por ejemplo el sistema con el que se articulan bibliotecas como jQuery, ZeptoJS o Mootools por ejemplo.

Gracias a estos patrones, podemos crear nuestros propios frameworks o aplicaciones cuyo código no interfiera con el de terceros. Se trata en definitiva de buenas prácticas que siempre es conveniente conocer.

Como cualquier otro patrón de diseño, la definición de estos módulos está sujeta a continua revisión y resulta frecuente encontrar variantes interesantes que mejoran su arquitectura base, su legibilidad, el rendimiento o cualquier otro parámetro ya sea mesurable o simplemente estético.

En esta ocasión, buscando nuevas reformulaciones, vamos a analizar el sistema que articula a la que posiblemente sea la biblioteca Javascript más popular en la actualidad: jQuery.

NOTA: A continuación analizaremos el código de la versión 1.6.3 de jQuery la cual es, durante la redacción de este artículo, la última disponible.

El patrón de módulo jQuery

Actualmente, jQuery utiliza la siguiente estructura del código:

(function( window, undefined ) {
 
  var jQuery = (function() {
    // Code goes here...
  })();
 
  window.jQuery = window.$ = jQuery;
 
})(window);

Pasemos a diseccionarla paso a paso para ver cómo funciona:

El marco de trabajo principal

La estructura se enmarca en una función autoejecutable que crea un namespace propio. Ésto, permite encapsular todo el contenido y aislarlo de cualquier otro script para evitar problemas de colisión o sobreescritura de variables:

(function( window, undefined ) {
  // ...
})(window);

Como ya explicamos en en su momento, aquí lo importante es que a la función autoejecutable se le pasa un solo argumento (window) mientras que recibe dos (el mismo window y undefined). El porqué de esto responde a una cuestión de rendimiento en el primer caso y a una de seguridad en el segundo: cachear el objeto window en el interior de nuestro script permite que cada vez que lo necesitemos, el intérprete no tiene que subir varios niveles hasta alcanzarlo. Con respecto a undefined, al no recibir ningún argumento, su valor se establece efectivamente como tal (sin definir) previniéndonos contra una hipotética sobreescritura anterior de esta primitiva.

Yendo un poco más lejos, en nuestro script es muy posible que en algún momento queramos interactuar con el DOM, conocer la versión del navegador para algún tipo de filtrado, o incluso extraer toda la información posible relativa a la URL del documento que ejecuta el script. Para facilitar el acceso a estos datos, jQuery los almacena en forma de variables para poder acceder rápidamente a su contenido en cualquier momento:

(function( window, undefined ) {
 
  var document  = window.document,
      navigator = window.navigator,
      location  = window.location;
 
})(window);

Con esto, cuando necesitemos utilizar alguno de los objetos anteriores, el intérprete dispondrá de ellos rápidamente sin la necesidad de ascender niveles o volver a recorrer al objeto padre.

Definiendo el core

Una vez creado el marco general y con algunas variables útiles ya declaradas, lo siguiente es crear la función principal que contendrá el núcleo o core de nuestro script. Para ello, utilizaremos una función declarada cuyo nombre se corresponderá con el de la biblioteca en la que trabajamos:

(function( window, undefined ) {
 
  var document  = window.document,
      navigator = window.navigator,
      location  = window.location;
 
  var jQuery = (function(){
    // Core goes here...
  })();
 
})(window);

Dentro de esta función es donde se definirán los métodos públicos (la API pública) que tendremos disponibles tras su inicialización. Estos métodos deben definirse a su vez en un objeto interno que debe ser devuelto mediante el comando return al ámbito de nuestra biblioteca. Llamaremos a este objeto intermedio core:

(function( window, undefined ) {
 
  var document  = window.document,
      navigator = window.navigator,
      location  = window.location;
 
  var jQuery = (function(){
 
    var core = {
      // Public methods goes here...
    };
 
    return core;
 
  })();
 
})(window);

Ese nuevo objeto core, como hemos comentado más arriba, guardará los métodos públicos de nuestra biblioteca mediante una notación simple de nombre y función a ejecutar:

(function( window, undefined ) {
 
  var document  = window.document,
      navigator = window.navigator,
      location  = window.location;
 
  var jQuery = (function(){
 
    var core = {
 
      publicMethod : function( subject ){
        // publicMethod action goes here...
      }
 
    };
 
    return core;
 
  })();
 
})(window);

Con esta estructura, conseguimos que el objeto jQuery devuelto por return, implemente la definición de los métodos que hemos declarado.

Creando los métodos de nuestra biblioteca

Para dar cierta funcionalidad a nuestra biblioteca, incluyamos dentro del core un método trim para eliminar los espacios en blanco a izquierda y derecha de una cadena dada:

(function( window, undefined ) {
 
  var document  = window.document,
      navigator = window.navigator,
      location  = window.location;
 
  var jQuery = (function(){
 
    var core = {
 
      trim : function( subject ){
        return subject.replace(/^\s+|\s+$/g, '');
      }
 
    };
 
    return core;
 
  })();
 
})(window);

En el código anterior, dentro de la variable jQuery tendremos ahora un objeto con nuestro nuevo método. Para poder utilizarlo, necesitaríamos devolver dicho objeto jQuery al ámbito global.

Salvado ese punto, solo sería cuestión de añadir más métodos al core hasta cubrir todas las necesidades o especificaciones de la aplicación.

Devolviendo la biblioteca al ámbito global

Como hemos comentado en el último ejemplo, la variable jQuery contiene el objeto con los métodos que hemos definido por lo que, si queremos asociar ese objeto al contexto global, únicamente necesitamos añadirlo al window (que siempre se corresponde con el nivel o ámbito más alto):

(function( window, undefined ) {
 
  var document  = window.document,
      navigator = window.navigator,
      location  = window.location;
 
  var jQuery = (function(){
 
    var core = {
 
      trim : function( subject ){
        return subject.replace(/^\s+|\s+$/g, '');
      }
 
    };
 
    return core;
 
  })();
 
  window.jQuery = jQuery;
 
})(window);

Ahora, el contexto global (window) cuenta con una nueva variable que en realidad contiene al objeto que hemos creado anteriormente. Como tal, posee aquellos métodos que definimos en el core y que son accesibles de forma pública mediante la notación tradicional:

jQuery.trim( '  Hello World    '); // "Hello World"

Nuestra biblioteca funciona como se espera; no ha sido tan difícil! Pero como observación, rápidamente vemos que todos sus métodos son públicos. ¿Cómo podemos implementar métodos privados dentro de este esquema?

Métodos privados en el patrón jQuery

Para crear métodos que solo sean visibles (accesibles) desde el interior de nuestra biblioteca, tenemos que definirlos fuera del objeto core.

Implementemos en nuestra biblioteca anterior el sistema para comprobar tipos que tratamos en un artículo anterior: la función toType:

var toType = function(obj) {
  return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}

Si queremos que esta utilidad solo sea accesible por los métodos de nuestra biblioteca sin que pertenezca a la API pública, podemos crear un nuevo objeto private que la contenga (idea original de @acido69):

(function( window, undefined ) {
 
  var document  = window.document,
      navigator = window.navigator,
      location  = window.location;
 
  var private = {
    toType : function(obj) {
      return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
    }
  }
 
  var jQuery = (function(){
 
    var core = {
 
      trim : function( subject ){
        return subject.replace(/^\s+|\s+$/g, '');
      }
 
    };
 
    return core;
 
  })();
 
  window.jQuery = jQuery;
 
})(window);

Como este nuevo objeto está fuera del jQuery, no se asocia al window y, por tanto, no llega nunca al ámbito global: permanecerá así como un método privado de nuestra biblioteca.

Para utilizar sus métodos dentro de nuestro script, bastará con llamarlo del modo tradicional ya que comparten scope:

(function( window, undefined ) {
 
  var document  = window.document,
      navigator = window.navigator,
      location  = window.location;
 
  var private = {
    toType : function(obj) {
      return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
    }
  }
 
  var jQuery = (function(){
 
    var core = {
 
      trim : function( subject ){
        if( private.toType( subject ) == 'string' )
          return subject.replace(/^\s+|\s+$/g, '');
        else
          return 'Object must be a String!';
      }
 
    };
 
    return core;
 
  })();
 
  window.jQuery = jQuery;
 
})(window);

Lo comprobamos rápidamente:

console.log( jQuery.trim( '   Hello World  ' ) ); // "Hello World"
console.log( jQuery.trim( [1,2] ) ); // Object must be a String!
console.log( jQuery.toType( [1,2] ) ); // TypeError: jQuery.toType is not a function

Como podemos ver, toType funciona cuando se llama desde el interior de la biblioteca pero lógicamente devuelve error si tratamos de acceder desde fuera. Hemos conseguido así restringir su visibilidad y limitar su acción exclusivamente al interior del script.

Asociando un álias

Finalmente, jQuery permite utilizar el caracter $ como un álias para invocar sus métodos:

jQuery.trim('foo');
$.trim('foo');

Ambas formas son idénticas. Para asociar un álias a nuestra biblioteca, basta con añadir el nombre escogido al objeto window apuntando al valor al de la biblioteca en sí. Cambiamos únicamente la última parte de nuestro patrón:

window.jQuery = window.$ = jQuery;

Con este último paso, tenemos lista y funcional nuestra biblioteca utilizando una estructura similar a la que presenta jQuery.

Conclusión

En el desarrollo moderno de aplicaciones Javascript, es frecuente recurrir a los patrones de diseño para garantizar una arquitectura sólida y fiable. Tradicionalmente, hemos contado con algunos ejemplos muy interesantes como son el conocido patrón módulo, el módulo revelado o el elegante “This namespaces proxy” de James Edwards.

Profundizando en estos ejemplos, jQuery ha desarrollado un patrón propio que ha demostrado un alto rendimiento con una elegante estructura. En definitiva, se trata de una función autoejecutable que en último término asocia un nuevo objeto al contexto global del entorno de ejecución. Dicho objeto cuenta con todos los métodos públicos (el API) que queremos ofrecer al usuario (o a terceros) permitiéndonos además, restringir cómodamente la visibilidad de aquellas funcionalidades que queramos definir como privadas.

Un patrón sin duda muy interesante que bien vale un estudio.

Más:

{12} Comentarios.

  1. pedro

    siempre hecho de menos en estos patrones JavaScript las atributos y métodos protegidos (Protected).

  2. fcodiaz

    me encantan estos pos tuyos, sobretodo por que nos muestras como funcionan las cosas internamente en jQuery y nos das la pauta a construir nuevas cosas y no como muchos otros que ya quieren solo llamar e impelementar metodos

  3. Martín

    hola, muy buena explicación me ha servido mucho ,pero, podrías explicar como pasarle un aprametro directamente a $().trim() ? como hace jQuery para apsarle un id por ejemplo, gracias!

  4. yeikos

    Sería algo así…

    (function (window, undefined) {
      var document = window.document,
        navigator = window.navigator,
        location = window.location;
    
      var jQuery = (function () {
        var core = function (argv) {
            return new core.fn.init(argv);
          };
    
        core.fn = core.prototype = {
          constructor: core,
          init: function (argv) {
            jQuery.argv = argv;
            return jQuery;
          }
        };
    
        core.trim = function (str) {
          if (core.argv) str = core.argv;
          return str.toString().replace(/^\s*/, '').replace(/\s*$/, '');
        };
    
        return core;
    
      })();
      window.jQuery = window.$ = jQuery;
    
    })(window);
    
    console.log($('   example1   ').trim());
    console.log($.trim('    example2   '));
    
  5. Martín

    Muchas gracias!!

  6. Fran

    Hola,

    Ante todo felicidades por la web y por los interesantes Post que sigo recientemente. Ante el artículo y posterior comentario de “yeikos” para pasar parámetros se me plantean algunas dudas que estoy tratando de resolver. A la hora de utilizar la forma de “yeikos” para pasar parámetros no consigo implementar directamente los métodos en el jQuery como ocurre en el ejemplo del artículo:

    var jQuery = (function(){
        var core = {
          trim : function( ){},
          otro:....,
          otro_mas:....
        };
        return core;
     })();
    

    Así que lo único que he logrado es extender sus métodos tal que así:

    var jQuery = (function () {
        var core = function (argv) {
            return new core.fn.init(argv);
          };
        core.fn = core.prototype = {
          constructor: core,
          init: function (argv) {
            jQuery.argv = argv;
            return jQuery;
          }
        };
        core.extend = core.prototype = {
            extend: function(el, opt)
            {          
    	for (var name in opt)
    	{
    	       el[name] = opt[name];
    	}
    	return el;				
           },
           trim : function( ){};
        };
        return core;
    })();
    
    window.jQuery.extend.extend( window.jQuery, window.jQuery.prototype );
    

    Esta forma funciona perfectamente, pero… os parece una buena manera o existe otra mejor???

    Un saludo y gracias.

  7. yeikos

    No tiene mucho sentido lo que estás haciendo, guíate por lo que escribí y empieza a desarrollar a partir de ahí. Hay más métodos de hacerlo, pero generalmente lo mejor es crear una nueva instancia, de esta manera aislamos la llamada a la función de cualquier otra, así podremos salvar el valor pasado como argumento.

  8. David

    Muy bueno el artículo, sobre todo por lo resumido y concreto.

    PDT: Hace falta el +1 de google ¿o está y no lo vi?, yo se lo daría al artículo.

    Gracias

    Saludos

  9. daniel

    muy bueno el articulo, y muy simple la explicación.

    Ahora , me gustaría saber que significa ‘return ({})’ cuando explicas sobre como hacer un método privado.

    • Carlos Benítez

      Hola;
      el return ({}), que puede parecer un poco raro a simple vista, es solo un ‘shortcut’, un atajo; sería idéntico a:

      return Object
      

      De hecho, es crear ese Object anterior mediante la notación literal (en Javascript {}) y, para que el intérprete lo evalúe como una expresión válida, encerrado entre paréntesis.
      Saludos!

  10. jorge

    El estudio de los patrones de diseño y su implementación son lo que terminan volviéndonos profesionales (o por lo menos muy hábiles) en programación sobre cualquier lenguaje
    Es una lastima que la mayoría de la información útil se encuentre en ingles 🙁
    Gracias por el artículo (aunque personalmente me tiro mas a PHP XD )

  11. Edgar

    Estupendo como siempre!!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *