web-dev-qa-db-fra.com

L'aliasing de la fonction JavaScript ne semble pas fonctionner

Je lisais juste cette question et je voulais essayer la méthode alias plutôt que la méthode wrapper de fonction, mais je n'arrivais pas à le faire fonctionner dans Firefox 3 ou 3.5beta4, ou Google Chrome , à la fois dans leurs fenêtres de débogage et dans une page Web de test.

Pyromane:

>>> window.myAlias = document.getElementById
function()
>>> myAlias('item1')
>>> window.myAlias('item1')
>>> document.getElementById('item1')
<div id="item1">

Si je le mets dans une page Web, l'appel à myAlias ​​me donne cette erreur:

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 (avec >>> inséré pour plus de clarté):

>>> window.myAlias = document.getElementById
function getElementById() { [native code] }
>>> window.myAlias('item1')
TypeError: Illegal invocation
>>> document.getElementById('item1')
<div id=?"item1">?

Et dans la page de test, je reçois le même "appel illégal".

Est-ce que je fais quelque chose de mal? Quelqu'un d'autre peut-il reproduire cela?

Aussi, curieusement, je viens d'essayer et cela fonctionne dans IE8.

81
Kev

Vous devez lier cette méthode à l'objet document. Regardez:

>>> $ = 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();">

Lorsque vous créez un alias simple, la fonction est appelée sur l'objet global, pas sur l'objet document. Utilisez une technique appelée fermetures pour résoudre ce problème:

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 cette façon, vous ne perdez pas la référence à l'objet d'origine.

En 2012, il y a la nouvelle méthode bind d'ES5 qui nous permet de le faire de manière plus sophistiquée:

>>> $ = document.getElementById.bind(document)
>>> $('bn_home')
<body id="bn_home" onload="init();">
38
Maciej Łebkowski

J'ai creusé profondément pour comprendre ce comportement particulier et je pense avoir trouvé une bonne explication.

Avant de comprendre pourquoi vous ne pouvez pas alias document.getElementById, Je vais essayer d'expliquer comment fonctionnent les fonctions/objets JavaScript.

Chaque fois que vous appelez une fonction JavaScript, l'interpréteur JavaScript détermine une étendue et la transmet à la fonction.

Considérez la fonction suivante:

function sum(a, b)
{
    return a + b;
}

sum(10, 20); // returns 30;

Cette fonction est déclarée dans la portée Window et lorsque vous l'invoquez, la valeur de this à l'intérieur de la fonction sum sera l'objet global Window.

Pour la fonction "somme", peu importe la valeur de "ceci" car elle ne l'utilise pas.


Considérez la fonction suivante:

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.

Lorsque vous appelez la fonction dave.getAge, l'interpréteur JavaScript voit que vous appelez la fonction getAge sur l'objet dave, il définit donc this sur dave et appelle le getAge. getAge() renverra correctement 100.


Vous savez peut-être qu'en JavaScript, vous pouvez spécifier la portée à l'aide de la méthode apply. Essayons ça.

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.

Dans la ligne ci-dessus, au lieu de laisser JavaScript décider de la portée, vous passez la portée manuellement en tant qu'objet bob. getAge renverra désormais 200 Même si vous "pensiez" avoir appelé getAge sur l'objet dave.


Quel est l'intérêt de tout ce qui précède? Les fonctions sont attachées de manière "lâche" à vos objets JavaScript. Par exemple. tu peux faire

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

Passons à l'étape suivante.

var dave = new Person(new Date(1909, 1, 1));
var ageMethod = dave.getAge;

dave.getAge(); //returns 100;
ageMethod(); //returns ?????

L'exécution de ageMethod génère une erreur! Qu'est-il arrivé?

Si vous lisez attentivement mes points ci-dessus, vous remarquerez que la méthode dave.getAge A été appelée avec dave comme objet this alors que JavaScript n'a pas pu déterminer la "portée" de ageMethod exécution. Il a donc passé la "fenêtre" globale comme "ceci". Maintenant que window n'a pas de propriété birthDate, l'exécution de ageMethod échouera.

Comment régler ceci? Facile,

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

Est-ce que tout ce qui précède avait du sens? Si c'est le cas, vous pourrez expliquer pourquoi vous ne pouvez pas alias document.getElementById:

var $ = document.getElementById;

$('someElement'); 

$ Est appelé avec window comme this et si l'implémentation de getElementById prévoit que this sera document, il le fera échouer.

Encore une fois pour résoudre ce problème, vous pouvez le faire

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

Alors pourquoi ça marche dans Internet Explorer?

Je ne connais pas l'implémentation interne de getElementById dans IE, mais un commentaire dans la source jQuery (implémentation de la méthode inArray) dit que dans IE, window == document. Si c'est le cas, l'alias document.getElementById Devrait fonctionner dans IE.

Pour illustrer cela davantage, j'ai créé un exemple élaboré. Jetez un œil à la fonction Person ci-dessous.

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();
    };

}

Pour la fonction Person ci-dessus, voici comment les différentes méthodes getAge se comporteront.

Créons deux objets en utilisant la fonction Person.

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.

Directement, la méthode getAge obtient l'objet yogi sous la forme this et génère 100.


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

L'interpréteur JavaScript définit l'objet window comme this et notre méthode getAge renverra -1.


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

Si nous définissons l'étendue correcte, vous pouvez utiliser la méthode ageAlias.


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

Si nous passons dans un autre objet personne, il calculera toujours correctement l'âge.

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

La fonction ageSmarter a capturé l'objet this d'origine, vous n'avez donc plus à vous soucier de fournir une portée correcte.


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

Le problème avec ageSmarter est que vous ne pouvez jamais définir la portée d'un autre objet.


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

La fonction ageSmartest utilisera la portée d'origine si une portée non valide est fournie.


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

Vous pourrez toujours passer un autre objet Person à getAgeSmartest. :)

184
SolutionYogi

Ceci est une réponse courte.

Ce qui suit fait une copie de (une référence à) la fonction. Le problème est que la fonction se trouve maintenant sur l'objet window lorsqu'elle a été conçue pour vivre sur l'objet document.

window.myAlias = document.getElementById

Les alternatives sont

  • utiliser un wrapper (déjà mentionné par Fabien Ménager)
  • ou vous pouvez utiliser deux alias.

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

En plus d'autres bonnes réponses, il existe une méthode jQuery simple $. Proxy .

Vous pouvez alias comme ceci:

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

Ou

myAlias = $.proxy(document.getElementById, document);
1
Sanghyun Lee