L'alias de la fonction JavaScript ne semble pas fonctionner

85

Je lisais juste cette question et je voulais essayer la méthode d'alias plutôt que la méthode de wrapper de fonction, mais je n'arrivais pas à la 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, j'obtiens la même "invocation illégale".

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

Aussi, assez curieusement, j'ai juste essayé et cela fonctionne dans IE8.

Kev
la source
1
dans IE8 - cela pourrait être une sorte de fonction magique ou quelque chose.
Maciej Łebkowski
J'ai ajouté des commentaires aux réponses incorrectes sur stackoverflow.com/questions/954417 et je les ai dirigés ici.
Grant Wagner
1
Pour les navigateurs prenant en charge la méthode Function.prototype.bind: window.myAlias ​​= document.getElementById.bind (document); Recherchez les documents MDN, il y aura un shim de liaison.
Ben Fleming

Réponses:

39

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 faites un simple alias, 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 bindméthode 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();">
Maciej Łebkowski
la source
4
Ce qui est vraiment un wrapper car il renvoie une nouvelle fonction. Je pense que la réponse à la question de Kev est que ce n'est pas possible pour les raisons que vous décrivez ci-dessus. La méthode que vous décrivez ici est probablement la meilleure option.
jiggy
Merci pour votre commentaire, je n'avais pas regardé assez attentivement pour m'en rendre compte. Donc, la syntaxe des réponses à l'autre question est complètement fausse? Intéressant ...
Kev
188

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

Avant d'expliquer pourquoi vous ne pouvez pas créer d'alias document.getElementById, je vais essayer d'expliquer le fonctionnement des fonctions / objets JavaScript.

Chaque fois que vous appelez une fonction JavaScript, l'interpréteur JavaScript détermine une portée 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'appelez, la valeur de l' thisintérieur de la fonction somme sera l' Windowobjet global .

Pour la fonction «somme», la valeur de «ceci» n'a pas d'importance 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 dave.getAge fonction, l'interpréteur JavaScript voit que vous appelez la fonction getAge sur l' daveobjet, il met thisà daveet appelle la getAgefonction. getAge()reviendra correctement 100.


Vous savez peut-être qu'en JavaScript, vous pouvez spécifier la portée à l'aide de la applyméthode. 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 bobqu'objet. getAgeva maintenant revenir 200même si vous «pensiez» avoir appelé getAgel' daveobjet.


Quel est l'intérêt de tout ce qui précède? Les fonctions sont «vaguement» attachées à vos objets JavaScript. Par exemple, vous pouvez 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 ?????

ageMethodl'exécution lève une erreur! Qu'est-il arrivé?

Si vous lisez attentivement mes points ci-dessus, vous remarquerez que la dave.getAgeméthode a été appelée avec davecomme thisobjet alors que JavaScript ne pouvait pas déterminer la «portée» de l' ageMethodexécution. Donc, il a passé la «fenêtre» globale comme «ceci». Maintenant qu'il windown'a pas de birthDatepropriété, l' ageMethodexécution échouera.

Comment régler ceci? Facile,

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

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

var $ = document.getElementById;

$('someElement'); 

$est appelé avec windowcomme thiset si l' getElementByIdimplémentation s'attend thisà l'être document, elle échouera.

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

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

Alors, pourquoi cela fonctionne-t-il dans Internet Explorer?

Je ne sais pas la mise en œuvre interne getElementByIddans IE, mais un commentaire dans la source jQuery ( inArrayde mise en œuvre de la méthode) dit que dans IE, window == document. Si tel est le cas, l'aliasing document.getElementByIddevrait fonctionner dans IE.

Pour illustrer cela davantage, j'ai créé un exemple élaboré. Jetez un œil à la Personfonction 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 Personfonction ci-dessus, voici comment les différentes getAgeméthodes se comporteront.

Créons deux objets en utilisant Personfunction.

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.

Simple, la méthode getAge obtient l' yogiobjet en tant thisque sortie 100.


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

L'interprète JavaScript définit l' windowobjet comme thiset notre getAgeméthode retournera -1.


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

Si nous définissons la bonne portée, vous pouvez utiliser ageAliasmethod.


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

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

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

La ageSmarterfonction a capturé l' thisobjet 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 ageSmarterest que vous ne pouvez jamais définir la portée sur un autre objet.


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

La ageSmartestfonction 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 Personobjet à getAgeSmartest. :)

SolutionYogi
la source
7
+1 pour savoir pourquoi IE fonctionne. Le reste est un peu hors sujet mais toujours utile en général. :)
Kev
44
Excellente réponse. Je ne vois pas en quoi tout cela est hors sujet.
Justin Johnson
Très belle réponse, en effet. Une version un peu plus courte est écrite ici .
Marcel Korpel
8
L'une des meilleures réponses que j'ai vues sur stackoverflow.
warbaker
pourquoi ça marche dans IE? parce que: stackoverflow.com/questions/6994139/… - donc cette implémentation de méthode peut en fait être {return window [id]}, donc cela fonctionnerait dans n'importe quel contexte
Maciej Łebkowski
3

Ceci est une réponse courte.

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

window.myAlias = document.getElementById

Les alternatives sont

  • d'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
    
Champ Bryan
la source
2

Une autre réponse courte, juste pour le wrapping / aliasing console.loget les méthodes de journalisation similaires. Ils s'attendent tous à être dans le consolecontexte.

Ceci est utilisable lors de l'encapsulation console.logavec certaines solutions de secours, au cas où vous ou vos utilisateurs rencontriez des problèmes lors de l' utilisation d'un navigateur qui ne le prend pas (toujours) en charge . Ce n'est cependant pas une solution complète à ce problème, car il doit s'agir de vérifications étendues et d'une solution de secours - votre kilométrage peut varier.

Exemple d'utilisation d'avertissements

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

Ensuite, utilisez-le comme d'habitude

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

Si vous préférez voir vos arguments de journalisation enveloppés dans un tableau (je le fais, la plupart du temps), remplacez-les .apply(...)par .call(...).

Si le travail avec console.log(), console.debug(), console.info(), console.warn(), console.error(). Voir aussi consolesur MDN .

Joël Purra
la source
1

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);
Sanghyun Lee
la source
+1 parce que c'est une solution réalisable. Mais, à proprement parler, les coulisses $.proxysont aussi vraiment un emballage.
pimvdb
-6

En fait, vous ne pouvez pas «alias pur» une fonction sur un objet prédéfini. Par conséquent, le plus proche de l'aliasing que vous pouvez obtenir sans wrapping est de rester dans le même objet:

>>> document.s = document.getElementById;
>>> document.s('myid');
<div id="myid">
Kev
la source
5
Ceci est légèrement incorrect. Vous pouvez «alias pur» une fonction à condition que l'implémentation de la fonction utilise «this». Cela dépend donc.
SolutionYogi
Désolé, je voulais dire dans le contexte de getElementById. Tu as raison. J'ai légèrement changé la réponse pour refléter cela ...
Kev