Por que estender objetos nativos é uma má prática?

136

Todo líder de opinião do JS diz que estender os objetos nativos é uma má prática. Mas por que? Temos um sucesso de desempenho? Eles temem que alguém faça "o caminho errado" e adiciona tipos inumeráveis Object, praticamente destruindo todos os loops de qualquer objeto?

Tome TJ Holowaychuk 's should.js por exemplo. Ele adiciona um simples getter para Objecte tudo funciona bem ( fonte ).

Object.defineProperty(Object.prototype, 'should', {
  set: function(){},
  get: function(){
    return new Assertion(Object(this).valueOf());
  },
  configurable: true
});

Isso realmente faz sentido. Por exemplo, pode-se estender Array.

Array.defineProperty(Array.prototype, "remove", {
  set: function(){},
  get: function(){
    return removeArrayElement.bind(this);
  }
});
var arr = [0, 1, 2, 3, 4];
arr.remove(3);

Existem argumentos contra a extensão de tipos nativos?

buschtoens
fonte
9
O que você espera que aconteça quando, mais tarde, um objeto nativo for alterado para incluir uma função "remover" com semântica diferente da sua? Você não controla o padrão.
ta.speot.is
1
Não é o seu tipo nativo. É o tipo nativo de todos.
6
"Eles temem que alguém faça isso da maneira errada" e adiciona tipos inumeráveis ​​a Object, praticamente destruindo todos os loops de qualquer objeto? " : Sim. Nos dias em que essa opinião foi formada, era impossível criar propriedades não enumeráveis. Agora, as coisas podem ser diferentes nesse aspecto, mas imagine cada biblioteca apenas estendendo objetos nativos da forma que eles desejam. Há uma razão pela qual começamos a usar espaços para nome.
Felix Kling
5
Pelo que vale a pena alguns "líderes de opinião", por exemplo, Brendan Eich consideram perfeitamente adequado estender os protótipos nativos.
Benjamin Gruenbaum 29/07
2
Foo não deve ser global nos dias de hoje, temos incluem, RequireJS, commonjs etc.
Jamie Pate

Respostas:

127

Quando você estende um objeto, altera seu comportamento.

Alterar o comportamento de um objeto que será usado apenas pelo seu próprio código está correto. Mas quando você altera o comportamento de algo que também é usado por outro código, existe o risco de que você interrompa esse outro código.

Quando se trata de adicionar métodos às classes de objeto e matriz em javascript, o risco de quebrar algo é muito alto, devido à maneira como o javascript funciona. Longos anos de experiência me ensinaram que esse tipo de coisa causa todos os tipos de erros terríveis em javascript.

Se você precisar de um comportamento personalizado, é muito melhor definir sua própria classe (talvez uma subclasse) em vez de alterar uma nativa. Dessa forma, você não quebrará nada.

A capacidade de alterar como uma classe funciona sem subclassificar é uma característica importante de qualquer boa linguagem de programação, mas é uma que deve ser usada raramente e com cautela.

Abhi Beckert
fonte
2
Então, adicionar algo como .stgringify()seria considerado seguro?
buschtoens
7
Existem muitos problemas, por exemplo, o que acontece se algum outro código também tentar adicionar seu próprio stringify()método com comportamento diferente? Realmente não é algo que você deve fazer na programação diária ... não se tudo o que você quer fazer é salvar alguns caracteres de código aqui e ali. Melhor definir sua própria classe ou função que aceita qualquer entrada e a especifica. A maioria dos navegadores define JSON.stringify(), o melhor seria verificar se isso existe e se não definir você mesmo.
Abhi Beckert
5
Eu prefiro someError.stringify()mais do que isso errors.stringify(someError). É simples e combina perfeitamente com o conceito de js. Estou fazendo algo especificamente vinculado a um determinado ErrorObject.
26612 buschtoens
1
O único argumento (mas bom) que permanece é que algumas macro-libs hediondas podem começar a dominar seus tipos e podem interferir umas nas outras. Ou há mais alguma coisa?
26612 buschtoens
4
Se o problema é que você pode ter uma colisão, você pode usar um padrão em que toda vez que faz isso, sempre confirma que a função está indefinida antes de defini-la? E se você sempre colocar bibliotecas de terceiros antes do seu próprio código, poderá sempre detectar tais colisões.
Dave Cousineau 22/09
32

