Wydaje się, że aliasowanie funkcji JavaScript nie działa

85

Właśnie czytałem to pytanie i chciałem wypróbować metodę aliasu zamiast metody opakowywania funkcji, ale nie mogłem sprawić, aby działała w Firefoksie 3 lub 3.5beta4 lub Google Chrome, zarówno w ich oknach debugowania, jak i na testowej stronie internetowej.

Firebug:

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

Jeśli umieszczę to na stronie internetowej, wywołanie myAlias ​​daje mi następujący błąd:

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 (dla jasności wstawiono >>>):

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

Na stronie testowej otrzymuję to samo „Nielegalne wywołanie”.

czy robię coś źle? Czy ktoś inny może to odtworzyć?

Co dziwne, właśnie spróbowałem i działa w IE8.

Kev
źródło
1
w IE8 - może to być jakaś magiczna funkcja czy coś.
Maciej Łebkowski
Dodałem komentarze do błędnych odpowiedzi na stackoverflow.com/questions/954417 i skierowałem je tutaj.
Grant Wagner
1
W przypadku przeglądarek obsługujących metodę Function.prototype.bind: window.myAlias ​​= document.getElementById.bind (dokument); Przeszukaj dokumenty MDN, będzie podkładka wiążąca.
Ben Fleming,

Odpowiedzi:

39

Musisz powiązać tę metodę z obiektem dokumentu. Popatrz:

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

Kiedy robisz prosty alias, funkcja jest wywoływana na obiekcie globalnym, a nie na obiekcie dokumentu. Użyj techniki zwanej domknięciami, aby to naprawić:

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

W ten sposób nie stracisz odniesienia do oryginalnego obiektu.

W 2012 roku pojawiła się nowa bindmetoda z ES5, która pozwala nam to zrobić w bardziej wyszukany sposób:

>>> $ = document.getElementById.bind(document)
>>> $('bn_home')
<body id="bn_home" onload="init();">
Maciej Łebkowski
źródło
4
Co jest naprawdę opakowaniem, ponieważ zwraca nową funkcję. Myślę, że odpowiedź na pytanie Keva jest taka, że ​​nie jest to możliwe z powodów, które opisałeś powyżej. Opisana tutaj metoda jest prawdopodobnie najlepszą opcją.
jiggy
Dziękuję za komentarz, nie przyjrzałem się wystarczająco uważnie, aby to zrozumieć. Czyli składnia odpowiedzi na drugie pytanie jest całkowicie fałszywa? Ciekawe ...
Kev
188

Kopałem głęboko, aby zrozumieć to szczególne zachowanie i myślę, że znalazłem dobre wytłumaczenie.

Zanim przejdę do tego, dlaczego nie możesz aliasować document.getElementById, spróbuję wyjaśnić, jak działają funkcje / obiekty JavaScript.

Za każdym razem, gdy wywołujesz funkcję JavaScript, interpreter JavaScript określa zakres i przekazuje go do funkcji.

Rozważ następującą funkcję:

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

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

Ta funkcja jest zadeklarowana w zakresie okna i kiedy ją wywołasz, wartość thiswewnątrz funkcji sum będzie Windowobiektem globalnym .

W przypadku funkcji „sum” nie ma znaczenia, jaka jest wartość „this”, ponieważ jej nie używa.


Rozważ następującą funkcję:

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.

Kiedy wywołujesz funkcję dave.getAge, interpreter JavaScript widzi, że wywołujesz funkcję getAge na daveobiekcie, więc ustawia thisdavei wywołuje getAge. getAge()wróci poprawnie 100.


Być może wiesz, że w JavaScript możesz określić zakres za pomocą applymetody. Spróbujmy tego.

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.

W powyższym wierszu, zamiast pozwolić JavaScript decydować o zakresie, przekazujesz zakres ręcznie jako bobobiekt. getAgebędzie teraz powrócić 200, nawet jeśli „myśli” dzwoniłeś getAgena daveobiekcie.


Jaki jest sens tego wszystkiego? Funkcje są „luźno” dołączone do obiektów JavaScript. Np. Możesz to zrobić

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

Zróbmy następny krok.

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

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

ageMethodwykonanie generuje błąd! Co się stało?

Jeśli czytasz moje powyższe punkty ostrożnie, by pamiętać, że dave.getAgemetoda została wywołana davejako thisobiekt natomiast JavaScript nie może określić zakres „” do ageMethodwykonania. Więc przeszedł globalne „Okno” jako „to”. Teraz, ponieważ windownie ma birthDatewłaściwości, ageMethodwykonanie zakończy się niepowodzeniem.

Jak to naprawić? Prosty,

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

Czy wszystkie powyższe miały sens? Jeśli tak, będziesz w stanie wyjaśnić, dlaczego nie możesz użyć aliasu document.getElementById:

var $ = document.getElementById;

$('someElement'); 

$nazywa się windowtak thisa jeśli getElementByIdrealizacja spodziewa thissię document, że zawiedzie.

