L'aliasing della funzione JavaScript non sembra funzionare


85

Stavo solo leggendo questa domanda e volevo provare il metodo alias piuttosto che il metodo function-wrapper, ma non riuscivo a farlo funzionare in Firefox 3 o 3.5beta4 o Google Chrome, entrambi nelle loro finestre di debug e in una pagina web di prova.

Firebug:

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

Se lo metto in una pagina web, la chiamata a myAlias ​​mi dà questo errore:

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 >>> inseriti per chiarezza):

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

E nella pagina di test, ottengo la stessa "chiamata illegale".

Sto facendo qualcosa di sbagliato? Qualcun altro può riprodurlo?

Inoltre, stranamente, ho appena provato e funziona in IE8.


1
in IE8 - potrebbe essere una sorta di funzione magica o qualcosa del genere.
Maciej Łebkowski

Ho aggiunto commenti alle risposte errate su stackoverflow.com/questions/954417 e li ho indirizzati qui.
Grant Wagner,

1
Per i browser che supportano il metodo Function.prototype.bind: window.myAlias ​​= document.getElementById.bind (document); Cerca nei documenti MDN, ci sarà uno spessore vincolante.
Ben Fleming

Risposte:


39

Devi associare quel metodo all'oggetto documento. Guarda:

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

Quando si esegue un semplice alias, la funzione viene chiamata sull'oggetto globale, non sull'oggetto documento. Usa una tecnica chiamata chiusure per risolvere questo 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();">

In questo modo non perdi il riferimento all'oggetto originale.

Nel 2012, c'è il nuovo bindmetodo di ES5 che ci permette di farlo in un modo più elaborato:

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

4
Che è davvero un wrapper perché restituisce una nuova funzione. Penso che la risposta alla domanda di Kev sia che non è possibile per i motivi che descrivi sopra. Il metodo che descrivi qui è probabilmente l'opzione migliore.
jiggy

Grazie per il tuo commento, non avevo guardato abbastanza da vicino per rendermene conto. Quindi, la sintassi nelle risposte sull'altra domanda è completamente fasulla? Interessante ...
Kev

188

Ho scavato a fondo per capire questo particolare comportamento e credo di aver trovato una buona spiegazione.

Prima di capire perché non sei in grado di creare alias document.getElementById, cercherò di spiegare come funzionano le funzioni / oggetti JavaScript.

Ogni volta che si richiama una funzione JavaScript, l'interprete JavaScript determina un ambito e lo passa alla funzione.

Considera la seguente funzione:

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

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

Questa funzione è dichiarata nell'ambito della finestra e quando la si richiama il valore di thisall'interno della funzione sum sarà l' Windowoggetto globale .

Per la funzione "somma", non importa quale sia il valore di "questo" poiché non lo utilizza.


Considera la seguente funzione:

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.

Quando si chiama dave.getAge la funzione, l'interprete JavaScript vede che si sta chiamando la funzione getAge sul daveoggetto, quindi imposta thisper davee chiama la getAgefunzione di. getAge()tornerà correttamente 100.


Potresti sapere che in JavaScript puoi specificare l'ambito utilizzando il applymetodo. Proviamolo.

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.

Nella riga precedente, invece di lasciare che JavaScript decida l'ambito, si passa l'ambito manualmente come boboggetto. getAgeora tornerà 200anche se hai "pensato" di aver chiamato getAgel' daveoggetto.


Qual è il punto di tutto quanto sopra? Le funzioni sono "liberamente" collegate ai tuoi oggetti JavaScript. Ad esempio, puoi farlo

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

Facciamo il passo successivo.

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

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

ageMethodl'esecuzione genera un errore! Quello che è successo?

Se leggi attentamente i miei punti precedenti, noterai che il dave.getAgemetodo è stato chiamato con davecome thisoggetto mentre JavaScript non è stato in grado di determinare l '"ambito" per l' ageMethodesecuzione. Quindi ha passato "Finestra" globale come "questo". Ora poiché windownon ha una birthDateproprietà, l' ageMethodesecuzione fallirà.

Come risolvere questo problema? Semplice,

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

Tutto quanto sopra aveva senso? In tal caso, sarai in grado di spiegare perché non sei in grado di creare l'alias document.getElementById:

var $ = document.getElementById;

$('someElement'); 

$viene chiamato con windowas thise se l' getElementByIdimplementazione si aspetta thisdi essere document, fallirà.

