Analisar String JSON em um Protótipo de Objeto Particular em JavaScript

173

Eu sei como analisar uma string JSON e transformá-lo em um objeto JavaScript. Você pode usar JSON.parse()em navegadores modernos (e IE9 +).

Isso é ótimo, mas como posso pegar esse objeto JavaScript e transformá-lo em um objeto JavaScript específico (ou seja, com um determinado protótipo)?

Por exemplo, suponha que você tenha:

function Foo()
{
   this.a = 3;
   this.b = 2;
   this.test = function() {return this.a*this.b;};
}
var fooObj = new Foo();
alert(fooObj.test() ); //Prints 6
var fooJSON = JSON.parse({"a":4, "b": 3});
//Something to convert fooJSON into a Foo Object
//....... (this is what I am missing)
alert(fooJSON.test() ); //Prints 12

Novamente, não estou pensando em como converter uma string JSON em um objeto JavaScript genérico. Quero saber como converter uma string JSON em um objeto "Foo". Ou seja, meu Objeto agora deve ter uma função 'test' e propriedades 'a' e 'b'.

ATUALIZAÇÃO Depois de fazer algumas pesquisas, pensei nisso ...

Object.cast = function cast(rawObj, constructor)
{
    var obj = new constructor();
    for(var i in rawObj)
        obj[i] = rawObj[i];
    return obj;
}
var fooJSON = Object.cast({"a":4, "b": 3}, Foo);

Isso vai funcionar?

ATUALIZAÇÃO maio de 2017 : A maneira "moderna" de fazer isso é via Object.assign, mas essa função não está disponível nos navegadores Android IE 11 ou anteriores.

BMiner
fonte

Respostas:

124

As respostas atuais contêm muitos códigos de biblioteca ou rolados à mão. Isso não é necessário.

  1. Use JSON.parse('{"a":1}')para criar um objeto simples.

  2. Use uma das funções padronizadas para definir o protótipo:

    • Object.assign(new Foo, { a: 1 })
    • Object.setPrototypeOf({ a: 1 }, Foo.prototype)
Erik van Velzen
fonte
2
Object.assign não está disponível em navegadores mais antigos, incluindo o IE e os navegadores Android mais antigos. kangax.github.io/compat-table/es6/…
BMiner 16/05
5
Há também um grande aviso contra o uso Object.setPrototypeOf(...). developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/…
christo8989
@SimonEpskamp Esse código não funciona. Verifique seu URL, o segundo parâmetro a setPrototypeOfser descritores de propriedade.
Erik van Velzen
6
A solução com configuração de protótipo não funciona se houver alguma propriedade que também precise ter protótipo. Em outras palavras: ele resolve apenas o primeiro nível da hierarquia de dados.
Vojta
2
confira minha solução abaixo que aplica Object.assign (..) recursivamente que pode resolver automaticamente propriedades (com um pouco de informação fornecida com antecedência)
vir us
73

Veja um exemplo abaixo (este exemplo usa o objeto JSON nativo). Minhas alterações estão comentadas em CAPITALS:

function Foo(obj) // CONSTRUCTOR CAN BE OVERLOADED WITH AN OBJECT
{
    this.a = 3;
    this.b = 2;
    this.test = function() {return this.a*this.b;};

    // IF AN OBJECT WAS PASSED THEN INITIALISE PROPERTIES FROM THAT OBJECT
    for (var prop in obj) this[prop] = obj[prop];
}

var fooObj = new Foo();
alert(fooObj.test() ); //Prints 6

// INITIALISE A NEW FOO AND PASS THE PARSED JSON OBJECT TO IT
var fooJSON = new Foo(JSON.parse('{"a":4,"b":3}'));

