O aliasing da função JavaScript parece não funcionar

85

Eu estava lendo esta pergunta e queria tentar o método de alias em vez do método de wrapper de função, mas não consegui fazê-lo funcionar no Firefox 3 ou 3.5beta4 ou no Google Chrome, ambos em suas janelas de depuração e em uma página da web de teste.

Firebug:

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

Se eu colocar em uma página da web, a chamada para myAlias ​​me dará este erro:

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 (com >>> inserido para maior clareza):

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

E na página de teste, recebo o mesmo "Invocação ilegal".

Estou fazendo algo errado? Alguém mais pode reproduzir isso?

Além disso, por incrível que pareça, eu apenas tentei e funciona no IE8.

Kev
fonte
1
no IE8 - pode ser algum tipo de função mágica ou algo assim.
Maciej Łebkowski
Adicionei comentários às respostas incorretas em stackoverflow.com/questions/954417 e os direcionei aqui.
Grant Wagner
1
Para navegadores que suportam o método Function.prototype.bind: window.myAlias ​​= document.getElementById.bind (document); Pesquise os documentos MDN, haverá um shim de ligação.
Ben Fleming

Respostas:

39

Você deve vincular esse método ao objeto de documento. Veja:

>>> $ = 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 você está criando um alias simples, a função é chamada no objeto global, não no objeto do documento. Use uma técnica chamada closures para corrigir isso:

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

Desta forma, você não perde a referência ao objeto original.

Em 2012, existe o novo bindmétodo da ES5 que nos permite fazer isso de uma forma mais sofisticada:

>>> $ = document.getElementById.bind(document)
>>> $('bn_home')
<body id="bn_home" onload="init();">
Maciej Łebkowski
fonte
4
Que é realmente um wrapper porque retorna uma nova função. Acho que a resposta à pergunta de Kev é que não é possível pelas razões que você descreveu acima. O método que você descreve aqui é provavelmente a melhor opção.
jiggy
Obrigado pelo seu comentário, eu não olhei de perto o suficiente para perceber isso. Então, a sintaxe nas respostas da outra pergunta é completamente falsa? Interessante ...
Kev
188

Pesquisei profundamente para entender esse comportamento específico e acho que encontrei uma boa explicação.

Antes de entrar no porquê de você não conseguir document.getElementByIdusar um alias , tentarei explicar como as funções / objetos JavaScript funcionam.

Sempre que você invoca uma função JavaScript, o interpretador JavaScript determina um escopo e o passa para a função.

Considere a seguinte função:

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

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

Esta função é declarada no escopo Window e quando você a invoca, o valor de thisdentro da função sum será o Windowobjeto global .

Para a função 'sum', não importa qual é o valor de 'this', pois não o está usando.


Considere a seguinte função:

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 você chama dave.getAge função, o interpretador JavaScript vê que você está chamando a função getAge sobre o daveobjeto, de modo que define thisa davee chama a getAgefunção. getAge()retornará corretamente 100.


Você deve saber que em JavaScript você pode especificar o escopo usando o applymétodo. Vamos tentar isso.

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.

Na linha acima, em vez de deixar o JavaScript decidir o escopo, você está passando o escopo manualmente como o bobobjeto. getAgeagora retornará 200mesmo que você "tenha pensado" que chamou getAgeo daveobjeto.


Qual é o ponto de tudo isso? As funções são anexadas "vagamente" aos objetos JavaScript. Por exemplo, você pode fazer

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

Vamos dar o próximo passo.

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

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

ageMethoda execução gera um erro! O que aconteceu?

Se você ler meus pontos acima com atenção, notará que o dave.getAgemétodo foi chamado com davecomo thisobjeto, ao passo que o JavaScript não pôde determinar o 'escopo' de ageMethodexecução. Portanto, passou a 'Janela' global como 'isto'. Agora como windownão tem uma birthDatepropriedade, a ageMethodexecução irá falhar.

Como consertar isto? Simples,

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

Todas as opções acima faziam sentido? Em caso afirmativo, você poderá explicar por que não consegue criar um alias document.getElementById:

var $ = document.getElementById;

$('someElement'); 