Ponownie, aby to naprawić, możesz to zrobić

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

Dlaczego więc działa w Internet Explorerze?

Nie wiem wewnętrzną implementację getElementByIdw IE, ale komentarz w źródle jQuery ( inArrayimplementacji metody) mówi, że w IE window == document. W takim przypadku aliasing document.getElementByIdpowinien działać w IE.

Aby dalej to zilustrować, stworzyłem rozbudowany przykład. Spójrz na Personponiższą funkcję.

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

}

PersonOto jak getAgezachowują się różne metody w przypadku powyższej funkcji .

Utwórzmy dwa obiekty za pomocą Personfunkcji.

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.

Od razu metoda getAge pobiera yogiobiekt jako thisi wyprowadza 100.


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

JavaScript interpreter ustawia windowobiekt jako, thisa nasza getAgemetoda zwróci -1.


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

Jeśli ustawimy prawidłowy zakres, możesz użyć ageAliasmetody.


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

Jeśli przekażemy obiekt innej osoby, nadal poprawnie obliczy wiek.

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

ageSmarterFunkcja schwytany oryginalnego thisobiektu, więc teraz nie trzeba się martwić o dostarczanie prawidłowego zakresu.


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

Problem ageSmarterpolega na tym, że nigdy nie można ustawić zakresu na inny obiekt.


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

ageSmartestFunkcja użyje pierwotnego zakresu, jeśli nieprawidłowy zakres jest dostarczany.


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

Nadal będziesz mógł przekazać inny Personobiekt getAgeSmartest. :)

Rozwiązanie Yogi
źródło
7
+1, dlaczego IE działa. Reszta jest nieco niezwiązana z tematem, ale ogólnie jest pomocna. :)
Kev
44
Doskonała odpowiedź. Nie widzę jednak, żeby cokolwiek z tego było nie na temat.
Justin Johnson,
Bardzo ładna odpowiedź. Napisano tutaj nieco krótszą wersję .
Marcel Korpel
8
Jedna z najlepszych odpowiedzi, jakie widziałem na temat stackoverflow.
warbaker
dlaczego to działa w IE? ponieważ: stackoverflow.com/questions/6994139/… - więc ta implementacja metody może faktycznie być {return window [id]}, więc działałaby w dowolnym kontekście
Maciej Łebkowski
3

To jest krótka odpowiedź.

Poniższy tekst tworzy kopię (odniesienie do) funkcji. Problem polega na tym, że teraz funkcja znajduje się na windowobiekcie, gdy została zaprojektowana do życia na documentobiekcie.

window.myAlias = document.getElementById

Są alternatywy

  • używać opakowania (wspomniane już przez Fabiena Ménagera)
  • lub możesz użyć dwóch aliasów.

    window.d = document // A renamed reference to the object
    window.d.myAlias = window.d.getElementById
    
Bryan Field
źródło
2

Kolejna krótka odpowiedź, tylko do zawijania / aliasingu console.logi podobnych metod logowania. Wszyscy oczekują, że znajdą się w consolekontekście.

Jest to przydatne podczas pakowania console.logz pewnymi awaryjnymi rozwiązaniami, na wypadek gdybyś Ty lub Twoi użytkownicy mieli problemy podczas korzystania z przeglądarki, która (nie zawsze) tego nie obsługuje . Nie jest to jednak pełne rozwiązanie tego problemu, ponieważ wymaga rozszerzonych kontroli i rezerwy - Twój przebieg może się różnić.

Przykład użycia ostrzeżeń

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

Następnie użyj go jak zwykle

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

Jeśli wolisz, aby zobaczyć swoje argumenty rejestrowania zapakowane w tablicy (ja, przez większość czasu), substytut .apply(...)z .call(...).

Powinny pracować z console.log(), console.debug(), console.info(), console.warn(), console.error(). Zobacz także consolena MDN .

Joel Purra
źródło
1

Oprócz innych świetnych odpowiedzi, istnieje prosta metoda jQuery $ .proxy .

Możesz alias w ten sposób:

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

Lub

myAlias = $.proxy(document.getElementById, document);
Sanghyun Lee
źródło
+1, ponieważ jest to wykonalne rozwiązanie. Ale, ściśle mówiąc, za kulisami $.proxytak naprawdę jest też opakowanie.
pimvdb
-6

W rzeczywistości nie można „czystego aliasu” funkcji na predefiniowanym obiekcie. Dlatego też najbliższy aliasingu, jaki można uzyskać bez zawijania, jest pozostanie w tym samym obiekcie:

>>> document.s = document.getElementById;
>>> document.s('myid');
<div id="myid">
Kev
źródło
5
To jest trochę niepoprawne. Możesz „czysty alias” funkcji pod warunkiem, że implementacja funkcji używa „this”. Więc to zależy.
Rozwiązanie
Przepraszam, miałem na myśli w kontekście getElementById. Masz rację. Zmieniłem nieco odpowiedź, aby to odzwierciedlić ...
Kev