Front End

5 mar, 2012

Estendendo nativos do JavaScript

Publicidade

A maioria dos objetos padrão do JavaScript são construtores, que possuem prototypes que contêm métodos e outras propriedades que definem o seu comportamento padrão.

 //(results will vary by browser)

Object.getOwnPropertyNames(Function.prototype)
//["bind", "arguments", "toString", "length", "call", "name", "apply", "caller", "constructor"]

Não é possível excluir ou substituir um protótipo nativo, mas é possível editar os valores de suas propriedades, ou criar novas:

 //create a new array method that removes a member
Array.prototype.remove = function(member) {
var index = this.indexOf(member);
if (index > -1) {
this.splice(index, 1);
}
return this;
}

['poppy', 'sesame', 'plain'].remove('poppy'); //["sesame", "plain"]
['ant', 'bee', 'fly'].remove('spider'); //["ant", "bee", "fly"]

Et voila! O código acima obtém uma extensão array útil de forma gratuita. Entretanto, se você se gaba de fazer isso em código de produção, espere ser atingido por uma onda de reprovação. Vamos discutir sobre o perigo do dogma e tentar chegar a uma conclusão aceitável:

 A oposição

Nos últimos anos, muitas críticas foram feitas contra extensão de prototypes nativos. Aqui está um resumo:

1. À prova de futuro

Caso versões futuras de navegadores implementem Array.prototype.remove (ou por causa de uma atualização do padrão EcmaScript, ou por vontade própria), a implementação da propriedade será sobrescrita pela customizada, que não só será menos eficiente (não é possível manipular interpretadores internos do navegador para a otimização do método) como também poderão ter saídas diferentes e fora do padrão.

Um caso ilustrativo: em 2005, o framework Prototype.js implementava a propriedade Function.prototype.bind. Quatro anos depois, o comitê do Ecma-262 (inspirado pelo Prototype.js) incluiu a propriedade Function.prototype.bind em sua especificação ES 5. Infelizmente, para os usuários do Prototype.js, o novo padrão ES 5 requeria uma funcionalidade adicional, que não era suportada pela versão simples do Prototype.js – por exemplo, o ES 5 especifica que quando uma função de repetição é usada como primeira operação do instanceof, o método interno [[HasInstance]] deve verificar a cadeia de prototypes  da função (ou alvo) original.

 var myObj = {};
var A = function() {};
var ABound = A.bind(myObj);

(new ABound()) instanceof A;
//true (in browsers which faithfully implement ES5 bind)
//false (in the same browsers but with prototype.js loaded)

Da mesma forma, o software que utiliza bibliotecas de terceiros corre o risco de que ocorra uma extensão no prototype nativo (internamente ou por terceiros) que pode ser sobrescrito (ou sobrescrever) uma implementação alternativa de uma mesma propriedade por outra biblioteca.

Essas preocupações podem ser parcialmente mitigadas ao verificar a existência de uma propriedade nativa antes de implementá-la:

 Array.prototype.remove = Array.prototype.remove || function(member) {
var index = this.indexOf(member);
if (index > -1) {
this.splice(index, 1);
}
return this;
}

Esta solução depende da adoção simultânea de novas funcionalidades em navegadores. Se o navegador Chrome implementar primeiro a propriedade Array.prototype.remove, então os outros navegadores ainda utilizarão a implementação interna que pode executar algo completamente diferente. Por este mesmo motivo, Prototype.js terá um problema com esta estratégia:  desde que o Array.prototype.bind não seja implementada na versão do IE 8 (ou em versões anteriores), esses navegadores recorrerão à funcionalidades mais limitadas do Prototype.js.

NOTA: como em Prototype 1.7.1, todas as funções definidas pelo ES 5 devem ser compatíveis com esta especificação.

 2. A estrutura de repetição For-In

A segunda reclamação comumente ouvida, porém mais difícil de explicar, é que estender propriedades nativas atrapalha o ciclo de iteração do objeto. O argumento é algo assim: uma vez que as iterações do for-in verificam todas as propriedades enumeráveis da cadeia de prototype do objeto, propriedades nativas customizadas serão incluídas nas verificações destas iterações:

 Object.prototype.values = function() {
//etc..
};

