Como definir o protótipo de um objeto JavaScript que já foi instanciado?

106

Suponha que eu tenha um objeto fooem meu código JavaScript. fooé um objeto complexo e é gerado em outro lugar. Como posso mudar o protótipo do fooobjeto?

Minha motivação é definir protótipos apropriados para objetos serializados de .NET para literais JavaScript.

Suponha que eu tenha escrito o seguinte código JavaScript em uma página ASP.NET.

var foo = <%=MyData %>;

Suponha que MyDataseja o resultado de invocar o .NET JavaScriptSerializerem um Dictionary<string,string>objeto.

Em tempo de execução, isso se torna o seguinte:

var foo = [{"A":"1","B":"2"},{"X":"7","Y":"8"}];

Como você pode ver, footorna-se uma série de objetos. Eu gostaria de poder inicializar foocom um protótipo apropriado. Eu não deseja modificar a Object.prototypenem Array.prototype. Como posso fazer isso?

Rio Vivian
fonte
Você deseja adicionar ao seu protótipo existente ou trocá-lo para um novo protótipo?
SLaks de
Você quer dizer fazer mudanças no protótipo - ou realmente mudar o protótipo como se fosse trocar um protótipo e substituí-lo por outro? Nem tenho certeza se o último caso é possível.
James Gaunt
2
Você quer dizer a propriedade de protótipo explícita ou o link de protótipo implícito ? (Esses dois são duas coisas muito diferentes)
Šime Vidas
Eu estava originalmente preocupado com isso: stackoverflow.com/questions/7013545/…
Vivian River
1
Você está familiarizado com o Backbone extendou com o Google goog.inherit? Muitos desenvolvedores fornecem maneiras de construir herança antes de chamar o newconstrutor idoso - o que é antes de nós recebermos Object.createe não precisamos nos preocupar com a substituição Object.prototype.
Ryan

Respostas:

114

EDITAR fevereiro de 2012: a resposta abaixo não é mais precisa. __proto__ está sendo adicionado ao ECMAScript 6 como "opcional normativo", o que significa que sua implementação não é obrigatória, mas se for, deve seguir o conjunto de regras fornecido. Isso não está resolvido no momento, mas pelo menos fará parte oficialmente da especificação do JavaScript.

Esta questão é muito mais complicada do que parece na superfície, e está além do nível de pagamento da maioria das pessoas no que diz respeito ao conhecimento interno de Javascript.

A prototypepropriedade de um objeto é usada ao criar novos objetos filho desse objeto. Mudá-lo não reflete no próprio objeto, ao invés disso, é refletido quando esse objeto é usado como um construtor para outros objetos, e não tem nenhuma utilidade na mudança do protótipo de um objeto existente.

function myFactory(){};
myFactory.prototype = someOtherObject;

var newChild = new myFactory;
newChild.__proto__ === myFactory.prototype === someOtherObject; //true

Os objetos têm uma propriedade interna [[prototype]] que aponta para o protótipo atual. A maneira como funciona é sempre que uma propriedade em um objeto é chamada, ela começa no objeto e então sobe pela cadeia [[protótipo]] até encontrar uma correspondência, ou falha, após o protótipo raiz do objeto. É assim que o Javascript permite a construção e modificação de objetos em tempo de execução; tem um plano para buscar o que precisa.

A __proto__propriedade existe em algumas implementações (muitas agora): qualquer implementação do Mozilla, todas as do webkit que eu conheço, algumas outras. Esta propriedade aponta para a propriedade interna [[prototype]] e permite modificação pós-criação em objetos. Todas as propriedades e funções serão instantaneamente alteradas para corresponder ao protótipo devido a essa pesquisa em cadeia.

Esse recurso, embora esteja sendo padronizado agora, ainda não é uma parte necessária do JavaScript e, em linguagens que o suportam, tem uma grande probabilidade de derrubar seu código na categoria "não otimizado". Os mecanismos JS têm que fazer o melhor para classificar o código, especialmente o código "quente", que é acessado com frequência, e se você estiver fazendo algo sofisticado como modificar __proto__, eles não otimizarão seu código de forma alguma.

Esta postagem https://bugzilla.mozilla.org/show_bug.cgi?id=607863 discute especificamente as implementações atuais __proto__e as diferenças entre elas. Cada implementação faz isso de maneira diferente, porque é um problema difícil e sem solução. Tudo em Javascript é mutável, exceto a.) A sintaxe b.) Objetos de host (o DOM existe fora do Javascript tecnicamente) e c.) __proto__. O resto está completamente nas mãos de você e de todos os outros desenvolvedores, então você pode ver por que __proto__se destaca como um polegar dolorido.

