El alias de función de JavaScript no parece funcionar
Estaba leyendo esta pregunta y quería probar el método de alias en lugar del método de contenedor de funciones, pero parece que no pude hacerlo funcionar ni en Firefox 3 ni en 3.5beta4, ni en Google Chrome, tanto en sus ventanas de depuración como en en una página web de prueba.
Insecto de fuego:
>>> window.myAlias = document.getElementById
function()
>>> myAlias('item1')
>>> window.myAlias('item1')
>>> document.getElementById('item1')
<div id="item1">
Si lo pongo en una página web, la llamada a myAlias me da este error:
uncaught exception: [Exception... "Illegal operation on WrappedNative prototype object" nsresult: "0x8057000c (NS_ERROR_XPC_BAD_OP_ON_WN_PROTO)" location: "JS frame :: file:///[...snip...]/test.html :: <TOP_LEVEL> :: line 7" data: no]
Chrome (con >>> insertados para mayor claridad):
>>> window.myAlias = document.getElementById
function getElementById() { [native code] }
>>> window.myAlias('item1')
TypeError: Illegal invocation
>>> document.getElementById('item1')
<div id=?"item1">?
Y en la página de prueba, aparece la misma "Invocación ilegal".
¿Estoy haciendo algo mal? ¿Alguien más puede reproducir esto?
Además, por extraño que parezca, lo intenté y funciona en IE8.
Profundicé para comprender este comportamiento en particular y creo que encontré una buena explicación.
Antes de explicar por qué no puedes usar alias document.getElementById
, intentaré explicar cómo funcionan las funciones/objetos de JavaScript.
Siempre que invoca una función de JavaScript, el intérprete de JavaScript determina un alcance y se lo pasa a la función.
Considere la siguiente función:
function sum(a, b)
{
return a + b;
}
sum(10, 20); // returns 30;
Esta función se declara en el alcance de la ventana y cuando la invoque, el valor this
dentro de la función de suma será el Window
objeto global.
Para la función 'suma', no importa cuál sea el valor de 'esto' ya que no lo está usando.
Considere la siguiente función:
function Person(birthDate)
{
this.birthDate = birthDate;
this.getAge = function() { return new Date().getFullYear() - this.birthDate.getFullYear(); };
}
var dave = new Person(new Date(1909, 1, 1));
dave.getAge(); //returns 100.
Cuando llama a la función dave.getAge, el intérprete de JavaScript ve que está llamando a la función getAge en el dave
objeto, por lo que establece this
y dave
llama a la getAge
función. getAge()
volverá correctamente 100
.
Quizás sepas que en JavaScript puedes especificar el alcance utilizando el apply
método. Intentemos eso.
var dave = new Person(new Date(1909, 1, 1)); //Age 100 in 2009
var bob = new Person(new Date(1809, 1, 1)); //Age 200 in 2009
dave.getAge.apply(bob); //returns 200.
En la línea anterior, en lugar de dejar que JavaScript decida el alcance, está pasando el alcance manualmente como bob
objeto. getAge
ahora regresará 200
aunque "pensaste" que habías llamado getAge
al dave
objeto.
¿Cuál es el punto de todo lo anterior? Las funciones están adjuntas "libremente" a sus objetos JavaScript. Por ejemplo, puedes hacer
var dave = new Person(new Date(1909, 1, 1));
var bob = new Person(new Date(1809, 1, 1));
bob.getAge = function() { return -1; };
bob.getAge(); //returns -1
dave.getAge(); //returns 100
Demos el siguiente paso.
var dave = new Person(new Date(1909, 1, 1));
var ageMethod = dave.getAge;
dave.getAge(); //returns 100;
ageMethod(); //returns ?????
ageMethod
¡La ejecución arroja un error! ¿Qué pasó?
Si lee atentamente los puntos anteriores, notará que dave.getAge
el método se llamó dave
como this
objeto, mientras que JavaScript no pudo determinar el "alcance" de ageMethod
la ejecución. Entonces pasó la 'Ventana' global como 'esta'. Ahora como window
no tiene una birthDate
propiedad, ageMethod
la ejecución fallará.
¿Cómo arreglar esto? Simple,
ageMethod.apply(dave); //returns 100.
¿Todo lo anterior tuvo sentido? Si es así, podrá explicar por qué no puede utilizar un alias document.getElementById
:
var $ = document.getElementById;
$('someElement');
$
se llama con window
as this
y si getElementById
se espera que la implementación this
sea document
, fallará.
Nuevamente para solucionar este problema, puedes hacer
$.apply(document, ['someElement']);
Entonces, ¿por qué funciona en Internet Explorer?
No conozco la implementación interna getElementById
en IE, pero un comentario en la fuente jQuery ( inArray
implementación del método) dice que en IE window == document
. Si ese es el caso, entonces el alias document.getElementById
debería funcionar en IE.
Para ilustrar esto aún más, he creado un ejemplo elaborado. Eche un vistazo a la Person
función a continuación.
function Person(birthDate)
{
var self = this;
this.birthDate = birthDate;
this.getAge = function()
{
//Let's make sure that getAge method was invoked
//with an object which was constructed from our Person function.
if(this.constructor == Person)
return new Date().getFullYear() - this.birthDate.getFullYear();
else
return -1;
};
//Smarter version of getAge function, it will always refer to the object
//it was created with.
this.getAgeSmarter = function()
{
return self.getAge();
};
//Smartest version of getAge function.
//It will try to use the most appropriate scope.
this.getAgeSmartest = function()
{
var scope = this.constructor == Person ? this : self;
return scope.getAge();
};
}
Para la Person
función anterior, así es como getAge
se comportarán los distintos métodos.
Creemos dos objetos usando Person
la función.
var yogi = new Person(new Date(1909, 1,1)); //Age is 100
var anotherYogi = new Person(new Date(1809, 1, 1)); //Age is 200
console.log(yogi.getAge()); //Output: 100.
Sencillo, el método getAge obtiene yogi
el objeto como this
y genera 100
.
var ageAlias = yogi.getAge;
console.log(ageAlias()); //Output: -1
El intérprete de JavaScript establece window
el objeto como this
y nuestro getAge
método devolverá -1
.
console.log(ageAlias.apply(yogi)); //Output: 100
Si configuramos el alcance correcto, puede usar ageAlias
el método.
console.log(ageAlias.apply(anotherYogi)); //Output: 200
Si pasamos algún objeto de otra persona, seguirá calculando la edad correctamente.
var ageSmarterAlias = yogi.getAgeSmarter;
console.log(ageSmarterAlias()); //Output: 100
La ageSmarter
función capturó el objeto original this
, por lo que ahora no tiene que preocuparse por proporcionar el alcance correcto.
console.log(ageSmarterAlias.apply(anotherYogi)); //Output: 100 !!!
El problema ageSmarter
es que nunca puedes establecer el alcance de algún otro objeto.
var ageSmartestAlias = yogi.getAgeSmartest;
console.log(ageSmartestAlias()); //Output: 100
console.log(ageSmartestAlias.apply(document)); //Output: 100
La ageSmartest
función utilizará el alcance original si se proporciona un alcance no válido.
console.log(ageSmartestAlias.apply(anotherYogi)); //Output: 200
Aún podrás pasar otro Person
objeto a getAgeSmartest
. :)
Tienes que vincular ese método al objeto del documento. Mirar:
>>> $ = document.getElementById
getElementById()
>>> $('bn_home')
[Exception... "Cannot modify properties of a WrappedNative" ... anonymous :: line 72 data: no]
>>> $.call(document, 'bn_home')
<body id="bn_home" onload="init();">
Cuando estás creando un alias simple, la función se llama en el objeto global, no en el objeto del documento. Utilice una técnica llamada cierres para solucionar este problema:
function makeAlias(object, name) {
var fn = object ? object[name] : null;
if (typeof fn == 'undefined') return function () {}
return function () {
return fn.apply(object, arguments)
}
}
$ = makeAlias(document, 'getElementById');
>>> $('bn_home')
<body id="bn_home" onload="init();">
De esta manera no pierdes la referencia al objeto original.
En 2012, existe el nuevo bind
método de ES5 que nos permite hacer esto de una manera más sofisticada:
>>> $ = document.getElementById.bind(document)
>>> $('bn_home')
<body id="bn_home" onload="init();">
Esta es una respuesta corta.
Lo siguiente hace una copia de (una referencia a) la función. El problema es que ahora la función está en el window
objeto cuando fue diseñada para vivir en el document
objeto.
window.myAlias = document.getElementById
Las alternativas son
- utilizar un envoltorio (ya mencionado por Fabien Ménager)
o puedes usar dos alias.
window.d = document // A renamed reference to the object window.d.myAlias = window.d.getElementById