//later..
var competitors = [];
var results = {'Mary':'23:16', 'Ana':'21:19', 'Evelyn':'22:47'};
for (var prop in results) {
competitors[competitors.length] = prop;
}

competitors; //["Mary", "Ana", "Evelyn", "values"]!!

Há varias razões que sugerem que este medo de estender propriedades nativas é exagerado. Primeiramente, o método hasOwnProperty pode ser usado para filtrar propriedades herdadas.

 var competitors = [];
var results = {'Mary':'23:16', 'Ana':'21:19', 'Evelyn':'22:47'};
for (var prop in results) {
results.hasOwnProperty(prop) && competitors.push(prop);
}

competitors; //["Mary", "Ana", "Evelyn"]

Em segundo lugar, o ES 5 permite que propriedades sejam designadas como não-enumerável e, por isso, estarão imunes na iteração for-in:

 //supporting browsers only (not IE version 8 and earlier)
Object.defineProperty(
Object.prototype, 'values', {enumerable: false});

var competitors = [];
var results = {'Mary':'23:16', 'Ana':'21:19', 'Evelyn':'22:47'};
for (var prop in results) {
competitors[competitors.length] = prop;
}

competitors; //["Mary", "Ana", "Evelyn"]

Por falar nisso, não há razão* para usar um for-in para varrer matrizes (arrays) — utilizar iterações for e while é mais conveniente, flexível e seguro — então, iterações for-in serão afetadas somente ao estender o Object.prototype.

(*Ok, quase nenhuma razão– nunca diga nunca em JavaScript – no caso improvável em que se está sobrecarregado por uma matriz que é esparsa o suficiente para causar uma sobrecarga significativa no desempenho – é o caso de matrizes muito esparsas. Então, interar com o for-in provavelmente irá ajudar. Mas mesmo assim, usar o hasOwnProperty irá protegê-lo de propriedades enumeráveis herdadas.)

 3. Shadowing

Quando se trata de extensão de Object.prototype (em oposição a objetos nativos em geral) há uma outra razão para ser cauteloso. Descendentes de Object.prototype (ou seja, todo objeto cujo prototype não é explicitamente nulo) irão perder o acesso à propriedade estendida, caso já tenha uma propriedade definida com o mesmo nome:

 Object.prototype.archive = function() {
//etc..
}

var concerto = {
composer: 'Mozart',
archive: 'K. 488'
}

concerto.archive();
//TypeError: Property 'archive' of object #<Object> is not a function

Cada vez que vamos definir uma propriedade em Object.prototype estamos, de fato, gerando um termo reservado ad hoc, o que é bastante perigoso quando se trabalha com objetos de versões de extensões antigas e bibliotecas de terceiros.

  • É proibido estender Object.prototype

Por alguns ou todos esses motivos, a comunidade JavaScript considerou as extensões Object.prototype um tabu durante vários anos, e é pouco provável ver essas extensões em código de produção ou em respeitáveis frameworks. Não vou dizer que você nunca deve estender um Object.prototype, mas ao fazer isso, o desenvolvedor será rejeitado pelo clã da comunidade de JavaScript.

  • E quanto aos objetos de Host?

Objetos de host são objetos específicos do fornecedor que não são abrangidos pelo padrão ES – principalmente objetos DOM, tais como Document, Node, Element e Event. Esses objetos não estão bem definidos por qualquer padrão (os padrões W3C – incluindo HTML5 – somente citam interfaces para objetos DOM, mas não exigem a existência de construtores específicos DOM) e tentar colocar extensões ordenadas em cima de um caos oficialmente sancionado é uma receita para dores de cabeça.

Para mais informações sobre os riscos de se estender objetos DOM, veja este artigo, do @kangax.

  • Então, posso estender Nativos em alguma situação?

Aqui foi descrito algumas razões para não estender prototypes nativos, e você deve saber sobre outros motivos. É necessário decidir se cada uma dessas questões será abordada pela sua extensão planejada, e se a extensão acrescentará força e clareza à sua base de código.

