¿Que es lo que hace: jQuery.noConflict()?


De vez en cuando me gusta leer el código de alguna buena librería para descubrir cosas interesantes. El código de jQuery es de esos que contiene un buen número de detalles de los que se puede aprender bastante. Es un código atractivo y más sencillo de seguir de lo que podría imaginarse.
Hoy, tras una conversación reciente, me quiero centrar en la implementación de la función noConflict()

El problema

Ya desde su versión 1.0, John Resig -creador de jQuery- tuvo muy clara una idea en la arquitectura de la librería: Causar la mínima polución de nombres posible. ¿Por qué? Por aquel entonces, la librería más popular -PrototypeJS- había seguido el camino de crear las funciones globales que creyó necesarias y de añadir propiedades y métodos a los objetos nativos de Javascript. Entre esas funciones globales, lo que más llamaba la atención era el uso de alias como $$H o $F$ es un nombre muy corto que enseguida se hizo popular. Claro, otras librerías decidieron usar ese mismo alias. Luego, cuando los desarrolladores-usuarios querían incluir en su página un determinado elemento hecho con una librería y otro hecho con otra distinta... Colisión de nombres y problemas sin fin.
Pero claro, por otra parte el alias $ es realmente apetecible por su comodidad. Así que en jQuery, buscando dar a sus usuarios la mayor flexibilidad posible para utilizar su librería junto a otras, se decidió usar el alias pero a la vez proporcionar un medio de liberar ese alias, la función noConflict().

Uso de noConflict()

El uso de noConflict() es sencillo. Cargamos jQuery, lo que define el alias $. Luego usamos noConflict() y eso libera el alias, dejando disponible el que hubiera antes en caso de que lo hubiera.
No hay mucho más que decir, más que nada porque ya está dicho en muchos sitios. Incluso en la propia documentaciónoficial de jQuery.

Pero, ¿cómo funciona?

En realidad es bastante sencillo. Todo se basa en el estricto seguimiento por parte de los desarrolladores del principio de encapsulación de alcance. Veámoslo directamente sobre el código. Lo que sigue es el código de jQuery 1.4.4. pero sólo las partes implicadas, claro:
(function( window, undefined ) {

    // Use the correct document accordingly with window argument (sandbox)
    var document = window.document;
    var jQuery = (function() {
        // Define a local copy of jQuery
        var jQuery = function( selector, context ) {
            // The jQuery object is actually just the init constructor 'enhanced'
            return new jQuery.fn.init( selector, context );
        }

        // Map over jQuery in case of overwrite
        _jQuery = window.jQuery
        // Map over the $ in case of overwrite
        _$ = window.$

        jQuery.fn = jQuery.prototype = {
            // The current version of jQuery being used
            jquery: "1.4.4"
        };
        jQuery.extend = jQuery.fn.extend = function() {
            // ...
        };

        jQuery.extend({
            noConflict: function( deep ) {
                window.$ = _$;
                if ( deep ) {
                    window.jQuery = _jQuery;
                }
                return jQuery;
            }
        });
        // Expose jQuery to the global object
        return (window.jQuery = window.$ = jQuery);
    })();
})(window);
Lo primero es que todo el código de la librería está contenido dentro de una función anónima a la que se llama de forma inmediata, recibiendo los valores de window y de undefined. Con esto lo que se logra es que, si seguimos la recomendable práctica de declarar nuestras variables localmente en el entorno que corresponde (usando var) evitaremos causar cualquier conflicto en el alcance global del intérprete. El recibir undefined se hace con un objetivo simétrico: evitar recibir valores de undefined que vengan alterados por otros scripts. window se recibe para obtener una referencia local (más rápido) y localizar la referencia al entorno global en 1 único sitio ( ver explicación en el comentario más abajo )
Una vez nos encontramos en ese entorno, se declara una variable llamada jQuery. Es importante notar que esta variable es local, vive únicamente dentro de este alcance. Y no va a salir de ahí. Definimos ahí un nuevo alcance y dentro de él es donde se va a desarrollar todo el contenido de jQuery. De hecho, es en ese nuevo alcance donde se define otrajQuery local y sobre ella se trabaja, añadiéndole todas las funciones que usamos habitualmente.
En ese mismo alcance donde está definiendo jQuery, se da el primer paso necesario para el funcionamiento denoConflict(): Se captura el (posible) valor existente de $. Fijémonos porque es interesante:
// Map over the $ in case of overwrite
_$ = window.$
Para acceder al $ global, lo hacemos explícitamente a través de window. Ah, y lo mismo se hace con el propio nombrejQuery. Bien, ambos valores se guardan creando sobre ellos una closure. Ahí estarán disponibles para cuando los necesitemos.
Tras crear todas las funciones y objetos necesarios, la forma de hacer disponibles tanto jQuery como $ en el alcance global, no es simplemente devolver los objetos in más, sino que se exponen explícitamente sobre el objeto window:
return (window.jQuery = window.$ = jQuery);
Ahora, después de haber cargado y construido jQuery, es cuando podemos llamar a noConflict(). ¿Qué es lo que ocurre cuando lo hacemos? Sencillo:
            noConflict: function( deep ) {
                window.$ = _$;
                if ( deep ) {
                    window.jQuery = _jQuery;
                }
                return jQuery;
            }