Há uma coisa que __proto__permite que isso seja impossível de fazer: a designação de um protótipo de objeto em tempo de execução separado de seu construtor. Este é um caso de uso importante e um dos principais motivos pelos quais __proto__ainda não morreu. É importante o suficiente para que tenha sido um ponto de discussão sério na formulação do Harmony, ou em breve será conhecido como ECMAScript 6. A capacidade de especificar o protótipo de um objeto durante a criação fará parte da próxima versão do Javascript e esta será o sino que indica __proto__os dias está formalmente contado.

No curto prazo, você pode usar __proto__se estiver visando navegadores que o suportem (não o IE, e nenhum IE jamais oferecerá). É provável que funcione no webkit e moz pelos próximos 10 anos, pois o ES6 não será finalizado até 2013.

Brendan Eich - re: Abordagem de novos métodos de Objeto no ES5 :

Desculpe, ... mas configurável __proto__, além do caso de uso do inicializador de objeto (ou seja, em um novo objeto ainda não alcançável, análogo ao Object.create do ES5), é uma ideia terrível. Escrevo isto tendo projetado e implementado configurável há __proto__mais de 12 anos.

... a falta de estratificação é um problema (considere os dados JSON com uma chave "__proto__"). E pior, a mutabilidade significa que as implementações devem verificar cadeias de protótipos cíclicos para evitar ilooping. [verificações constantes para recursão infinita são necessárias]

Finalmente, a mutação __proto__em um objeto existente pode interromper métodos não genéricos no novo objeto de protótipo, que não pode funcionar no objeto receptor (direto) que __proto__está sendo definido. Isso é simplesmente uma má prática, uma forma de confusão intencional de tipo, em geral.

pangular
fonte
Excelente repartição! Embora não seja exatamente a mesma coisa que um protótipo mutável, o ECMA Harmony provavelmente implementará proxies , que permitem aplicar funcionalidades adicionais em objetos específicos usando um padrão genérico.
Nick Husher
2
O problema de Brendan Eich é nativo das linguagens de prototipagem como um todo. Possivelmente, fazer __proto__ non-writable, configurableresolveria essas preocupações, forçando o usuário a reconfigurar explicitamente a propriedade. No final, as más práticas estão associadas aos abusos das capacidades de uma linguagem, não das capacidades em si. __Proto__ gravável não é inédito. Existem muitas outras propriedades graváveis ​​não enumeráveis ​​e, embora existam perigos, também existem as melhores práticas. A cabeça de um martelo não deve ser removida simplesmente porque pode ser usada indevidamente para ferir alguém.
Swivel
A mutação __proto__é necessária, porque Object.create produzirá apenas objetos, não funções, por exemplo, nem símbolos, regexes, elementos DOM ou outros objetos hospedeiros. Se você deseja que seus objetos possam ser chamados, ou especiais de outras maneiras, mas ainda assim alterar sua cadeia de protótipo, você está preso sem __proto__proxies configuráveis ou configuráveis
user2451227
2
Seria melhor se não fosse feito com uma propriedade mágica, mas sim com Object.setPrototype, eliminando a __proto__preocupação " em JSON". As outras preocupações de Brendan Eich são ridículas. Cadeias de protótipos recursivas ou usando um protótipo com métodos inadequados para o objeto dado são erros do programador, não falhas de linguagem e não devem ser um fator, e além disso podem acontecer com Object.create tanto quanto com livremente configurável __proto__.
user2451227
1
Suporta o IE 10 __proto__.
kzh de
14

Você pode usar constructorem uma instância de um objeto para alterar o protótipo de um objeto no local. Eu acredito que é isso que você está pedindo para fazer.

Isso significa que se você tem o fooque é uma instância de Foo:

function Foo() {}

var foo = new Foo();

Você pode adicionar uma propriedade bara todas as instâncias de Foo, fazendo o seguinte:

foo.constructor.prototype.bar = "bar";

Aqui está um violino mostrando a prova de conceito: http://jsfiddle.net/C2cpw/ . Não tenho muita certeza de como os navegadores mais antigos se sairão usando essa abordagem, mas tenho quase certeza de que isso deve funcionar muito bem.

Se sua intenção é misturar funcionalidade em objetos, este snippet deve fazer o trabalho:

function mix() {
  var mixins = arguments,
      i = 0, len = mixins.length;

  return {
    into: function (target) {
      var mixin, key;

      if (target == null) {
        throw new TypeError("Cannot mix into null or undefined values.");
      }

      for (; i < len; i += 1) {
        mixin = mixins[i];
        for (key in mixin) {
          target[key] = mixin[key];
        }

        // Take care of IE clobbering `toString` and `valueOf`
        if (mixin && mixin.toString !== Object.prototype.toString) {
          target.toString = mixin.toString;
        } else if (mixin && mixin.valueOf !== Object.prototype.valueOf) {
          target.valueOf = mixin.valueOf;
        }
      }
      return target;
    }
  };
};
Tim
fonte
1
+1: Isso é informativo e interessante, mas realmente não me ajuda a conseguir o que desejo. Estou atualizando minha pergunta para ser mais específico.
Vivian River
Você também pode usarfoo.__proto__.bar = 'bar';
Jam Risser
9

Você pode fazer foo.__proto__ = FooClass.prototype, AFAIK que é compatível com Firefox, Chrome e Safari. Lembre-se de que a __proto__propriedade não é padrão e pode ser desativada em algum momento.

Documentação: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/proto . Consulte também http://www.mail-archive.com/[email protected]/msg00392.html para obter uma explicação de por que não existe Object.setPrototypeOf()e por que __proto__está obsoleto.

Wladimir Palant
fonte
3

Você pode definir sua função de construtor de proxy e, em seguida, criar uma nova instância e copiar todas as propriedades do objeto original para ele.

// your original object
var obj = { 'foo': true };

// your constructor - "the new prototype"
function Custom(obj) {
    for ( prop in obj ) {
        if ( obj.hasOwnProperty(prop) ) {
            this[prop] = obj[prop];
        }
    }
}

// the properties of the new prototype
Custom.prototype.bar = true;

// pass your original object into the constructor
var obj2 = new Custom(obj);

// the constructor instance contains all properties from the original 
// object and also all properties inherited by the new prototype
obj2.foo; // true
obj2.bar; // true

Demonstração ao vivo: http://jsfiddle.net/6Xq3P/

O Customconstrutor representa o novo protótipo, portanto, seu Custom.prototypeobjeto contém todas as novas propriedades que você gostaria de usar com seu objeto original.

Dentro do Customconstrutor, você apenas copia todas as propriedades do objeto original para o novo objeto de instância.

Este novo objeto de instância contém todas as propriedades do objeto original (elas foram copiadas para ele dentro do construtor), e também todas as novas propriedades definidas dentro Custom.prototype(porque o novo objeto é uma Custominstância).

Šime Vidas
fonte
3

Você não pode alterar o protótipo de um objeto JavaScript que já foi instanciado em um navegador cruzado. Como outros mencionaram, suas opções incluem:

  1. mudando a não standard / cross browser __proto__propriedade
  2. Copie as propriedades dos Objetos para um novo objeto

Nenhum deles é particularmente bom, especialmente se você tiver que percorrer recursivamente um objeto em objetos internos para alterar efetivamente o protótipo inteiro de um elemento.

Solução alternativa para questionar

Vou dar uma olhada mais abstrata na funcionalidade que parece que você deseja.

Basicamente, protótipos / métodos permitem apenas uma maneira de agrupar funções com base em um objeto.
Em vez de escrever

function trim(x){ /* implementation */ }
trim('   test   ');

você escreve

'   test  '.trim();