Os códigos shims (correções também conhecidas como polyfills) apresentam um bom caso para que se estendam nativos. Um shim é um pedaço de código projetado para conciliar diferenças no comportamento entre ambientes, fornecendo implementações que faltam nos códigos. O suporte ao ES 5 não é igual em navegadores mais antigos, especialmente a versão do IE 8 (e anteriores), que pode ser frustrante para os desenvolvedores que queiram tirar vantagem de funcionalidades mais recentes do ES 5 (tais como Function.prototype.bind e as funções de ordenação de matrizes: forEach, map, filter, etc), mas que também precisam suportar navegadores mais antigos. Aqui está uma parte de um ES 5 Shim bem popular (com os comentários removidos):

 //see https://github.com/kriskowal/es5-shim

if (!Array.prototype.forEach) {
Array.prototype.forEach = function forEach(fun /*, thisp*/) {
var self = toObject(this),
thisp = arguments[1],
i = -1,
length = self.length >>> 0;

if (_toString(fun) != '[object Function]') {
throw new TypeError(); // TODO message
}

while (++i < length) {
if (i in self) {
fun.call(thisp, self[i], i, self);
}
}
};
}

A primeira instrução verifica se o Array.prototype.forEach já está implementado. Se sim, não executa o código customizado. As outras bases também estão abrangidas: todas as propriedades adicionadas aos prototypes nativos são definidas pela norma ES 5, então é seguro assumir que propriedades homônimas não irão colidir futuramente; nenhuma propriedade do ES 5 estende Object.prototype, assim a poluição em enumerações não deve ocorrer. Cada propriedade da ES 5 está bem documentada, por isso não há razão para a ambiguidade sobre a forma como a correção deve ser implementada e é claro que os nomes sejam efetivamente reservados pelo padrão ES 5 (“bind”, “forEach”, etc ).

As extensões ES 5 de shimming fazem sentido. Sem elas, nós somos reféns das inadequações de navegadores menores e somos incapazes de tirar vantagem do padrão definido para a linguagem. Sim, podemos fazer uso de uma funcionalidade equivalente oferecida por bibliotecas bem escritas, como underscore.js, mas ainda estamos presos ao código fora de padrão, assinaturas de métodos invertidas em que os métodos são estáticos e objetos são argumentos extras – uma disposição deselegante para uma linguagem instance-only. Em algum momento, todos os navegadores suportados serão compatíveis com o ES 5, fazendo com que códigos do shim seja removido e seguindo em frente – enquanto os unshimmed devem escolher entre uma grande refatoração ou uma biblioteca de utilitários estáticos fora do padrão.

NOTA: Não é tudo um mar de rosas. Alguns métodos do ES 5 são impossíveis de implementar corretamente quando se usa JavaScript em navegadores mais antigos; Eles falham silenciosamente ou lançam uma exceção. Em outros (como Function.prototype.bind) há vários casos extremos de código que levam muitas iterações para funcionarem corretamente. Como Kris Kowal diz de sua própria biblioteca de shim ES 5: “Tão perto quanto possível do ES5 não é muito perto. Muitos destes shims visam apenas permitir que o código seja escrito para ES5 sem causar erros, com execução em navegadores mais antigos. Em muitos casos, isto significa que estes shims, silenciosamente, fazem muitos métodos para ES5 falharem. Decida com cuidado se é isso que você quer. “

E há uma última coisa para se preocupar…

4. E se todos fizessem isso?

Se você decidir por estender um prototype nativo, surge outro problema: os fornecedores de outra biblioteca podem chegar à mesma conclusão. Cuidados devem ser tomados para não incluir bibliotecas cujas extensões do prototype irão colidir com a sua; a solução mais segura é deixar apenas um framework (ou seu código fonte, ou uma biblioteca incluída) desempenhar o papel de extensor do nativo. No caso de shims do ES isso não deve ser difícil; provavelmente, você não irá escrever o shim você mesmo, então se certifique de que apenas uma biblioteca shim externa está incluída no projeto.

  • Sandboxing

E se pudéssemos ter o nosso próprio objeto Array, String, ou Function privado, que pudéssemos estender e usar sob demanda, sem bagunçar a versão global? Como @jdalton explica, há várias técnicas para a criação de sandboxes nativos – o navegador mais neutro utiliza o IFRAME:

 //Rough and ready version to illustrate technique