alert(fooJSON.test() ); //Prints 12
Oliver Moran
fonte
Suponho que você possa fazer o "oposto" disso também. Construa um Foo Object em branco e copie as propriedades de fooJSON para o novo Foo Object. Por fim, defina fooJSON para apontar para o objeto Foo.
BMiner 3/11/11
8
Isso é muito perigoso. Se o obj tiver um atributo que não esteja na definição do Foo, você criará um objeto Foo com uma propriedade oculta extra que não conhece seu nome ... Em vez de um loop, eu simplesmente o faço: this.a = obj. a e this.b = obj.b. Ou diretamente eu iria passar "a" e "b" como parâmetros: new Foo (obj.a, obj.b)
Gabriel Lamas
2
Vale a pena ouvir o conselho de GagleKas. (Embora "muito perigoso" seja um pouco OTT.) O exemplo acima é apenas para lhe dar uma idéia. A implementação correta dependerá do seu aplicativo.
Oliver Moran
11
Você pode querer se proteger das propriedades do protótipo. for (var prop in obj) {if (obj.hasOwnProperty(prop)) {this[prop] = obj[prop];}}
Romain Vergnory
3
@RomainVergnory Para ainda mais segurança, I propriedades só Inicializar criados no construtor, isso em vez de obj: for (var prop in obj) {if (this.hasOwnProperty(prop)) {this[prop] = obj[prop];}}. Isso pressupõe que você espera do servidor para preencher todas as propriedades, IMO deve também jogar se obj.hasOwnProperty () falhar ...
tekHedd
42

Deseja adicionar a funcionalidade de serialização / desserialização JSON, certo? Então veja o seguinte:

Você deseja conseguir isso:

UML

toJson () é um método normal.
fromJson () é um método estático.

Implementação :

var Book = function (title, author, isbn, price, stock){
    this.title = title;
    this.author = author;
    this.isbn = isbn;
    this.price = price;
    this.stock = stock;

    this.toJson = function (){
        return ("{" +
            "\"title\":\"" + this.title + "\"," +
            "\"author\":\"" + this.author + "\"," +
            "\"isbn\":\"" + this.isbn + "\"," +
            "\"price\":" + this.price + "," +
            "\"stock\":" + this.stock +
        "}");
    };
};

Book.fromJson = function (json){
    var obj = JSON.parse (json);
    return new Book (obj.title, obj.author, obj.isbn, obj.price, obj.stock);
};

Uso :

var book = new Book ("t", "a", "i", 10, 10);
var json = book.toJson ();
alert (json); //prints: {"title":"t","author":"a","isbn":"i","price":10,"stock":10}

var book = Book.fromJson (json);
alert (book.title); //prints: t

Nota: Se você quiser, pode mudar todas as definições de propriedade, como this.title, this.author, etc por var title, var author, etc. e adicionar getters a eles para realizar a definição da UML.

Gabriel Llamas
fonte
4
Concordo. Esta implementação definitivamente funcionará, e é ótima ... apenas um pouco prolixo e específico para o Objeto de Livro. IMHO, o poder do JS vem dos protótipos e da capacidade de ter algumas propriedades extras, se você quiser. É tudo o que estou dizendo. Eu estava realmente procurando o one-liner: x .__ proto__ = X.prototype; (embora não seja compatível com o navegador IE)
BMiner 3/11/11
4
Não se esqueça que seu toJson()método - independentemente de ter propriedades individuais codificadas ou usar um para cada um - precisará adicionar códigos de escape de barra invertida para alguns caracteres que possam estar em cada propriedade de string. (A título do livro pode ter aspas, por exemplo.)
nnnnnn
1
Sim, eu sei, a minha resposta foi um exemplo e a melhor resposta para a pergunta, mas ... nem mesmo um ponto positivo ... Eu não sei por que eu perder meu tempo ajudando os outros
Gabriel Lamas
7
Hoje em dia eu usaria em JSON.stringify()vez de escrever para JSon (). Não há necessidade de reinventar a roda agora que todos os navegadores modernos a suportam.
stone
2
Concordou com @skypecakes. Se você deseja serializar apenas um subconjunto de propriedades, crie uma constante de propriedades serializáveis. serializable = ['title', 'author', ...]. JSON.stringify(serializable.reduce((obj, prop) => {...obj, [prop]: this[prop]}, {}))
Atticus
18

Um post que achei útil: Noções básicas sobre protótipos JavaScript

Você pode mexer com a propriedade __proto__ do objeto.

var fooJSON = jQuery.parseJSON({"a":4, "b": 3});
fooJSON.__proto__ = Foo.prototype;

Isso permite que o fooJSON herde o protótipo Foo.

