La función de alias de JavaScript no parece funcionar

85

Estaba leyendo esta pregunta y quería probar el método de alias en lugar del método de envoltura de función, pero parece que no pude hacerlo funcionar en Firefox 3 o 3.5beta4, o Google Chrome, tanto en sus ventanas de depuración como en una página web de prueba.

Firebug:

>>> 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 >>> insertado 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, obtengo 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.

Kev
fuente
1
en IE8, podría ser algún tipo de función mágica o algo así.
Maciej Łebkowski
Agregué comentarios a las respuestas incorrectas en stackoverflow.com/questions/954417 y los dirigí aquí.
Grant Wagner
1
Para navegadores que admiten el método Function.prototype.bind: window.myAlias ​​= document.getElementById.bind (document); Busque en los documentos de MDN, habrá una corrección de enlace.
Ben Fleming

Respuestas:

39

Tienes que vincular ese método al objeto del documento. Mira:

>>> $ = 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á haciendo 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 forma no perderá la referencia al objeto original.

En 2012, existe el nuevo bindmétodo de ES5 que nos permite hacer esto de una manera más elegante:

>>> $ = document.getElementById.bind(document)
>>> $('bn_home')
<body id="bn_home" onload="init();">
Maciej Łebkowski
fuente
4
Lo cual es realmente un contenedor porque devuelve una nueva función. Creo que la respuesta a la pregunta de Kev es que no es posible por las razones que describiste anteriormente. El método que describe aquí es probablemente la mejor opción.
jiggy
Gracias por tu comentario, no había mirado lo suficiente para darme cuenta de eso. Entonces, ¿la sintaxis en las respuestas a la otra pregunta es completamente falsa? Interesante ...
Kev
188

Investigué profundamente para comprender este comportamiento en particular y creo que he encontrado una buena explicación.

Antes de explicar por qué no puede crear un 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 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 ámbito de la ventana y cuando la invoca, el valor de thisdentro de la función de suma será el Windowobjeto 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 se llama a la función dave.getAge, el intérprete de JavaScript ve que está llamando la función getAge en el daveobjeto, por lo que pone thisa davey llama a la getAgefunción. getAge()regresará correctamente 100.


Es posible que sepa que en JavaScript puede especificar el alcance utilizando el applymétodo. Probemos 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 bobobjeto. getAgeahora regresará 200a pesar de que "pensó" que llamó getAgeal daveobjeto.


¿Cuál es el punto de todo lo anterior? Las funciones se adjuntan "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! ¿Que pasó?

Si usted lee mis puntos por encima con cuidado, que le cuenta que dave.getAgeel método fue llamado con davecomo thisobjeto mientras que JavaScript no pudo determinar el 'alcance' de ageMethodejecución. Así que pasó la 'Ventana' global como 'esto'. Ahora windowque no tiene una birthDatepropiedad, la ageMethodejecución fallará.

¿Cómo arreglar esto? Sencillo,

ageMethod.apply(dave); //returns 100.

¿Todo lo anterior tenía sentido? Si es así, entonces podrá explicar por qué no puede usar un alias document.getElementById:

var $ = document.getElementById;

$('someElement'); 

$se llama con windowtan thisy si getElementByIdla aplicación está a la espera thisde ser document, se producirá un error.

Nuevamente para solucionar esto, puede hacer

$.apply(document, ['someElement']);

Entonces, ¿por qué funciona en Internet Explorer?

No sé la implementación interna de getElementByIden IE, pero un comentario en la fuente de jQuery ( inArrayimplementación del método) dice que en el IE, window == document. Si ese es el caso, entonces el aliasing document.getElementByIddebería funcionar en IE.

Para ilustrar esto aún más, he creado un ejemplo elaborado. Eche un vistazo a la Personfunció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 Personfunción anterior, así es como getAgese comportarán los distintos métodos.

Creemos dos objetos usando la Personfunció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.

Directo, el método getAge obtiene el yogiobjeto como thisy la salida 100.


var ageAlias = yogi.getAge;
console.log(ageAlias()); //Output: -1

El intérprete de JavaScript establece el windowobjeto como thisy nuestro getAgemétodo regresará -1.


