Analyser la chaîne JSON en un prototype d'objet particulier en JavaScript

173

Je sais comment analyser une chaîne JSON et la transformer en objet JavaScript. Vous pouvez utiliser JSON.parse()dans les navigateurs modernes (et IE9 +).

C'est génial, mais comment puis-je prendre cet objet JavaScript et le transformer en un objet JavaScript particulier (c'est-à-dire avec un certain prototype)?

Par exemple, supposons que vous ayez:

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

Encore une fois, je ne me demande pas comment convertir une chaîne JSON en un objet JavaScript générique. Je veux savoir comment convertir une chaîne JSON en un objet "Foo". Autrement dit, mon objet devrait maintenant avoir une fonction «test» et les propriétés «a» et «b».

MISE À JOUR Après avoir fait quelques recherches, j'ai pensé à ceci ...

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);

Ça marchera?

MISE À JOUR Mai 2017 : La façon «moderne» de faire cela est via Object.assign, mais cette fonction n'est pas disponible dans IE 11 ou les navigateurs Android plus anciens.

BMiner
la source

Réponses:

124

Les réponses actuelles contiennent beaucoup de code roulé à la main ou de bibliothèque. Ce n'est pas nécessaire.

  1. Utilisez JSON.parse('{"a":1}')pour créer un objet simple.

  2. Utilisez l'une des fonctions standardisées pour définir le prototype:

    • Object.assign(new Foo, { a: 1 })
    • Object.setPrototypeOf({ a: 1 }, Foo.prototype)
Erik van Velzen
la source
2
Object.assign n'est pas disponible dans les anciens navigateurs, y compris IE et les anciens navigateurs Android. kangax.github.io/compat-table/es6/…
BMiner
5
Il y a aussi un gros avertissement contre l'utilisation Object.setPrototypeOf(...). developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
christo8989
@SimonEpskamp Ce code ne fonctionne pas. Vérifiez votre URL, le deuxième paramètre à setPrototypeOfêtre des descripteurs de propriété.
Erik van Velzen
6
La solution avec le paramétrage du prototype ne fonctionne pas s'il y a une propriété qui doit également avoir un prototype. En d'autres termes: il ne résout que le premier niveau de hiérarchie des données.
Vojta
2
Découvrez ma solution ci-dessous qui applique Object.assign (..) récursivement qui peut résoudre automatiquement les propriétés (avec un peu d'informations fournies à l'avance)
vir nous
73

Voir un exemple ci-dessous (cet exemple utilise l'objet JSON natif). Mes modifications sont commentées en MAJUSCULES:

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
la source
Je suppose que vous pourriez aussi faire le «contraire» de ceci. Construisez un objet Foo vierge et copiez les propriétés de fooJSON dans le nouvel objet Foo. Enfin, définissez fooJSON pour qu'il pointe vers l'objet Foo.
BMiner
8
C'est très dangereux. Si l'obj a un attribut qui n'est pas dans la définition Foo, vous allez créer un objet Foo avec une propriété cachée supplémentaire dont vous ne connaissez pas son nom ... Au lieu d'une boucle, je ferai simplement: this.a = obj. a et ceci.b = obj.b. Ou directement je passerais "a" et "b" comme paramètres: new Foo (obj.a, obj.b)
Gabriel Llamas
2
Les conseils de GagleKas méritent d'être écoutés. (Bien que "très dangereux" soit un peu OTT.) L'exemple ci-dessus est juste pour vous donner une idée. La mise en œuvre correcte dépendra de votre application.
Oliver Moran
11
Vous voudrez peut-être vous protéger des propriétés du prototype. for (var prop in obj) {if (obj.hasOwnProperty(prop)) {this[prop] = obj[prop];}}
Romain Vergnory
3
@RomainVergnory Pour encore plus de sécurité, je ne propriétés initialize créées dans le constructeur, au lieu de ce obj: for (var prop in obj) {if (this.hasOwnProperty(prop)) {this[prop] = obj[prop];}}. Cela suppose que vous vous attendez à ce que le serveur remplisse toutes les propriétés, l'OMI devrait également lancer si obj.hasOwnProperty () échoue ...
tekHedd
42

Voulez-vous ajouter une fonctionnalité de sérialisation / désérialisation JSON, n'est-ce pas? Ensuite, regardez ceci:

Vous voulez réaliser ceci:

UML

toJson () est une méthode normale.
fromJson () est une méthode statique.

Implémentation :

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);
};

Utilisation :

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

Remarque: Si vous voulez , vous pouvez modifier les définitions de propriétés comme this.title, this.author, etc par var title, var author, etc. et les ajouter getters à accomplir la définition UML.

Gabriel Llamas
la source
4
Je suis d'accord. Cette implémentation fonctionnera certainement, et c'est génial ... juste un peu verbeux et spécifique à l'objet Livre. À mon humble avis, la puissance de JS vient des prototypes et de la possibilité d'avoir des propriétés supplémentaires si vous le souhaitez. C'est tout ce que je dis. Je cherchais vraiment le one-liner: x .__ proto__ = X.prototype; (bien que ce ne soit pas compatible avec le navigateur IE pour le moment)
BMiner
4
N'oubliez pas que votre toJson()méthode - qu'elle ait des propriétés individuelles codées en dur ou qu'elle utilise un pour chacune - devra ajouter des codes d'échappement de barre oblique inverse pour certains caractères qui pourraient être dans chaque propriété de chaîne. (Un titre de livre peut avoir des guillemets, par exemple.)
nnnnnn
1
Oui, je sais, ma réponse était un exemple et la meilleure réponse à la question, mais ... même pas un point positif ... Je ne sais pas pourquoi je perds mon temps à aider les autres
Gabriel Llamas
7
Ces jours-ci, j'utiliserais JSON.stringify()plutôt que d'écrire à JSon () moi-même. Plus besoin de réinventer la roue maintenant que tous les navigateurs modernes la prennent en charge.
pierre
2
D'accord avec @skypecakes. Si vous souhaitez sérialiser uniquement un sous-ensemble de propriétés, créez une constante de propriétés sérialisables. serializable = ['title', 'author', ...]. JSON.stringify(serializable.reduce((obj, prop) => {...obj, [prop]: this[prop]}, {}))
Atticus
19