Ancora una volta per risolvere questo problema, puoi farlo

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

Allora perché funziona in Internet Explorer?

Non so l'implementazione interna di getElementByIdin IE, ma un commento nel sorgente di jQuery ( inArrayimplementazione del metodo) dice che in IE, window == document. In tal caso, l'aliasing document.getElementByIddovrebbe funzionare in IE.

Per illustrare ulteriormente questo aspetto, ho creato un esempio elaborato. Dai un'occhiata alla Personfunzione qui sotto.

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

}

Per la Personfunzione sopra, ecco come getAgesi comporteranno i vari metodi.

Creiamo due oggetti usando 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.

Semplicemente, il metodo getAge ottiene l' yogioggetto come thise gli output 100.


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

L'interprete JavaScript imposta l' windowoggetto come thise il nostro getAgemetodo restituirà -1.


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

Se impostiamo l'ambito corretto, puoi utilizzare il ageAliasmetodo.


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

Se passiamo in qualche altro oggetto persona, calcolerà comunque correttamente l'età.

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

La ageSmarterfunzione ha catturato l' thisoggetto originale , quindi ora non devi preoccuparti di fornire l'ambito corretto.


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

Il problema ageSmarterè che non puoi mai impostare l'ambito su un altro oggetto.


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

La ageSmartestfunzione utilizzerà l'ambito originale se viene fornito un ambito non valido.


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

Sarai comunque in grado di passare un altro Personoggetto a getAgeSmartest. :)


7
+1 per il motivo per cui IE funziona. Il resto è un po 'fuori tema ma comunque utile in generale. :)
Kev

44
Ottima risposta. Tuttavia, non vedo come tutto ciò sia fuori tema.
Justin Johnson,

Risposta molto bella, davvero. Una versione un po 'più breve è scritta qui .
Marcel Korpel

8
Una delle migliori risposte che ho visto su stackoverflow.
warbaker il

perché funziona in IE? perché: stackoverflow.com/questions/6994139/… - quindi l'implementazione di questo metodo potrebbe in effetti essere {return window [id]}, quindi funzionerebbe in qualsiasi contesto
Maciej Łebkowski

3

Questa è una risposta breve.

Quanto segue fa una copia di (un riferimento a) la funzione. Il problema è che ora la funzione è windowsull'oggetto quando è stata progettata per vivere documentsull'oggetto.

window.myAlias = document.getElementById

Le alternative sono

  • usare un wrapper (già menzionato da Fabien Ménager)
  • oppure puoi usare due alias.

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

2

Un'altra risposta breve, solo per wrapping / aliasing console.loge metodi di registrazione simili. Tutti si aspettano di essere nel consolecontesto.

Questo è utilizzabile quando console.logsi esegue il wrapping con alcuni fallback, nel caso in cui tu oi tuoi utenti abbiate avuto problemi quando utilizzate un browser che non lo supporta (sempre) . Tuttavia, questa non è una soluzione completa a questo problema, poiché è necessario espandere i controlli e un ripiego: il tuo chilometraggio può variare.

Esempio di utilizzo degli avvisi

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

Quindi usalo come al solito

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

Se preferisci vedere i tuoi argomenti di registrazione racchiusi in un array (lo faccio, la maggior parte delle volte), sostituiscilo .apply(...)con .call(...).

Dovrebbe funzionare con console.log(), console.debug(), console.info(), console.warn(), console.error(). Vedi anche consolesu MDN .


1

Oltre ad altre ottime risposte, c'è il semplice metodo jQuery $ .proxy .

Puoi alias in questo modo:

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

O

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

+1 perché è una soluzione fattibile. Ma, in senso stretto, anche dietro le quinte $.proxyè davvero un involucro.
pimvdb

-6

In realtà non puoi "alias puro" una funzione su un oggetto predefinito. Pertanto, il più vicino all'aliasing che puoi ottenere senza wrapping è rimanere all'interno dello stesso oggetto:

>>> document.s = document.getElementById;
>>> document.s('myid');
<div id="myid">

5
Questo è leggermente errato. È possibile "alias puro" una funzione a condizione che l'implementazione della funzione utilizzi "this". Quindi dipende.
SoluzioneYogi

Scusa, intendevo nel contesto di getElementById. Hai ragione. Ho cambiato leggermente la risposta per riflettere questo ...
Kev il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.