Não há desvantagem mensurável, como um impacto no desempenho. Pelo menos ninguém mencionou nenhum. Portanto, esta é uma questão de preferência e experiências pessoais.

O principal argumento profissional: parece melhor e é mais intuitivo: açúcar de sintaxe. É uma função específica de tipo / instância, portanto, deve ser especificamente vinculada a esse tipo / instância.

O principal contra-argumento: o código pode interferir. Se a lib A adicionar uma função, ela poderá sobrescrever a função da lib B. Isso pode quebrar o código com muita facilidade.

Ambos têm razão. Quando você confia em duas bibliotecas que alteram diretamente seus tipos, provavelmente terminará com código quebrado, pois a funcionalidade esperada provavelmente não é a mesma. Eu concordo totalmente com isso. As bibliotecas de macros não devem manipular os tipos nativos. Caso contrário, você como desenvolvedor nunca saberá o que realmente está acontecendo nos bastidores.

E é por isso que não gosto de bibliotecas como jQuery, sublinhado, etc. Não me interpretem mal; eles são absolutamente bem programados e funcionam como um encanto, mas são grandes . Você usa apenas 10% deles e entende cerca de 1%.

É por isso que prefiro uma abordagem atomística , onde você só precisa do que realmente precisa. Dessa forma, você sempre sabe o que acontece. As micro-bibliotecas fazem apenas o que você quer que elas façam, para que não interfiram. No contexto em que o usuário final saiba quais recursos foram adicionados, a extensão de tipos nativos pode ser considerada segura.

TL; DR Em caso de dúvida, não estenda os tipos nativos. Somente estenda um tipo nativo se tiver 100% de certeza de que o usuário final conhecerá e desejará esse comportamento. Em nenhum caso, manipule as funções existentes de um tipo nativo, pois isso interromperia a interface existente.

Se você decidir estender o tipo, use Object.defineProperty(obj, prop, desc); se não puder , use os tipos prototype.


Originalmente, eu fiz essa pergunta porque queria que Errors fossem enviados via JSON. Então, eu precisava de uma maneira de especificá-los. error.stringify()me senti muito melhor do que errorlib.stringify(error); como sugere o segundo construto, estou operando errorlibe não sobre errorsi mesmo.

buschtoens
fonte
Estou aberto a outras opiniões sobre isso.
buschtoens
4
Você está sugerindo que jQuery e sublinhado estendem objetos nativos? Eles não. Portanto, se você os está evitando por esse motivo, está enganado.
JLRishe
1
Aqui está uma pergunta que acredito estar perdida no argumento de que estender objetos nativos com a lib A pode entrar em conflito com a lib B: Por que você teria duas bibliotecas que são de natureza tão semelhante ou tão ampla que esse conflito é possível? Ou seja, eu escolho o lodash ou enfatizo não os dois. Nossa paisagem libraray atual em javascript é tão excessivamente saturada e desenvolvedores (em geral) se tornam tão descuidados em adicionando-lhes que acabamos de evitar as melhores práticas para apaziguar nossa Lib Lords
micahblu
2
@micahblu - uma biblioteca pode decidir modificar um objeto padrão apenas para a conveniência de sua própria programação interna (é por isso que as pessoas querem isso). Portanto, esse conflito não é evitado apenas porque você não usaria duas bibliotecas que têm a mesma função.
precisa saber é o seguinte
1
Você também pode (a partir de es2015) criar uma nova classe que estende uma classe existente e usá-la em seu próprio código. portanto, se MyError extends Errorele puder ter um stringifymétodo sem colidir com outras subclasses. Você ainda precisa lidar com erros não gerados pelo seu próprio código, portanto, pode ser menos útil do Errorque com outros.
ShadSterling
16

Na minha opinião, é uma má prática. A principal razão é a integração. Citando documentos should.js:

OMG ESTENDE OBJETO ???!?! @ Sim, sim, sim, com um único getter deveria, e não, não vai quebrar o seu código

Bem, como o autor pode saber? E se minha estrutura de zombaria fizer o mesmo? E se minha lib de promessas fizer o mesmo?

Se você está fazendo isso em seu próprio projeto, tudo bem. Mas para uma biblioteca, é um design ruim. Underscore.js é um exemplo do que foi feito da maneira certa:

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
Stefan
fonte
2
Tenho certeza de que a resposta do TJ seria não usar essas promessas ou frameworks de zombaria: x
Jim Schubert
O _(arr).flatten()exemplo realmente me convenceu a não estender objetos nativos. Minha razão pessoal para fazer isso era puramente sintática. Mas isso satisfaz meu sentimento estético :) Mesmo usando um nome de função mais regular, como foo(native).coolStuff()para convertê-lo em algum objeto "estendido", parece ótimo sintaticamente. Então, obrigado por isso!
egst 27/10/18
16

Se você olhar caso a caso, talvez algumas implementações sejam aceitáveis.

String.prototype.slice = function slice( me ){
  return me;
}; // Definite risk.

A substituição de métodos já criados cria mais problemas do que resolve, e é por isso que geralmente é afirmado, em muitas linguagens de programação, para evitar essa prática. Como os Devs devem saber que a função foi alterada?

String.prototype.capitalize = function capitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // A little less risk.

Nesse caso, não estamos substituindo nenhum método JS básico conhecido, mas estamos estendendo String. Um argumento neste post mencionou como o novo desenvolvedor deve saber se esse método faz parte do JS principal ou onde encontrar os documentos? O que aconteceria se o objeto JS String principal obtivesse um método chamado capitalize ?

E se, em vez de adicionar nomes que possam colidir com outras bibliotecas, você usasse um modificador específico de empresa / aplicativo que todos os desenvolvedores pudessem entender?

String.prototype.weCapitalize = function weCapitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // marginal risk.

var myString = "hello to you.";
myString.weCapitalize();
// => Hello to you.

Se você continuasse estendendo outros objetos, todos os desenvolvedores os encontrariam na natureza com (nesse caso) nós , que os notificariam de que era uma extensão específica da empresa / aplicativo.

Isso não elimina colisões de nomes, mas reduz a possibilidade. Se você determinar que a extensão dos principais objetos JS é para você e / ou sua equipe, talvez seja para você.

SoEzPz
fonte
7

Estender protótipos de built-ins é realmente uma má ideia. No entanto, o ES2015 introduziu uma nova técnica que pode ser utilizada para obter o comportamento desejado:

Utilizando WeakMaps para associar tipos a protótipos internos

A implementação a seguir estende os protótipos Numbere Arraysem tocá-los:

// new types

const AddMonoid = {
  empty: () => 0,
  concat: (x, y) => x + y,
};

const ArrayMonoid = {
  empty: () => [],
  concat: (acc, x) => acc.concat(x),
};

const ArrayFold = {
  reduce: xs => xs.reduce(
   type(xs[0]).monoid.concat,
   type(xs[0]).monoid.empty()
)};


// the WeakMap that associates types to prototpyes

types = new WeakMap();

types.set(Number.prototype, {
  monoid: AddMonoid
});

types.set(Array.prototype, {
  monoid: ArrayMonoid,
  fold: ArrayFold
});


// auxiliary helpers to apply functions of the extended prototypes

const genericType = map => o => map.get(o.constructor.prototype);
const type = genericType(types);


// mock data

xs = [1,2,3,4,5];
ys = [[1],[2],[3],[4],[5]];


// and run

console.log("reducing an Array of Numbers:", ArrayFold.reduce(xs) );
console.log("reducing an Array of Arrays:", ArrayFold.reduce(ys) );
console.log("built-ins are unmodified:", Array.prototype.empty);

Como você pode ver, até protótipos primitivos podem ser estendidos por essa técnica. Ele usa uma estrutura e Objectidentidade de mapa para associar tipos a protótipos internos.