$é chamado com windowcomo thise se getElementByIda execução está esperando thispara ser document, ele irá falhar.

Novamente, para corrigir isso, você pode fazer

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

Então, por que funciona no Internet Explorer?

Não conheço a implementação interna do getElementByIdno IE, mas um comentário no código fonte jQuery ( inArrayimplementação do método) diz que no IE window == document,. Se for esse o caso, o aliasing document.getElementByIddeve funcionar no IE.

Para ilustrar isso ainda mais, criei um exemplo elaborado. Dê uma olhada na Personfunção abaixo.

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 a Personfunção acima, veja como os vários getAgemétodos se comportarão.

Vamos criar dois objetos usando Personfunção.

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.

Em linha reta, o método getAge obtém o yogiobjeto como thise as saídas 100.


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

O intérprete de JavaScript define o windowobjeto como thise nosso getAgemétodo retornará -1.


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

Se definirmos o escopo correto, você pode usar o ageAliasmétodo.


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

Se passarmos algum objeto de outra pessoa, ele ainda calculará a idade corretamente.

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

A ageSmarterfunção capturou o thisobjeto original, então agora você não precisa se preocupar em fornecer o escopo correto.


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

O problema ageSmarteré que você nunca pode definir o escopo para algum outro objeto.


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

A ageSmartestfunção usará o escopo original se um escopo inválido for fornecido.


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

Você ainda poderá passar outro Personobjeto para getAgeSmartest. :)

SolutionYogi
fonte
7
1 para saber por que o IE funciona. O resto está um pouco fora do tópico, mas ainda é útil em geral. :)
Kev
44
Excelente resposta. Não vejo como isso está fora do assunto, no entanto.
Justin Johnson
Muito boa resposta, de fato. Uma versão um pouco mais curta está escrita aqui .
Marcel Korpel
8
Uma das melhores respostas que já vi no stackoverflow.
warbaker
por que funciona no IE? porque: stackoverflow.com/questions/6994139/… - então esta implementação de método pode na verdade ser {return window [id]}, então funcionaria em qualquer contexto
Maciej Łebkowski
3

Esta é uma resposta curta.

O seguinte faz uma cópia de (uma referência à) função. O problema é que agora a função está no windowobjeto quando foi projetada para viver nele document.

window.myAlias = document.getElementById

As alternativas são

  • usar um invólucro (já mencionado por Fabien Ménager)
  • ou você pode usar dois aliases.

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

Outra resposta curta, apenas para agrupamento / aliasing console.loge métodos de registro semelhantes. Todos eles esperam estar no consolecontexto.

Isso pode ser usado durante o empacotamento console.logcom alguns substitutos, caso você ou seus usuários tenham problemas ao usar um navegador que (sempre) não oferece suporte . No entanto, esta não é uma solução completa para esse problema, pois precisa de verificações expandidas e um fallback - sua milhagem pode variar.

Exemplo de uso de avisos

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

Em seguida, use-o normalmente

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

Se você preferir ver seus argumentos de log empacotados em um array (eu faço, na maioria das vezes), substitua .apply(...)por .call(...).

Deve trabalhar com console.log(), console.debug(), console.info(), console.warn(), console.error(). Veja também consoleno MDN .

Joel Purra
fonte
1

Além de outras ótimas respostas, há o método jQuery simples $ .proxy .

Você pode usar um alias como este:

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

Ou

myAlias = $.proxy(document.getElementById, document);
Sanghyun Lee
fonte
+1 porque é uma solução viável. Mas, estritamente falando, nos bastidores $.proxytambém é um invólucro.
pimvdb de
-6

Na verdade, você não pode criar um "alias puro" para uma função em um objeto predefinido. Portanto, o mais próximo do aliasing que você pode obter sem agrupar é permanecer no mesmo objeto:

>>> document.s = document.getElementById;
>>> document.s('myid');
<div id="myid">
Kev
fonte
5
Isso é um pouco incorreto. Você pode usar o 'apelido puro' de uma função, desde que a implementação da função use 'this'. Então, depende.
SolutionYogi
Desculpe, eu quis dizer no contexto de getElementById. Você está certo. Mudei ligeiramente a resposta para refletir isso ...
Kev