Eu não acho que isso funcione no IE, no entanto ... pelo menos pelo que li.

BMiner
fonte
2
Na verdade, algo assim foi o meu primeiro instinto.
Oliver Moran
14
Observe que __proto__há muito tempo foi preterido . Além disso, por motivos de desempenho, não é recomendável modificar a propriedade interna [[Prototype]] de um objeto já criado (por configuração __proto__ou por qualquer outro meio).
Yu Asakusa
1
Infelizmente, nenhuma das soluções realmente não desaprovadas são muito mais complexos do que isso ...
Wim Leers
Fiz alguns testes de desempenho das alterações [[prototype]]e parece irrelevante no Chrome. No firefox, chamar new é mais lento do que usar prototype, e Object.create é mais rápido. Acho que o problema com o FF é que o primeiro teste é mais lento que o anterior, apenas a ordem da execução é importante. No cromo, tudo roda quase na mesma velocidade. Quero dizer acesso à propriedade e nvocation de métodos. A creatina é mais rápida com as novas, mas isso não é tão importante. Veja: jsperf.com/prototype-change-test-8874874/1 e: jsperf.com/prototype-changed-method-call
Bogdan Mart
4
Suponho que hoje em dia, alguém ligaria em Object.setPrototypeOf(fooJSON, Foo.prototype)vez de definir fooJSON.__proto__... certo?
stakx - não está mais contribuindo com
11

Estou faltando alguma coisa na pergunta ou por que mais ninguém mencionou o reviverparâmetro deJSON.parse desde 2011?

Aqui está o código simplista da solução que funciona: https://jsfiddle.net/Ldr2utrr/

function Foo()
{
   this.a = 3;
   this.b = 2;
   this.test = function() {return this.a*this.b;};
}


var fooObj = new Foo();
alert(fooObj.test() ); //Prints 6
var fooJSON = JSON.parse(`{"a":4, "b": 3}`, function(key,value){
if(key!=="") return value; //logic of course should be more complex for handling nested objects etc.
  let res = new Foo();
  res.a = value.a;
  res.b = value.b;
  return res;
});
// Here you already get Foo object back
alert(fooJSON.test() ); //Prints 12

PS: Sua pergunta é confusa: >> Isso é ótimo, mas como posso pegar esse objeto JavaScript e transformá-lo em um objeto JavaScript específico (ou seja, com um certo protótipo)? contradiz o título, onde você pergunta sobre a análise JSON, mas o parágrafo citado pergunta sobre a substituição do protótipo do objeto de tempo de execução JS.

Philipp Munin
fonte
3

Uma abordagem alternativa poderia estar usando Object.create . Como primeiro argumento, você passa o protótipo e, para o segundo, passa um mapa de nomes de propriedades para os descritores:

function SomeConstructor() {
  
};

SomeConstructor.prototype = {
  doStuff: function() {
      console.log("Some stuff"); 
  }
};

var jsonText = '{ "text": "hello wrold" }';
var deserialized = JSON.parse(jsonText);

// This will build a property to descriptor map
// required for #2 argument of Object.create
var descriptors = Object.keys(deserialized)
  .reduce(function(result, property) {
    result[property] = Object.getOwnPropertyDescriptor(deserialized, property);
  }, {});

var obj = Object.create(SomeConstructor.prototype, descriptors);

Matías Fidemraizer
fonte
3

Gosto de adicionar um argumento opcional ao construtor e da chamada Object.assign(this, obj)e depois manipular quaisquer propriedades que sejam objetos ou matrizes de objetos:

constructor(obj) {
    if (obj != null) {
        Object.assign(this, obj);
        if (this.ingredients != null) {
            this.ingredients = this.ingredients.map(x => new Ingredient(x));
        }
    }
}
Jason Goemaat
fonte
2

Por uma questão de completude, eis uma simples linha que eu acabei (não precisei verificar se não há propriedades Foo):

var Foo = function(){ this.bar = 1; };

// angular version
var foo = angular.extend(new Foo(), angular.fromJson('{ "bar" : 2 }'));

// jquery version
var foo = jQuery.extend(new Foo(), jQuery.parseJSON('{ "bar" : 3 }'));
Roubar
fonte
2

