Analizar la cadena JSON en un prototipo de objeto particular en JavaScript

173

Sé cómo analizar una cadena JSON y convertirla en un objeto JavaScript. Puede usar JSON.parse()en navegadores modernos (e IE9 +).

Eso es genial, pero ¿cómo puedo tomar ese Objeto JavaScript y convertirlo en un Objeto JavaScript particular (es decir, con un cierto prototipo)?

Por ejemplo, suponga que tiene:

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

Nuevamente, no me pregunto cómo convertir una cadena JSON en un objeto JavaScript genérico. Quiero saber cómo convertir una cadena JSON en un objeto "Foo". Es decir, mi Objeto ahora debería tener una función 'prueba' y propiedades 'a' y 'b'.

ACTUALIZACIÓN Después de investigar un poco, pensé en esto ...

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

¿Eso funcionará?

ACTUALIZACIÓN Mayo de 2017 : La forma "moderna" de hacerlo es a través de Object.assign, pero esta función no está disponible en IE 11 o en navegadores Android anteriores.

BMiner
fuente

Respuestas:

124

Las respuestas actuales contienen mucho código enrollado a mano o de biblioteca. Esto no es necesario.

  1. Use JSON.parse('{"a":1}')para crear un objeto plano.

  2. Use una de las funciones estandarizadas para configurar el prototipo:

    • Object.assign(new Foo, { a: 1 })
    • Object.setPrototypeOf({ a: 1 }, Foo.prototype)
Erik van Velzen
fuente
2
Object.assign no está disponible en navegadores antiguos, incluidos IE y navegadores Android anteriores. kangax.github.io/compat-table/es6/…
BMiner
55
También hay una gran advertencia contra el uso Object.setPrototypeOf(...). developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
christo8989
@SimonEpskamp Ese código no funciona. Verifique su url, el segundo parámetro setPrototypeOfson los descriptores de propiedad.
Erik van Velzen
66
La solución con la configuración del prototipo no funciona si hay alguna propiedad que también necesita tener un prototipo. En otras palabras: solo resuelve el primer nivel de jerarquía de datos.
Vojta
2
Eche un vistazo a mi solución a continuación que aplica Object.assign (..) de forma recursiva que puede resolver automáticamente las propiedades (con un poco de información proporcionada de antemano)
vir us
73

Vea un ejemplo a continuación (este ejemplo usa el objeto JSON nativo). Mis cambios se comentan en CAPITALES:

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
fuente
Supongo que también podrías hacer lo "opuesto" a esto. Construya un objeto Foo en blanco y copie las propiedades de fooJSON en el nuevo objeto Foo. Finalmente, configure fooJSON para que apunte al Objeto Foo.
BMiner
8
Esto es muy peligroso Si el obj tiene un atributo que no está en la definición de Foo, creará un objeto Foo con una propiedad oculta adicional que no conoce su nombre ... En lugar de un bucle, simplemente haré: this.a = obj. ay esto.b = obj.b. O directamente pasaría "a" y "b" como parámetros: nuevo Foo (obj.a, obj.b)
Gabriel Llamas
2
Vale la pena escuchar el consejo de GagleKas. (Aunque "muy peligroso" es un poco OTT). El ejemplo anterior es solo para darle una idea. La implementación correcta dependerá de su aplicación.
Oliver Moran
11
Es posible que desee protegerse de las propiedades del prototipo. for (var prop in obj) {if (obj.hasOwnProperty(prop)) {this[prop] = obj[prop];}}
Romain Vergnory
3
@RomainVergnory Para una mayor seguridad, I propiedades sólo initialize creadas en el constructor, esta vez de obj: for (var prop in obj) {if (this.hasOwnProperty(prop)) {this[prop] = obj[prop];}}. Esto supone que espera que el servidor complete todas las propiedades, la OMI también debería lanzar si obj.hasOwnProperty () falla ...
tekHedd
42

¿Desea agregar la funcionalidad de serialización / deserialización JSON, verdad? Entonces mira esto:

quieres lograr esto:

UML

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

implementación :

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: Si lo desea, puede cambiar todas las definiciones de propiedades como this.title, this.author, etc por var title, var author, etc y añadir captadores a ellos para llevar a cabo la definición de UML.

Gabriel Llamas
fuente
44
Estoy de acuerdo. Esta implementación definitivamente funcionará, y es genial ... solo un poco prolijo y específico para el objeto del libro. En mi humilde opinión, el poder de JS proviene de los prototipos y la capacidad de tener algunas propiedades adicionales si lo desea. Eso es todo lo que digo. Realmente estaba buscando el one-liner: x .__ proto__ = X.prototype; (aunque no es compatible con el navegador IE en este momento)
BMiner
44
No olvide que su toJson()método, independientemente de si tiene propiedades individuales codificadas o utiliza un para cada, necesitará agregar códigos de escape de barra invertida para algunos caracteres que podrían estar en cada propiedad de cadena. (El título de un libro podría tener comillas, por ejemplo)
Nnnnnn
1
Sí, lo sé, mi respuesta fue un ejemplo y la mejor respuesta para la pregunta, pero ... ni siquiera un punto positivo ... no sé por qué pierdo el tiempo ayudando a otros
Gabriel Llamas
77
En estos días lo usaría en JSON.stringify()lugar de escribirle a JSon (). No es necesario reinventar la rueda ahora que todos los navegadores modernos lo admiten.
piedra
2
De acuerdo con @skypecakes. Si solo desea serializar un subconjunto de propiedades, cree una constante de propiedades serializables. serializable = ['title', 'author', ...]. JSON.stringify(serializable.reduce((obj, prop) => {...obj, [prop]: this[prop]}, {}))
Atticus
18

