Разобрать строку JSON в прототип конкретного объекта в JavaScript
Я знаю, как разобрать строку JSON и превратить ее в объект JavaScript. Ты можешь использовать JSON.parse()
в современных браузерах (и IE9+).
Это здорово, но как я могу взять этот объект JavaScript и превратить его в определенный объект JavaScript (то есть с определенным прототипом)?
Например, предположим, у вас есть:
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
Опять же, мне не интересно, как преобразовать строку JSON в общий объект JavaScript. Я хочу знать, как преобразовать строку JSON в объект "Foo". То есть мой объект теперь должен иметь функцию 'test' и свойства 'a' и 'b'.
ОБНОВЛЕНИЕ После некоторых исследований, я подумал об этом...
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);
Будет ли это работать?
ОБНОВЛЕНИЕ Май 2017: "Современный" способ сделать это через Object.assign
, но эта функция недоступна в браузерах Android версии 11 и выше.
16 ответов
Текущие ответы содержат много ручного или библиотечного кода. Это не обязательно.
использование
JSON.parse('{"a":1}')
создать простой объект.Используйте одну из стандартизированных функций для установки прототипа:
Object.assign(new Foo, { a: 1 })
Object.setPrototypeOf({ a: 1 }, Foo.prototype)
См. Пример ниже (этот пример использует собственный объект JSON). Мои изменения комментируются ЗАГЛАВНЫМИ БУКВАМИ:
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
Хотите добавить функциональность сериализации / десериализации JSON, верно? Тогда посмотрите на это:
Вы хотите добиться этого:
toJson () - нормальный метод.
fromJson () - статический метод.
Реализация:
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);
};
Использование:
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
Примечание. При желании вы можете изменить все определения свойств, например: this.title
, this.author
и т. д. var title
, var author
и т. д. и добавьте к ним геттеры, чтобы выполнить определение UML.
Сообщение в блоге, которое я нашел полезным: Понимание прототипов JavaScript
Вы можете связываться со свойством __proto__ объекта.
var fooJSON = jQuery.parseJSON({"a":4, "b": 3});
fooJSON.__proto__ = Foo.prototype;
Это позволяет fooJSON наследовать прототип Foo.
Я не думаю, что это работает в IE, хотя... по крайней мере из того, что я прочитал.
Я что-то упустил в вопросе или почему еще никто не упомянул reviver
параметр JSON.parse
с 2011 года?
Вот упрощенный код для решения, которое работает: 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: Ваш вопрос сбивает с толку: >> Это здорово, но как я могу взять этот объект JavaScript и превратить его в определенный объект JavaScript (т.е. с определенным прототипом)? противоречит названию, где вы спрашиваете о разборе JSON, но в цитируемом абзаце спрашивается о замене прототипа объекта времени выполнения JS.
Принятый в настоящее время ответ не работал у меня. Вам нужно правильно использовать Object.assign():
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`;
}
}
Обычно вы создаете объекты этого класса:
let matt = new Person('matt', 12);
console.log(matt.greet()); // prints "hello my name is matt and i am 12 years old"
Если у вас есть строка json, которую необходимо проанализировать в классе Person, сделайте это так:
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
Мне нравится добавлять необязательный аргумент в конструктор и вызывать Object.assign(this, obj)
затем обрабатывает любые свойства, которые являются объектами или массивами самих объектов:
constructor(obj) {
if (obj != null) {
Object.assign(this, obj);
if (this.ingredients != null) {
this.ingredients = this.ingredients.map(x => new Ingredient(x));
}
}
}
Альтернативный подход мог бы использовать Object.create
, В качестве первого аргумента вы передаете прототип, а для второго вы передаете дескриптору карту имен свойств:
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);
Очень простой способ получить желаемый эффект — добавить атрибут типа при создании строки json и использовать эту строку при анализе строки для создания объекта:
serialize = function(pObject) {
return JSON.stringify(pObject, (key, value) => {
if (typeof(value) == "object") {
value._type = value.constructor.name;
}
return value;
});
}
deSerialize = function(pJsonString) {
return JSON.parse(pJsonString, (key, value) => {
if (typeof(value) == "object" && value._type) {
value = Object.assign(eval('new ' + value._type + '()'), value);
delete value._type;
}
return value;
});
}
Вот небольшой пример использования:
class TextBuffer {
constructor() {
this.text = "";
}
getText = function() {
return this.text;
}
setText = function(pText) {
this.text = pText;
}
}
let textBuffer = new TextBuffer();
textBuffer.setText("Hallo");
console.log(textBuffer.getText()); // "Hallo"
let newTextBuffer = deSerialize(serialize(textBuffer));
console.log(newTextBuffer.getText()); // "Hallo"
Для полноты, вот простая однострочная строка, с которой я закончил (мне не нужно было проверять не-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 }'));
Я создал пакет под названием json-dry. Он поддерживает (круговые) ссылки, а также экземпляры классов.
Вы должны определить 2 новых метода в вашем классе (toDry
на прототип и unDry
как статический метод), зарегистрируйте класс (Dry.registerClass
) и поехали.
Хотя это технически не то, что вы хотите, если вы знаете заранее тип объекта, который вы хотите обработать, вы можете использовать методы call/apply прототипа вашего известного объекта.
Вы можете изменить это
alert(fooJSON.test() ); //Prints 12
к этому
alert(Foo.prototype.test.call(fooJSON); //Prints 12
Я объединил решения, которые мне удалось найти, и скомпилировал его в универсальное решение, которое может автоматически анализировать пользовательский объект и все его поля рекурсивно, чтобы вы могли использовать методы-прототипы после десериализации.
Одно из предположений заключается в том, что вы определили специальное поле, которое указывает его тип в каждом объекте, для которого вы хотите автоматически применить его тип (this.__type
в примере).
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;
}
}
использование:
var responseMsg = //json string of Msg object received;
responseMsg = assignType(responseMsg);
if(responseMsg.isSuccess()){ // isSuccess() is now available
//furhter logic
//...
}
Функция присваивания типов (она работает рекурсивно для назначения типов любым вложенным объектам; она также перебирает массивы для поиска любых подходящих объектов):
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);
}
Вот решение с использованием машинописного текста и декораторов.
- Объекты сохраняют свои методы после десериализации
- Пустые объекты и их дочерние элементы инициализируются по умолчанию.
Как это использовать:
@SerializableClass
class SomeClass {
serializedPrimitive: string;
@SerializableProp(OtherSerializedClass)
complexSerialized = new OtherSerializedClass();
}
@SerializableClass
class OtherSerializedClass {
anotherPrimitive: number;
someFunction(): void {
}
}
const obj = new SomeClass();
const json = Serializable.serializeObject(obj);
let deserialized = new SomeClass();
Serializable.deserializeObject(deserialized, JSON.parse(json));
deserialized.complexSerialized.someFunction(); // this works!
Как это работает
Сериализация:
Сохраните имя типа в прототипе ()
Использовать
JSON.stringify
с методом заменителя, который добавляет в JSON.
Десериализация:
Храните все сериализуемые типы в
Serializable.__serializableObjects
Хранить список сложных типизированных свойств в каждом объекте ()
Инициализировать объект
theObject
через имя типа и__serializableObjects
.Проходить через
theObject.__serializedProps
и пройтись по нему рекурсивно (начать с последнего шага с каждым сериализованным свойством). Назначьте результаты соответствующему свойству.Использовать
Object.assign
чтобы назначить все остальные примитивные свойства.
Код:
// @Class decorator for serializable objects
export function SerializableClass(targetClass): void {
targetClass.prototype.__typeName = targetClass.name;
Serializable.__serializableObjects[targetClass.name] = targetClass;
}
// @Property decorator for serializable properties
export function SerializableProp(objectType: any) {
return (target: {} | any, name?: PropertyKey): any => {
if (!target.constructor.prototype?.__serializedProps)
target.constructor.prototype.__serializedProps = {};
target.constructor.prototype.__serializedProps[name] = objectType.name;
};
}
export default class Serializable {
public static __serializableObjects: any = {};
private constructor() {
// don't inherit from me!
}
static serializeObject(typedObject: object) {
return JSON.stringify(typedObject, (key, value) => {
if (value) {
const proto = Object.getPrototypeOf(value);
if (proto?.__typeName)
value.__typeName = proto.__typeName;
}
return value;
}
);
}
static deserializeObject(typedObject: object, jsonObject: object): object {
const typeName = typedObject.__typeName;
return Object.assign(typedObject, this.assignTypeRecursion(typeName, jsonObject));
}
private static assignTypeRecursion(typeName, object): object {
const theObject = new Serializable.__serializableObjects[typeName]();
Object.assign(theObject, object);
const props = Object.getPrototypeOf(theObject).__serializedProps;
for (const property in props) {
const type = props[property];
try {
if (type == Array.name) {
const obj = object[property];
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; ++i) {
const arrItem = obj[i];
obj[i] = Serializable.assignTypeRecursion(arrItem.__typeName, arrItem);
}
} else
object[property] = [];
} else
object[property] = Serializable.assignTypeRecursion(type, object[property]);
} catch (e) {
console.error(`${e.message}: ${type}`);
}
}
return theObject;
}
}
Комментарии Поскольку я новичок в js/ts (< 10 дней), я буду более чем рад получить любые отзывы/комментарии/предложения. Вот некоторые из моих мыслей:
Это могло бы быть чище: к сожалению, я не нашел способа избавиться от избыточного параметра
@SerializableProp
.
Это может быть более удобно для памяти: после звонка
serializeObject()
каждый объект хранит
__typeName
что может сильно увеличить объем памяти. К счастью
__serializedProps
сохраняется только один раз для каждого класса.
Это может быть более удобно для процессора: это самый неэффективный код, который я когда-либо писал. Но ладно, это только для веб-приложений, так что кого это волнует ;-) Может, стоит хотя бы избавиться от рекурсии.
Почти нет обработки ошибок: ну, это задача на другой день
Ответы Оливерса очень ясны, но если вы ищете решение в angular js, я написал хороший модуль под названием Angular-jsClass, который облегчает эту задачу: объекты, определенные в литеральной нотации, всегда плохи, когда вы стремитесь к большому проекту. но, говоря о том, что разработчики сталкиваются с проблемой, о которой говорил BMiner, как сериализовать json для прототипа или конструктора нотационных объектов
var jone = new Student();
jone.populate(jsonString); // populate Student class with Json string
console.log(jone.getName()); // Student Object is ready to use