Un article de blog que j'ai trouvé utile: Comprendre les prototypes JavaScript

Vous pouvez jouer avec la propriété __proto__ de l'objet.

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

Cela permet à fooJSON d'hériter du prototype Foo.

Je ne pense pas que cela fonctionne dans IE, cependant ... du moins d'après ce que j'ai lu.

BMiner
la source
2
En fait, quelque chose comme ça a été mon premier instinct.
Oliver Moran
14
Notez que cela __proto__est obsolète depuis longtemps . De plus, pour des raisons de performances, il n'est pas recommandé de modifier la propriété interne [[Prototype]] d'un objet déjà créé (par paramétrage __proto__ou par tout autre moyen).
Yu Asakusa
1
Hélas, aucune des solutions réellement non obsolètes n'est beaucoup plus complexe que celle-ci…
Wim Leers
J'ai fait quelques tests de performances de changement [[prototype]]et cela ne semble pas pertinent dans Chrome. Dans Firefox, appeler new est plus lent que d'utiliser prototype, et Object.create est le plus rapide. Je suppose que le problème avec FF est que le premier test est plus lent que le dernier, l'ordre d'exécution compte. Dans Chrome, tout fonctionne à peu près à la même vitesse. Je veux dire l'accès à la propriété et la nvocation de méthodes. La créatine est plus rapide avec le nouveau, mais ce n'est pas si important. voir: jsperf.com/prototype-change-test-8874874/1 et: jsperf.com/prototype-changed-method-call
Bogdan Mart
4
Je suppose que ces jours-ci, on appellerait Object.setPrototypeOf(fooJSON, Foo.prototype)au lieu de régler fooJSON.__proto__... non?
stakx - ne contribue plus le
11

Est-ce que je manque quelque chose dans la question ou pourquoi personne n'a mentionné le reviverparamètre de JSON.parsedepuis 2011?

Voici un code simpliste pour une solution qui fonctionne: 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: Votre question prête à confusion: >> C'est génial, mais comment puis-je prendre cet objet JavaScript et le transformer en un objet JavaScript particulier (c'est-à-dire avec un certain prototype)? contredit le titre, où vous posez des questions sur l'analyse JSON, mais le paragraphe cité pose des questions sur le remplacement du prototype d'objet d'exécution JS.

Philipp Munin
la source
3

Une autre approche pourrait être utilisée Object.create. Comme premier argument, vous passez le prototype, et pour le second vous passez une carte des noms de propriétés aux descripteurs:

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
la source
3

J'aime ajouter un argument facultatif au constructeur et appeler Object.assign(this, obj), puis gérer toutes les propriétés qui sont des objets ou des tableaux d'objets eux-mêmes:

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

Par souci d'exhaustivité, voici un simple one-liner avec lequel j'ai fini (je n'avais pas besoin de vérifier les propriétés non 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 }'));
Rob
la source
2

J'ai créé un package appelé json-dry . Il prend en charge les références (circulaires) ainsi que les instances de classe.

Vous devez définir 2 nouvelles méthodes dans votre classe ( toDrysur le prototype et en unDrytant que méthode statique), enregistrer la classe ( Dry.registerClass), et c'est parti.

Skerit
la source
1

Bien que ce ne soit pas techniquement ce que vous voulez, si vous connaissez à l'avance le type d'objet que vous voulez gérer, vous pouvez utiliser les méthodes call / apply du prototype de votre objet connu.

tu peux changer ça

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

pour ça

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

J'ai combiné les solutions que j'ai pu trouver et les ai compilées en une solution générique qui peut analyser automatiquement un objet personnalisé et tous ses champs de manière récursive afin que vous puissiez utiliser des méthodes de prototype après la désérialisation.

Une hypothèse est que vous avez défini un champ spécial qui indique son type dans chaque objet auquel vous souhaitez appliquer son type automatiquement ( this.__typedans l'exemple).

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;
    }
}

usage:

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

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

Fonction d'affectation de type (elle fonctionne de manière récursive pour attribuer des types à tous les objets imbriqués; elle itère également dans les tableaux pour trouver les objets appropriés):

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);
}
virus
la source
0

La réponse actuellement acceptée ne fonctionnait pas pour moi. Vous devez utiliser Object.assign () correctement:

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`;
    }
}

Vous créez normalement des objets de cette classe:

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

Si vous avez une chaîne json que vous devez analyser dans la classe Person, procédez comme suit:

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
Luke
la source
-3

Les réponses d'Olivers sont très claires, mais si vous cherchez une solution en angular js, j'ai écrit un joli module appelé Angular-jsClass qui fait cette facilité, avoir des objets définis en notation litarale est toujours mauvais lorsque vous visez un gros projet mais en disant que les développeurs sont confrontés à un problème, exactement ce que dit BMiner, comment sérialiser un json en objets de notation prototype ou constructeur

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
la source