A sintaxe acima foi cunhada como OOP por causa da sintaxe object.method (). Algumas das principais vantagens de OOPs sobre a programação funcional tradicional incluem:

  1. Nomes de métodos curtos e menos variáveis obj.replace('needle','replaced')vs ter que lembrar nomes como str_replace ( 'foo' , 'bar' , 'subject')e a localização das diferentes variáveis
  2. método chaining ( string.trim().split().join()) é potencialmente mais fácil de modificar e escrever do que funções aninhadasjoin(split(trim(string))

Infelizmente em JavaScript (como mostrado acima) você não pode modificar um protótipo já existente. Idealmente acima, você poderia modificar Object.prototypeapenas para os objetos fornecidos acima, mas, infelizmente, a modificação Object.prototypepoderia quebrar scripts (resultando em colisão e substituição de propriedades).

Não há meio termo comumente usado entre esses 2 estilos de programação e nenhuma maneira OOP de organizar funções personalizadas.

O UnlimitJS fornece um meio-termo que permite definir métodos personalizados. Evita:

  1. Colisão de propriedade, porque não estende os protótipos dos objetos
  2. Ainda permite uma sintaxe de encadeamento OOP
  3. É um script de navegador cruzado de 450 bytes (IE6 +, Firefox 3.0 +, Chrome, Opera, Safari 3.0+) que o Unlimit tem muitos problemas de colisão de propriedade de protótipo do JavaScript

Usando seu código acima, eu simplesmente criaria um namespace de funções que você pretende chamar no objeto.

Aqui está um exemplo:

var foo = [{"A":"1","B":"2"},{"X":"7","Y":"8"}];

// define namespace with methods
var $ = {
  log:function(){
    console.log(this);
    return this;
  }[Unlimit](),
  alert:function(){
    alert(''+this);
  }[Unlimit]()
}


foo[$.log]()
   [$.log]()
   [$.alert]();

Você pode ler mais exemplos aqui UnlimitJS . Basicamente, quando você chama [Unlimit]()uma função, permite que a função seja chamada como um método em um objeto. É como um meio-termo entre o OOP e as estradas funcionais.

Lima
fonte
2

Você não pode mudar a [[prototype]]referência de objetos já construídos, pelo que eu sei. Você poderia alterar a propriedade prototype da função do construtor original, mas, como você já comentou, esse construtor é Object, e alterar as construções JS principais é uma coisa ruim.

Você pode criar um objeto proxy do objeto construído que implementa a funcionalidade adicional de que você precisa. Você também pode fazer um monkeypatch de métodos e comportamentos adicionais atribuindo diretamente ao objeto em questão.

Talvez você consiga o que deseja de outra maneira, se estiver disposto a abordar de um ângulo diferente: O que você precisa fazer para mexer no protótipo?

Nick Husher
fonte
Alguém mais pode comentar sobre a precisão disso?
Vivian River de
Parece, com base neste terrível quebra-cabeça, que você pode alterar o protótipo de um objeto existente alterando sua __proto__propriedade. Isso só funcionará em navegadores que suportam a __proto__notação, que é o Chrome e o Firefox, e está obsoleto . Portanto, em resumo, você pode alterar a configuração [[prototype]]de um objeto, mas provavelmente não deveria.
Nick Husher
1

Se você conhece o protótipo, por que não injetá-lo no código?

var foo = new MyPrototype(<%= MyData %>);

Então, uma vez que os dados são serializados, você obtém

var foo = new MyPrototype([{"A":"1","B":"2"},{"X":"7","Y":"8"}]);

agora você só precisa de um construtor que tenha um array como argumento.

reescrito
fonte
0

Não há como realmente herdar Arrayou "subclassificar" isso.

O que você pode fazer é o seguinte ( AVISO: FESTERING CODE AHEAD ):

function Foo(arr){
  [].push.apply(this, arr)
}
Foo.prototype = []
Foo.prototype.something = 123

var foo = new Foo(<%=MyData %>)

foo.length // => 2
foo[0] // => {"A":"1","B":"2"}
foo.something // => 123

Isso funciona, mas causará certos problemas para qualquer pessoa que cruzar seu caminho (parece um array, mas as coisas darão errado se você tentar manipulá-lo).

Por que você não segue o caminho lógico e adiciona métodos / propriedades diretamente fooou usa um construtor e salva seu array como uma propriedade?

function Foo(arr){
  this.items = arr
}
Foo.prototype = {
  someMethod : function(){ ... }
  //...
}

var foo = new Foo(<%=MyData %>)
foo.items // => [{"A":"1","B":"2"},{"X":"7","Y":"8"}]
Ricardo Tomasi
fonte
0

se você quiser criar um protótipo em tempo real, este é um dos caminhos

function OntheFlyProto (info){
    this.items = info;
    this.y =-1;
    for(var i = 0; i < this.items.length ; i++){
        OntheFlyProto.prototype["get"+this.items[i].name] = function (){
            this.y++;
            return this.items[this.y].value;
        }
    }
}

var foo = [{name:"one", value:1},{name:"two", value:2}];
v = new OntheFlyProto(foo);
Yene Mulatu
fonte
-1
foo.prototype.myFunction = function(){alert("me");}
PhD
fonte
Não, você não pode. Essa é uma propriedade estática.
SLaks de
@SLaks prototypeé estático? Não tenho certeza do que você quer dizer.
Šime Vidas
1
prototypeé uma propriedade de uma função, não de um objeto. Object.prototypeexiste; {}.prototypenão.
SLaks de
Se você não se preocupa com a compatibilidade do navegador, pode usar Object.getPrototypeOf(foo)para obter o objeto de protótipo do construtor. Você pode modificar as propriedades para modificar o protótipo do objeto. Ele só funciona em navegadores recentes, mas não pode dizer quais.
Nick Husher
@TeslaNick: Você pode simplesmente ir e modificar Object.prototypediretamente porque é mais provável que Object.getPrototypeOf(foo)isso retorne. Não é muito útil.
Wladimir Palant