console.log(ageAlias.apply(yogi)); //Output: 100

Si configuramos el alcance correcto, puede usar el ageAliasmétodo.


console.log(ageAlias.apply(anotherYogi)); //Output: 200

Si pasamos algún objeto de otra persona, aún calculará la edad correctamente.

var ageSmarterAlias = yogi.getAgeSmarter;    
console.log(ageSmarterAlias()); //Output: 100

La ageSmarterfunción capturó el thisobjeto original, por lo que ahora no tiene que preocuparse por suministrar el alcance correcto.


console.log(ageSmarterAlias.apply(anotherYogi)); //Output: 100 !!!

El problema ageSmarteres que nunca se puede establecer el alcance en otro objeto.


var ageSmartestAlias = yogi.getAgeSmartest;
console.log(ageSmartestAlias()); //Output: 100
console.log(ageSmartestAlias.apply(document)); //Output: 100

La ageSmartestfunción utilizará el alcance original si se proporciona un alcance no válido.


console.log(ageSmartestAlias.apply(anotherYogi)); //Output: 200

Aún podrá pasar otro Personobjeto a getAgeSmartest. :)

SoluciónYogi
fuente
7
+1 por qué funciona IE. El resto está un poco fuera de tema, pero sigue siendo útil en general. :)
Kev
44
Excelente respuesta. Sin embargo, no veo cómo nada de eso está fuera de tema.
Justin Johnson
Muy buena respuesta, de hecho. Aquí se escribe una versión algo más corta .
Marcel Korpel
8
Una de las mejores respuestas que he visto en stackoverflow.
warbaker
¿Por qué funciona en IE? porque: stackoverflow.com/questions/6994139/… - por lo que esta implementación de método puede ser de hecho {return window [id]}, por lo que funcionaría en cualquier contexto
Maciej Łebkowski
3

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 windowobjeto cuando fue diseñado para vivir en el documentobjeto.

window.myAlias = document.getElementById

Las alternativas son

  • usar una envoltura (ya mencionada por Fabien Ménager)
  • o puede utilizar dos alias.

    window.d = document // A renamed reference to the object
    window.d.myAlias = window.d.getElementById
    
Bryan Field
fuente
2

Otra respuesta corta, solo para envoltura / aliasing console.logy métodos de registro similares. Todos esperan estar en el consolecontexto.

Esto se puede utilizar cuando se envuelve console.logcon algunas alternativas, en caso de que usted o sus usuarios tengan problemas al usar un navegador que (siempre) no lo admite . Sin embargo, esta no es una solución completa para ese problema, ya que necesita una revisión ampliada y una alternativa; su kilometraje puede variar.

Ejemplo usando advertencias

var warn = function(){ console.warn.apply(console, arguments); }

Entonces úsalo como de costumbre

warn("I need to debug a number and an object", 9999, { "user" : "Joel" });

Si prefiere ver sus argumentos de registro envueltos en una matriz (yo lo hago, la mayoría de las veces), sustitúyalos .apply(...)por .call(...).

En caso de trabajar con console.log(), console.debug(), console.info(), console.warn(), console.error(). Consulte también consoleMDN .

Joel Purra
fuente
1

Además de otras excelentes respuestas, existe un método simple de jQuery $ .proxy .

Puedes usar un alias como este:

myAlias = $.proxy(document, 'getElementById');

O

myAlias = $.proxy(document.getElementById, document);
Sanghyun Lee
fuente
+1 porque es una solución factible. Pero, estrictamente hablando, detrás de escena $.proxytambién es realmente un envoltorio.
pimvdb
-6

En realidad , no puede "crear un alias puro" de una función en un objeto predefinido. Por lo tanto, lo más cercano al alias que puede obtener sin ajustar es permanecer dentro del mismo objeto:

>>> document.s = document.getElementById;
>>> document.s('myid');
<div id="myid">
Kev
fuente
5
Esto es un poco incorrecto. Puede 'puro alias' una función siempre que la implementación de la función use 'this'. Entonces depende.
SolutionYogi
Lo siento, quise decir en el contexto de getElementById. Tienes razón. Cambié la respuesta ligeramente para reflejar esto ...
Kev