Meu exemplo habilita uma reducefunção que apenas espera um Arraycomo argumento único, porque pode extrair as informações de como criar um acumulador vazio e como concatenar elementos com esse acumulador a partir dos elementos da própria matriz.

Observe que eu poderia ter usado o Maptipo normal , pois as referências fracas não fazem sentido quando elas representam apenas protótipos internos, que nunca são coletados como lixo. No entanto, um WeakMapnão é iterável e não pode ser inspecionado, a menos que você tenha a chave certa. Esse é um recurso desejado, pois quero evitar qualquer forma de reflexão de tipo.


fonte
Este é um uso legal do WeakMap. Tenha cuidado com isso xs[0]em matrizes vazias tho. type(undefined).monoid ...
Obrigado
@naomik eu sei - veja minha pergunta mais recente no trecho de código 2.
Quão padrão é o suporte para este @ftor?
Onassar
5
-1; qual é o sentido disso? Você está apenas criando um tipo de mapeamento de dicionário global separado para métodos e, em seguida, procurando explicitamente os métodos nesse dicionário por tipo. Como conseqüência, você perde o suporte à herança (um método "adicionado" Object.prototypedessa maneira não pode ser chamado em um Array) e a sintaxe é significativamente mais longa / mais feia do que se você realmente estendesse um protótipo. Quase sempre seria mais simples criar apenas classes de utilitários com métodos estáticos; a única vantagem dessa abordagem é um suporte limitado ao polimorfismo.
Mark Amery
4
@ MarkAmery Ei amigo, você não entende. Eu não perco a herança, mas me livre dela. A herança é tão dos anos 80 que você deve esquecê-la. O objetivo desse hack é imitar classes de tipos, que, simplesmente, são funções sobrecarregadas. Existe uma crítica legítima: É útil imitar classes de tipo em Javascript? Não é não. Em vez disso, use uma linguagem digitada que os suporte nativamente. Tenho certeza de que é isso que você quis dizer.
7

Mais uma razão pela qual você não deve estender objetos nativos:

Usamos o Magento, que usa prototype.js e estende muitas coisas sobre objetos nativos. Isso funciona bem até você decidir obter novos recursos e é aí que começam os grandes problemas.

Como apresentamos os componentes da Web em uma de nossas páginas, o webcomponents-lite.js decide substituir todo o objeto de evento (nativo) no IE (por quê?). É claro que isso quebra o prototype.js, que por sua vez quebra o Magento. (até encontrar o problema, você pode investir muitas horas rastreando-o de volta)

Se você gosta de problemas, continue fazendo isso!

Eugen Wesseloh
fonte
4

Eu posso ver três razões para não fazer isso (de dentro de um aplicativo , pelo menos), apenas dois dos quais são abordados nas respostas existentes aqui:

  1. Se você errar, adicionará acidentalmente uma propriedade enumerável a todos os objetos do tipo estendido. Trabalhou facilmente usando Object.defineProperty, o que cria propriedades não enumeráveis ​​por padrão.
  2. Você pode causar um conflito com uma biblioteca que você está usando. Pode ser evitado com diligência; basta verificar quais métodos as bibliotecas que você define definem antes de adicionar algo a um protótipo, verificar as notas de versão ao atualizar e testar seu aplicativo.
  3. Você pode causar um conflito com uma versão futura do ambiente JavaScript nativo.

O ponto 3 é sem dúvida o mais importante. Você pode garantir, por meio de testes, que suas extensões de protótipo não causem conflitos com as bibliotecas usadas, porque você decide quais bibliotecas você usa. O mesmo não ocorre com objetos nativos, supondo que seu código seja executado em um navegador. Se você definir Array.prototype.swizzle(foo, bar)hoje, e amanhã o Google adicionar Array.prototype.swizzle(bar, foo)ao Chrome, é provável que você acabe com alguns colegas confusos que se perguntam por que .swizzleo comportamento de um usuário parece não corresponder ao que está documentado no MDN.