Una publicación de blog que me pareció útil: comprender los prototipos de JavaScript

Puede meterse con la propiedad __proto__ del objeto.

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

Esto permite que fooJSON herede el prototipo de Foo.

Sin embargo, no creo que esto funcione en IE ... al menos por lo que he leído.

BMiner
fuente
2
En realidad, algo así fue mi primer instinto.
Oliver Moran
14
Tenga en cuenta que __proto__ha quedado en desuso por mucho tiempo . Además, por razones de rendimiento, no se recomienda modificar la propiedad interna [[Prototipo]] de un objeto ya creado (mediante configuración __proto__o por cualquier otro medio).
Yu Asakusa
1
Desgraciadamente, ninguna de las soluciones no obsoletas es mucho más compleja que esto ...
Wim Leers
He realizado algunas pruebas de rendimiento de cambio [[prototype]]y parece ser irrelevante en Chrome. En Firefox, llamar a nuevo es más lento que usar un prototipo, y Object.create es más rápido. Supongo que el problema con FF es que la primera prueba es más lenta que la última, solo importa el orden de ejecución. En Chrome todo funciona casi a la misma velocidad. Me refiero al acceso a la propiedad y la invocación de métodos. La creatina es más rápida con las nuevas, pero eso no es tan importante. ver: jsperf.com/prototype-change-test-8874874/1 y: jsperf.com/prototype-changed-method-call
Bogdan Mart el
44
Supongo que en estos días, uno llamaría en Object.setPrototypeOf(fooJSON, Foo.prototype)lugar de configurar fooJSON.__proto__... ¿verdad?
stakx - ya no contribuye
11

¿Me falta algo en la pregunta o por qué nadie mencionó el reviverparámetro JSON.parsedesde 2011?

Aquí hay un código simplista para la solución 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

PD: Su pregunta es confusa: >> Eso es genial, pero ¿cómo puedo tomar ese Objeto JavaScript y convertirlo en un Objeto JavaScript particular (es decir, con un cierto prototipo)? contradice el título, donde pregunta sobre el análisis JSON, pero el párrafo citado pregunta sobre el reemplazo del prototipo de objeto de tiempo de ejecución JS.

Philipp Munin
fuente
3

Se podría usar un enfoque alternativo Object.create. Como primer argumento, pasa el prototipo, y para el segundo pasa un mapa de nombres de propiedades a los descriptores:

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
fuente
3

Me gusta agregar un argumento opcional al constructor y llamar Object.assign(this, obj), luego manejar cualquier propiedad que sea objeto o matriz de objetos en sí:

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

En aras de la exhaustividad, aquí hay una línea simple con la que terminé (no tuve necesidad de verificar las propiedades no 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 }'));
Robar
fuente
2

Creé un paquete llamado json-dry . Admite referencias (circulares) y también instancias de clase.

Debe definir 2 nuevos métodos en su clase ( toDryen el prototipo y unDrycomo método estático), registrar la clase ( Dry.registerClass) y listo.

skerit
fuente
1

Si bien, esto no es técnicamente lo que desea, si conoce de antemano el tipo de objeto que desea manejar, puede usar los métodos de llamada / aplicación del prototipo de su objeto conocido.

puedes cambiar esto

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

a esto

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

Combiné las soluciones que pude encontrar y las compilé en una genérica que puede analizar automáticamente un objeto personalizado y todos sus campos de forma recursiva para que pueda usar métodos prototipo después de la deserialización.

Una suposición es que definió un archivo especial que indica que es tipo en cada objeto que desea aplicar su tipo automáticamente ( this.__typeen el ejemplo).

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
      //...
}

Función de asignación de tipo (funciona de forma recursiva para asignar tipos a cualquier objeto anidado; también itera a través de matrices para encontrar cualquier objeto adecuado):

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
fuente
0

La respuesta actualmente aceptada no estaba funcionando para mí. Necesita usar Object.assign () correctamente:

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

Normalmente crea objetos de esta clase:

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

Si tiene una cadena json que necesita analizar en la clase Persona, hágalo así:

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
fuente
-3

Las respuestas de Olivers son muy claras, pero si está buscando una solución en angular js, he escrito un buen módulo llamado Angular-jsClass que facilita esta tarea, tener objetos definidos en notación literal siempre es malo cuando apunta a un gran proyecto pero diciendo que los desarrolladores enfrentan un problema que exactamente BMiner dijo, cómo serializar un json para prototipos o objetos de notación de constructor

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
fuente