El alias de función de JavaScript no parece funcionar

Resuelto Kev asked hace 15 años • 6 respuestas

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.

Kev avatar Jun 17 '09 21:06 Kev
Aceptado

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 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 llama a la función dave.getAge, el intérprete de JavaScript ve que está llamando a la función getAge en el daveobjeto, por lo que establece thisy davellama a la getAgefunción. getAge()volverá correctamente 100.


Quizás sepas que en JavaScript puedes especificar el alcance utilizando el applymé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 bobobjeto. getAgeahora regresará 200aunque "pensaste" que habías llamado getAgeal daveobjeto.


¿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.getAgeel método se llamó davecomo thisobjeto, mientras que JavaScript no pudo determinar el "alcance" de ageMethodla ejecución. Entonces pasó la 'Ventana' global como 'esta'. Ahora como windowno tiene una birthDatepropiedad, ageMethodla 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 windowas thisy si getElementByIdse espera que la implementación thissea 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 getElementByIden IE, pero un comentario en la fuente jQuery ( inArrayimplementación del método) dice que en IE window == document. Si ese es el caso, entonces el alias 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 Personla 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 yogiel objeto como thisy genera 100.


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

El intérprete de JavaScript establece windowel objeto como thisy nuestro getAgemétodo devolverá -1.


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

Si configuramos el alcance correcto, puede usar ageAliasel 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 ageSmarterfunció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 ageSmarteres 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 ageSmartestfunció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 Personobjeto a getAgeSmartest. :)

SolutionYogi avatar Jul 21 '2009 23:07 SolutionYogi

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 bindmé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();">
Maciej Łebkowski avatar Jun 17 '2009 14:06 Maciej Łebkowski

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ñada para vivir en el documentobjeto.

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
    
700 Software avatar Feb 03 '2011 17:02 700 Software