//For production-ready version see http://msdn.microsoft.com/en-us/scriptjunkie/gg278167
var sb, iframe = document.createElement('IFRAME');
document.body.appendChild(iframe);
sb = window.frames[1];

//later...
sb.Array.prototype.remove = function(member) {
var index = this.indexOf(member);
if (index > -1) {
this.splice(index, 1);
}
return this;
}

//much later...
var arr = new sb.Array('carrot', 'potato', 'leek');
arr.remove('potato');
arr; //['carrot', 'leek']

//global array is untouched
Array.prototype.remove; //undefined

Os sandboxes nativos, quando bem escritos, oferecem segurança na replicação de extensões nativas entre navegadores. Eles são um compromisso decente, mas são um compromisso do mesmo jeito. Afinal de contas, o poder de extensões de prototoype está na sua capacidade de modificar todas as instâncias de um determinado tipo e fornecer a cada uma delas o acesso ao mesmo comportamento. Com o sandboxing, somos obrigados a saber qual dos nossos casos as instancias da matriz são “super-arrays” e quais são nativos. Os bugs adoram essas incertezas. É também lamentável que os objetos do sandboxing não podem tirar vantagem da notação literal, o que pode acontecer de passagem de parâmetro e declarações de variáveis estranhas.

  •  Wrap Up

JavaScript é uma linguagem prototypical – adicionando uma definição para o prototype o torna imediatamente disponível para todas as instâncias – e os prototypes de seus principais objetos (core objects)estão bem documentados e disponíveis gratuitamente para extensão. Além disso, tudo em JavaScript é uma instância e quando somos forçados (como o jQuery) a envolver os nossos utilitários em wrappers estáticos que jogam contra a linguagem, prendendo nossos utilitários em assinaturas não intuitivas e invertidas.

Não estender prototypes nativos, às vezes, pode parecer algo como “querer ver os dentes de um cavalo dado”, ou como @andrewdupont, desenvolvedor líder de Prototype.js, diz: “deixando o plástico no sofá novo”. Sim, há fortes razões para ser cauteloso e tomar precauções, mas há também situações em que é seguro e benéfico “rasgar todo aquele plástico”.

É bem possível que você esteja trabalhando em uma equipe pequena, ou por si próprio, com total controle sobre o ambiente de programação e sendo capaz de mudar de rumo em curto prazo. Ou talvez o seu projeto não requeira suporte entre navegadores. Ou ainda: a equipe de desenvolvimento seja um pouco mais aplicada do que os fabricantes acreditem ser. O String.prototype.trim foi uma extensão sem problemas em muitas bases de código desenvolvedor – muito antes de ser padronizado pela especificação ES 5 -, chegando a uma certa altura em que era relativamente fácil adicionar um agente para delegar versões nativas, sempre que disponíveis. E nós temos memória curta. O Prototype.js e Mootools não pararam a Internet. Muitos bons projetos de JavaScript foram construídos sobre estes frameworks e extensões pioneiras de prototype, que abriram portas com as quais o ES 5, posteriormente, beneficiou toda a comunidade.

Uma palavra sobre o dogma. Muitos how-tos e guias de estilo de JavaScript proclamam (com absoluta certeza) que estender prototypes nativos é um mal inominável, oferecendo pouco ou nada como forma de provas materiais (além dos avisos alarmistas sobre quebrar os loops for-in, que na realidade eram apenas relevantes para essa relíquia da época passada, conhecida como Object.prototype.myNuttyExtension). Não devemos pedir às pessoas que sigam regras que não podemos explicar ou propor ações que não podemos defender.

Extensões nativas não são nem certas, nem erradas; assim como tantas outras coisas no reino do JavaScript, há mais de cinza, do que de preto e branco. O melhor que podemos fazer é nos informarmos e pesar cada caso pelos seus próprios méritos. Sejam bem conscientes das consequências e jogue bem com os outros, mas sempre que fizer sentido, deixe a linguagem fazer o trabalho para você.

Recursos adicionais:

 ***

Texto original disponível em: http://javascriptweblog.wordpress.com/2011/12/05/extending-javascript-natives/