Por una parte, volvemos a exponer como window.$ el objeto que habíamos capturado inicialmente en _$. Si lo pedimos, se hace también lo mismo con el nombre jQuery. La liberación de $ es interesante para cualquier librería que lo use, la de jQuery permite la convivencia con otras versiones de la propia librería. Además, por si el usuario lo prefiere devolvemos también el objeto jQuery.

Ejemplo

Si vemos el siguiente ejemplo:
<!DOCTYPE HTML >
<html>
<head>
<title>Muchas jQueries!</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js" ></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" ></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js" ></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js" ></script>
<script type="text/javascript">
    var jqs = {};
    jqs.jq144 = jQuery.noConflict(true);
    jqs.jq143 = jQuery.noConflict(true);
    jqs.jq142 = jQuery.noConflict(true);
    jqs.jq141 = jQuery.noConflict(true);

    jqs.jq144(function($) {
        $("#out").html("141: " + jqs.jq141().jquery + "<br/>142: " + jqs.jq142().jquery +"<br/>143: " + jqs.jq143().jquery +"<br/>144: " + jqs.jq144().jquery );
    });
</script>
</head>
<body>
<div id="out"></div>
</body>
</html>
Tras haber cargado las 4 versiones de jQuery, lo que tenemos es algo como esto:
jQuery {
    version 1.4.4
    closure ( _jQuery = jQuery {
        version 1.4.3
        closure ( _jQuery = jQuery {
            version 1.4.2
            closure ( _jQuery = jQuery {
                version 1.4.1
            })
        })
    })
}
Con la primera llamada a jQuery.noConflict() lo que hacemos es devolver el actual jQuery y asignar el anterior (_jQuery) a window.jQuery. Así queda nuestra estructura:
    jQuery {
        version 1.4.3
        closure ( _jQuery = jQuery {
            version 1.4.2
            closure ( _jQuery = jQuery {
                version 1.4.1
            })
        })
    }
Algo similar se va haciendo con $. Cuando llamamos por segunda vez... Realmente estamos llamando alnoConflict() del objeto que está ahora en jQuery, que es la versión 1.4.3. Es decir, no es -aunque lo parezca- la misma llamada que antes. Se llama igual, pero es una función de otro objeto.

Conclusiones

jQuery utiliza una estructura muy definida para evitar por todos los medios crear conflictos de colisiones con otras librerías o incluso con otras versiones de ella misma. Además, proporciona la función noConflict() como herramienta fundamental para solucionar esas colisiones.

No hay comentarios:

Publicar un comentario