Às vezes, você pode querer substituir métodos globais incorporados como setTimeout e setInterval. Se tentar, você pode descobrir que é muito mais difícil que imagina fazer isso em todos os navegadores, principalmente se quiser encontrar os originais novamente. Depois de muitas experiências dolorosas, acho que tenho uma solução definitiva que funciona em todos os navegadores com o mínimo de efeitos colaterais.
Abordagens fracassadas
Comecei o trabalho com a abordagem simples:
setTimeout = function() {};
// or
window.setTimeout = function() {};
Isso pareceu funcionar na maioria dos navegadores. No entanto, o Internet Explorer 8 e inferiores não apreciam essa técnica. No primeiro caso, o IE irá realmente lançar uma exceção dizendo: “Objeto não suporta esta ação”. A segunda opção funciona, mas ela só vai afetar o valor de window.setTimeout, deixando o velho e mundial setTimeout sozinho. Isso é viável, mas não é ideal. Hora de procurar outra solução.
Outra tentativa
Depois de falar com @jcummins sobre isso, pensamos em usar a palavra-chave var, e ver se isso ajudaria. Assim, isso me leva à minha próxima abordagem:
var setTimeout = function() {};
A boa notícia é que isso funciona em todas as áreas! Ambos setTimeout e window.setTimeout agora se referem à minha nova função, em todos os navegadores, e você pode, seguramente, fazer quaisquer atribuições necessárias, sem exceções. Mas agora a pergunta é: para onde que o setTimeout original vai? Não é uma coisa fácil de encontrar.
Você pode tentar algo como o seguinte:
var temp = setTimeout,
setTimeout = function() {};
Agora, você pode esperar temp para conter o setTimeout original, mas infelizmente surgirá undefined. Isso é devido à elevação do JavaScript. Eu até tentei var temp = window.setTimeout primeiro, mas a propriedade no window foi imediatamente içada no topo.
Então, resolvi achar outra maneira de obter o valor original. Depois de algumas pesquisas, descobri uma referência ao setTimeout original no protótipo do windows, que você pode acessar em window.constructor.prototype.setTimeout. As coisas pareciam ir bem. Infelizmente, elas entraram rapidamente em declínio.
- Na maioria dos navegadores, window é construído por uma função chamada “DOMWindow“. Ou seja, window.constructor.name === “DOMWindow”. No entanto, esse não é o caso no Safari, no qual a propriedade constructor referencia Object. Nós não temos uma maneira de acessar DOMWindow diretamente, por isso não podemos chegar ao protótipo. Felizmente, podemos usar a propriedade do ECMAScript 5 __ proto__, e encontrar setTimeout em window.__proto__.setTimeout. Então meu pega-tudo tornou-se (window.__proto__ | | window.constructor.prototype).setTimeout.
- Acontece que o Opera tem o mesmo problema que o Safari, e não podemos acessar o protótipo correto em window.constructor.prototype. No entanto, ele também não parece capaz de acessá-lo em window.__proto__, deixando-nos com poucas opções. Em breve, mais sobre esse assunto.
- IE7 e inferiores não têm as propriedades constructor e __proto__ em window, e não parece ser qualquer outra forma de obter acesso direto ao protótipo do window.
Nesse ponto, anunciei que procurar os exemplares originais de setTimeout fora do escopo global era uma causa perdida, e voltei para a prancheta. Você poderia, em teoria, instanciar um novo <iframe>, e copiar o setTimeout dele, mas preferi não introduzir essa grande sobrecarga.
Uma solução
Nesse momento, eu percebi que a melhor solução para burlar as regras do JavaScript seria a elevação. Como sabemos, a elevação ocorre imediatamente depois de introduzir um contexto de execução. Assim, para evitá-la, teríamos de introduzir um segundo contexto de execução Você poderia fazer isso facilmente em HTML:
<script>
var temp = setTimeout;
</script>
<script>
var setTimeout = function() {};
</script>
No entanto, estava procurando uma solução pura em JS. Então, depois de um pequeno exame de consciência, decidi que tiraria eval.
var temp = setTimeout;
eval("var setTimeout;");
setTimeout = function() {};
Feito. A coisa mais flexível para fazer é apenas corrigir rapidamente a inconsistência onde o IE não permite que você substitua diretamente setTimeout e prossiga para fazer o que você precisa. Aqui está um código de exemplo rápido, mas você pode facilmente adaptar melhor em seu próprio projeto:
var __originals = {
st: setTimeout,
si: setInterval,
ct: clearTimeout,
ci: clearInterval
};
eval("var setTimeout, setInterval, clearTimeout, clearInterval;");
setTimeout = __originals.st;
setInterval = __originals.si;
clearTimeout = __originals.ct;
clearInterval = __originals.ci;
__originals = undefined;
Esse trecho vai suavizar essa inconsistência no Internet Explorer, e permitirá que você continue com as substituições no que quer ou que você precisa sobre esses métodos. Não há necessidade de usar var novamente no futuro, assim poderá evitar o incômodo da elevação. Testei isso nos IE6, 7, 8 e 9, assim como em Chrome, Safari, Firefox 3/4, e Opera, tudo em Mac e Windows, e é rock-solid.
Update! Uma melhor solução
Após uma investigação mais aprofundada, ajudado por muitos, incluindo @angusTweets e @kangax, eu descobri totalmente o problema no IE, com uma solução extremamente simples e segura.
Acontece que o problema não está sempre presente. Por exemplo, abra uma nova janela em branco no IE7 ou IE8, e tente o seguinte no console JS:
window.setTimeout = 1;
setTimeout; // 1
setTimeout = 2; // 2
Mas, então, experimente (mais uma vez, em uma nova janela):
setTimeout;
window.setTimeout = 1;
setTimeout; // {...}
window.setTimeout; // 1
setTimeout = 1; // Error
Então, o que está acontecendo aqui? Bem, eu trouxe uma teoria sólida.
Inicialmente, a propriedade setTimeout existe no protótipo do window, não no window em si. Então, quando você pede por window.setTimeout, ele realmente atravessa um passo na cadeia de protótipos para resolver a referência. Da mesma forma, quando você pergunta para setTimeout, ele vai transitar para baixo da cadeia do escopo, então para o window, então para baixo da cadeia de protótipos para resolver a referência.
Suspeito que o IE tem uma otimização embutida que armazena automaticamente em cache a resolução de globais implícitas que são descobertas por todo o caminho no protótipo do objeto global. É bom ter uma boa razão para fazê-lo, uma vez que estas são referências comumente solicitadas, e que atravessar aquela cadeia é custoso. No entanto, deve-se definir essa referência como somente de leitura, já que é apenas uma otimização de cache. Isso tem o infeliz efeito colateral de causar uma exceção para ser acionada quando tentar atribuir à referência, usando-a como um lvalue. A única maneira de acabar com essa nova referência é usando o var no escopo global, mas isso nos coloca o problema de elevação. O que fazer?
Assumindo que essa teoria está correta, há realmente uma solução simples. Para evitar essa “otimização”, precisamos simplesmente mover a referência a um passo da cadeia, diretamente no window. Felizmente, isso é muito fácil:
window.setTimeout = window.setTimeout;
É isso aí! Supondo que você tenha feito isso antes de qualquer referência ao setTimeout por conta própria, isso irá passar a referência e evitar completamente esse problema. Você pode, agora com segurança, atribuir a setTimeout sem usar var. Isso funciona porque window.setTimeout, quando usado como um lvalue (no lado esquerdo da atribuição), não anda a cadeia de protótipos, mas sim do lado direito. Isso irá, então, sempre puxar uma propriedade para fora da cadeia de protótipos e colocá-la direito sobre o objeto. O IE apresenta o mesmo problema com todas as outras propriedades no protótipo window, e a mesma solução irá corrigi-los. Você pode encontrar facilmente as outras propriedades, mas algumas delas são alert, attachEvent, scrollTo, blur, e muitas outras.
Esta solução foi testada em todos os navegadores, não tem efeitos colaterais, e é maravilhosamente simples. É sempre bom quando as coisas funcionam dessa maneira.
?
Texto original disponível em http://www.adequatelygood.com/2011/4/Replacing-setTimeout-Globally