(Veja também a história de como os mootools brincando com protótipos que eles não possuíam forçaram um método ES6 a ser renomeado para evitar quebrar a web .)

Isso é evitável usando um prefixo específico do aplicativo para métodos adicionados a objetos nativos (por exemplo, defina em Array.prototype.myappSwizzlevez de Array.prototype.swizzle), mas isso é meio feio; é tão fácil de resolver usando funções de utilitário independentes em vez de aumentar protótipos.

Mark Amery
fonte
2

Perf também é uma razão. Às vezes, você pode precisar fazer um loop sobre as teclas. Existem várias maneiras de fazer isso

for (let key in object) { ... }
for (let key in object) { if (object.hasOwnProperty(key) { ... } }
for (let key of Object.keys(object)) { ... }

Eu costumo usar for of Object.keys()como ele faz a coisa certa e é relativamente concisa, não há necessidade de adicionar a verificação.

Mas, é muito mais lento .

resultados perf for-of vs for-in

Apenas supor que o motivo Object.keysé lento é óbvio, Object.keys()tem que fazer uma alocação. De fato, o AFAIK precisa alocar uma cópia de todas as chaves desde então.

  const before = Object.keys(object);
  object.newProp = true;
  const after = Object.keys(object);

  before.join('') !== after.join('')

É possível que o mecanismo JS possa usar algum tipo de estrutura de chave imutável, para que Object.keys(object)retorne uma referência que itere sobre chaves imutáveis ​​e que object.newPropcrie um objeto de chaves imutáveis ​​totalmente novo, mas seja o que for, é claramente mais lento em até 15x

Até a verificação hasOwnPropertyé até 2x mais lenta.

O ponto de tudo isso é que, se você tiver um código sensível ao desempenho e precisar fazer um loop sobre as chaves, poderá usar for insem precisar chamar hasOwnProperty. Você só pode fazer isso se não tiver modificadoObject.prototype

observe que se você Object.definePropertymodificar o protótipo se as coisas adicionadas não forem enumeráveis, elas não afetarão o comportamento do JavaScript nos casos acima. Infelizmente, pelo menos no Chrome 83, eles afetam o desempenho.

insira a descrição da imagem aqui

Adicionei 3000 propriedades não enumeráveis ​​apenas para tentar forçar a exibição de quaisquer problemas de perf. Com apenas 30 propriedades, os testes foram muito próximos para dizer se houve algum impacto no desempenho.

https://jsperf.com/does-adding-non-enumerable-properties-affect-perf

O Firefox 77 e o Safari 13.1 não mostraram diferença no perf entre as classes Augmented e Unaugmented. Talvez a v8 seja corrigida nessa área e você possa ignorar os problemas de perf.

Mas deixe-me acrescentar que há a história deArray.prototype.smoosh . A versão curta é a Mootools, uma biblioteca popular, criada por eles mesmos Array.prototype.flatten. Quando o comitê de padrões tentou adicionar um nativo, Array.prototype.flatteneles encontraram o impossível sem quebrar muitos sites. Os desenvolvedores que descobriram sobre o intervalo sugeriram nomear o método es5 smooshcomo uma piada, mas as pessoas se assustaram ao não entender que era uma piada. Eles se estabeleceram em flatvez deflatten

A moral da história é que você não deve estender objetos nativos. Caso contrário, você poderá encontrar o mesmo problema de quebra de suas coisas e, a menos que sua biblioteca em particular seja tão popular quanto o MooTools, é improvável que os fornecedores de navegadores contornem o problema que você causou. Se a sua biblioteca ficar tão popular, seria meio que forçar todo mundo a solucionar o problema que você causou. Portanto, não estenda objetos nativos

gman
fonte
Aguarde, hasOwnPropertyinforma se a propriedade está no objeto ou no protótipo. Mas o for inloop apenas fornece propriedades enumeráveis. Eu apenas tentei isso: adicione uma propriedade não enumerável ao protótipo Array e ele não aparecerá no loop. Não há necessidade não modificar o protótipo para o loop mais simples de trabalho.
ygoe
Mas ainda é uma prática ruim por outros motivos;)
gman