Eu criei um pacote chamado json-dry . Ele suporta referências (circulares) e também instâncias de classe.

Você precisa definir 2 novos métodos em sua classe ( toDryno protótipo e unDrycomo método estático), registrar a classe ( Dry.registerClass) e pronto.

skerit
fonte
1

Embora isso não seja tecnicamente o que você deseja, se você souber de antemão o tipo de objeto que deseja manipular, poderá usar os métodos de chamada / aplicação do protótipo do seu objeto conhecido.

você pode mudar isso

alert(fooJSON.test() ); //Prints 12

para isso

alert(Foo.prototype.test.call(fooJSON); //Prints 12
Remus
fonte
1

Combinei as soluções que consegui encontrar e compilei em uma genérica que pode analisar automaticamente um objeto personalizado e todos os seus campos recursivamente, para que você possa usar métodos de protótipo após a desserialização.

Uma suposição é que você definiu um arquivo especial que indica seu tipo em cada objeto que você deseja aplicá-lo automaticamente ( this.__typeno exemplo).

function Msg(data) {
    //... your init code
    this.data = data //can be another object or an array of objects of custom types. 
                     //If those objects defines `this.__type', their types will be assigned automatically as well
    this.__type = "Msg"; // <- store the object's type to assign it automatically
}

Msg.prototype = {
    createErrorMsg: function(errorMsg){
        return new Msg(0, null, errorMsg)
    },
    isSuccess: function(){
        return this.errorMsg == null;
    }
}

uso:

var responseMsg = //json string of Msg object received;
responseMsg = assignType(responseMsg);

if(responseMsg.isSuccess()){ // isSuccess() is now available
      //furhter logic
      //...
}

Função de atribuição de tipo (funciona recursivamente para atribuir tipos a qualquer objeto aninhado; também itera através de matrizes para encontrar objetos adequados):

function assignType(object){
    if(object && typeof(object) === 'object' && window[object.__type]) {
        object = assignTypeRecursion(object.__type, object);
    }
    return object;
}

function assignTypeRecursion(type, object){
    for (var key in object) {
        if (object.hasOwnProperty(key)) {
            var obj = object[key];
            if(Array.isArray(obj)){
                 for(var i = 0; i < obj.length; ++i){
                     var arrItem = obj[i];
                     if(arrItem && typeof(arrItem) === 'object' && window[arrItem.__type]) {
                         obj[i] = assignTypeRecursion(arrItem.__type, arrItem);
                     }
                 }
            } else  if(obj && typeof(obj) === 'object' && window[obj.__type]) {
                object[key] = assignTypeRecursion(obj.__type, obj);
            }
        }
    }
    return Object.assign(new window[type](), object);
}
vírus
fonte
0

A resposta atualmente aceita não estava funcionando para mim. Você precisa usar Object.assign () corretamente:

class Person {
    constructor(name, age){
        this.name = name;
        this.age = age;
    }

    greet(){
        return `hello my name is ${ this.name } and i am ${ this.age } years old`;
    }
}

Você cria objetos desta classe normalmente:

let matt = new Person('matt', 12);
console.log(matt.greet()); // prints "hello my name is matt and i am 12 years old"

Se você possui uma string json, precisa analisar na classe Person, faça o seguinte:

let str = '{"name": "john", "age": 15}';
let john = JSON.parse(str); // parses string into normal Object type

console.log(john.greet()); // error!!

john = Object.assign(Person.prototype, john); // now john is a Person type
console.log(john.greet()); // now this works
Lucas
fonte
-3

As respostas de Olivers são muito claras, mas se você está procurando uma solução em js angulares, escrevi um bom módulo chamado Angular-jsClass que facilita isso, pois ter objetos definidos em notação litaral é sempre ruim quando você está visando um grande projeto mas dizendo que os desenvolvedores enfrentam um problema que exatamente o BMiner disse, como serializar um json para criar objetos de notação de protótipo ou construtor

var jone = new Student();
jone.populate(jsonString); // populate Student class with Json string
console.log(jone.getName()); // Student Object is ready to use

https://github.com/imalhasaranga/Angular-JSClass

imal hasaranga perera
fonte