I have class with property this.eyes. Code Below. As you can see in first case in data.num saved value in second reference. In my project I have no needs to array, so I need somehow to make first sample to save reference like second one. Any idea how?
// Warning! Pseudo Code
// Sample One
class Human {
constructor() {
this.eyes = null;
}
addEye() {
this.eyes = 1;
}
}
const william = new Human();
const data = { num: william.eyes }
william.addEye();
// data = { num: null }
// Warning! Pseudo Code
// Sample One
class Human {
constructor() {
this.eyes = [];
}
addEye() {
this.eyes.push(1);
}
}
const william = new Human();
const data = { num: william.eyes }
william.addEye();
// data = { num: [1] }
JavaScript has a small set of built-in types, one of those is number which is a "value type" (and as far as I can tell, your human.eyes value is always null or a number value).
JavaScript does not support references to number values, so after this data = { num: william.eyes } the data.num value will be a copy of william.eyes and not a reference as you have correctly surmised.
However, JavaScript does support non-trivial properties with custom getter/setter logic (Object.defineProperty). You could use this, in conjunction with a reference to the william object, to have the behaviour you want:
const william = new Human();
const data = {}; // new empty object
Object.defineProperty( data, 'num', {
enumerable: true,
configurable: true,
writable: true,
get: function() { return this.human.eyes; },
set: function(newValue) { this.human.eyes = newValue; }
} );
// Need to give `data` a reference to the `Human`:
data.human = william;
william.addEye();
william.addEye();
console.log( data.num ); // outputs "2"
data.num++;
console.log( william.eyes ); // outputs "3"
If I got what you trying to do here correctly , the following should work for you :
class Human {
constructor() {
this.eyes = 0;
}
addEye() {
this.eyes += 1;
}
}
Related
Problem:
According https://javascript.info/property-descriptors
property descriptors are like objects with own properties like:
{
value:
writable:
readable:
configurable:
enumerable:
}
one can do this with normal object:
for(var n in object) {
console.log(object[n]);
}
to write out the values of the keys in that object.
I want to do the same. Get descriptors, then for each descriptor print the value.
This is one of several ways I tried to do it. For each of the Property Descriptor in descriptors, print out the value of its properties.
I imagined getting something like
"John Doe"
0
etc...
Basically, put in other words, using Descriptors to show the values stored within an object.
Own tryes:
I tryed several times, so many times and ways that I
actually forgot, had to delete to to keep my head somewhat clear from the clutter of previous tryes. It resulted in all kinds of imaginable output except of what I expected. like numbers: 0, 1, 2, 3, 4... names of the properties instead of their values, or rows of "undefined" whilst I was expecting something like
"John Doe"
0
function
function
10 etc...
I DID check stackoverflow, AND other pages as well.
http://speakingjs.com/es5/ch17.html#property_attributes
being just one of say, 3-4.
It is NOT a homework, Also I do belive its probably not the best practise in doing it this way, using descriptors, but I got frustrated and obsessed, because I feel it should be possible.
I also apologize for eventuall weirdness in code. Im new at Javascript. I hope people focus on the problem and not on other stuff. English isnt my first language. I DID try to make the code as short as possible, and replicable. Hopefully Its clear what I want to do, otherwise Ill probably just give up xD.
Code
"use strict";
function Person() {
this._name = "";
this.age = 0;
this.greet = function() {
console.log(`hi Im ${this._name} and im ${this.age} years old`);
};
this.beep = function(times) {
for(var i = 0; i < times; i++) {
console.log("beeeep");
}
};
Object.defineProperty(this, "something", {
value: 10,
writable: false,
readable: true,
configurable: false,
enumerable: true,
});
Object.defineProperty(this, "name", {
get() {
return this._name;
},
set(nam) {
this._name = nam;
}
} );
}
var obj = new Person();
console.log(obj.something);
obj.name = "John Doe";
console.log(obj.name);
console.log("##############");
var properties = Object.getOwnPropertyDescriptors(obj);
for(var property in properties) {
for(var val in property) {
console.log(property[val]);
}
}
property is a key not a value, use that key to access the related value of the object:
for(var property in properties) {
for(var val in properties[property]) {
console.log(property, val, properties[property][val]);
}
}
property in your example at the end is a string, the name of the property in the object returned by getOwnPropertyDescriptors. You want the value of that property for your second loop:
var properties = Object.getOwnPropertyDescriptors(obj);
for(var property in properties) {
var descriptor = properties[property]; // ***
for(var val in descriptor) {
// *** --------^^^^^^^^^^
console.log(property[val]);
}
}
Or in a modern environment you'd probably use Object.values or Object.entries instead:
const properties = Object.getOwnPropertyDescriptors(obj);
for (const [propName, descriptor] of Object.entries(properties)) {
console.log(`${propName}:`);
for (const [key, value] of Object.entries(descriptor)) {
console.log(` ${key}: ${value}`);
}
}
Live Example:
"use strict";
function Person() {
this._name = "";
this.age = 0;
this.greet = function() {
console.log(`hi Im ${this._name} and im ${this.age} years old`);
};
this.beep = function(times) {
for(var i = 0; i < times; i++) {
console.log("beeeep");
}
};
Object.defineProperty(this, "something", {
value: 10,
writable: false,
readable: true,
configurable: false,
enumerable: true,
});
Object.defineProperty(this, "name", {
get() {
return this._name;
},
set(nam) {
this._name = nam;
}
} );
}
var obj = new Person();
console.log(obj.something);
obj.name = "John Doe";
console.log(obj.name);
console.log("##############");
const properties = Object.getOwnPropertyDescriptors(obj);
for (const [propName, descriptor] of Object.entries(properties)) {
console.log(`${propName}:`);
for (const [key, value] of Object.entries(descriptor)) {
console.log(` ${key}: ${value}`);
}
}
.as-console-wrapper {
max-height: 100% !important;
}
Own answer, Also clearly showing what I wanted to do
Apparently its almost encouraged to answer own questions.
What threw me off was that one sort of needs to "backreference" inside the for loops. (Perhaps due to learning Java before).
To get property value one needs to mention the object that contains it.
descriptions[property]
and to get what is inside that you need in turn write
descriptions[property][whatever]
Anyways. If anyone wondered, heres the now working code, albeight with slightly different variable names. But then again, shorter and easyer to follow. Shows exactly what I was looking for :D
"use strict";
function User(name, age) {
this.name = name;
this.age = age;
this.func = function() {
console.log("hola");
};
}
var person = new User("John Doe", 22);
var descriptors = Object.getOwnPropertyDescriptors(person);
for(var property in descriptors) {
console.log(property);
for(var k in descriptors[property]) { //<-------------
console.log(k + " " + descriptors[property][k]);//<-----------
}
console.log("_______________");
}
I denoted what I previously didnt get with arrows in the comments.
Q1: Can someone explain how to trigger setter in defineProperty, using it via function by this way?
Q2: How to get last key in setter?
fiddle is here
function test(root) {
Object.defineProperty(this, 'subtree', {
get: function() {
console.log("get");
return root.subtree;
},
set: function(value) { //doesn't triggered
console.log("set");
root.subtree = value;
}
});
}
var demo = new test({
subtree: {
state: null,
test: 1
}
});
console.log("START", demo.subtree);
demo.subtree.state = 13; // doesn't triggered setter, but change object
console.log("END", demo.subtree);
To make it simpler, this code
let variable = null;
let obj = {
set variable(value) {
variable = value;
}
get variable() {
return variable;
}
};
obj.variable = {a: 5};
console.log(obj.variable);
does exactly the same thing as this one
let variable = null;
let obj = {
setVariable(value) {
variable = value;
}
getVariable() {
return variable;
}
};
obj.setVariable({a: 5}); // equivalent to obj.variable = {a: 5}
console.log(obj.getVariable()); // equivalent to obj.variable
but the latter clearly shows what's going on.
We want to access a and set it to some value
console.log(obj.getVariable().a); // get a
obj.getVariable().a = 6; // set a!
Notice that we don't call setVariable to set a's value!!! This is exactly what happens in your code. You get subtree and set state to 13. To call setter, you do the following
obj.setVariable({a: 6});
obj.variable = {a: 6}; // getter/setter example
demo.subtree = {state: 13}; // your code
This and this (linked by you) present how scopes and capturing work, so you should get your hands on some book that covers all those things (or browse SO, there are (probably) plenty of questions about that).
I have two IFFE:
var Helper = (function () {
return {
number: null,
init: function (num) {
number = num;
}
}
})();
var Helper2 = (function () {
return {
options: {
number: [],
},
init: function(num){
this.options.number = num;
},
getData: function () {
return this.options.number;
}
}
})();
Helper2.init(Helper.number);
console.log(Helper2.getData());
Helper.init(5);
console.log(Helper2.getData());
What I want is
Helper2.init(Helper.number);
console.log(Helper2.getData()); // null
Helper.init(5);
console.log(Helper2.getData()); // 5
what I get is
Helper2.init(Helper.number);
console.log(Helper2.getData()); // null
Helper.init(5);
console.log(Helper2.getData()); // null
What techniques can be done to have it pass by reference, if it can?
JSBIN: https://jsbin.com/gomakubeka/1/edit?js,console
Edit: Before tons of people start incorporating different ways to have Helper2 depend on Helper, the actual implementation of Helper is unknown and could have 100's of ways they implement the number, so Helper2 needs the memory address.
Edit 2: I suppose the path I was hoping to get some start on was knowing that arrays/objects do get passed by reference, how can I wrap this primitive type in such a way that I can use by reference
Passing by reference in JavaScript can only happen to objects.
The only thing you can pass by value in JavaScript are primitive data types.
If on your first object you changed the "number:null" to be nested within an options object like it is in your second object then you can pass a reference of that object to the other object. The trick is if your needing pass by reference to use objects and not primitive data types. Instead nest the primitive data types inside objects and use the objects.
I altered you code a little bit but I think this works for what you were trying to achieve.
var Helper = function (num) {
return {
options: {
number: num
},
update: function (options) {
this.options = options;
}
}
};
var Helper2 = function (num) {
return {
options: {
number: num,
},
update: function(options){
this.options = options;
},
getData: function () {
return this.options.number;
}
}
};
var tempHelp = new Helper();
var tempHelp2 = new Helper2();
tempHelp2.update(tempHelp.options);
tempHelp.options.number = 5;
console.log(tempHelp2.getData());
First of all why doesn't it work:
helper is a self activating function that returns an object. When init is called upon it sets an number to the Helper object.
Then in Helper2 you pass an integer (Helper.number) to init setting the object to null. So you're not passing the reference to Helper.number. Only the value set to it.
You need to pass the whole object to it and read it out.
An example:
var Helper = (function () {
return {
number: null,
init: function (num) {
this.number = num; //add this
}
}
})();
var Helper2 = (function () {
return {
options: {
number: [],
},
init: function(obj){
this.options = obj; //save a reference to the helper obj.
},
getData: function () {
if (this.options.number)
{
return this.options.number;
}
}
}
})();
Helper2.init(Helper); //store the helper object
console.log(Helper2.getData());
Helper.init(5);
console.log(Helper2.getData());
I don't think you're going to be able to get exactly what you want. However, in one of your comments you said:
Unfortunately interfaces aren't something in javascript
That isn't exactly true. Yes, there's no strong typing and users of your code are free to disregard your suggestions entirely if you say that a function needs a specific type of object.
But, you can still create an interface of sorts that you want users to extend from in order to play nice with your own code. For example, you can tell users that they must extend from the Valuable class with provides a mechanism to access a value computed property which will be a Reference instance that can encapsulate a primitive (solving the problem of not being able to pass primitive by reference).
Since this uses computed properties, this also has the benefit of leveraging the .value notation. The thing is that the .value will be a Reference instead of the actual value.
// Intermediary class that can be passed around and hold primitives
class Reference {
constructor(val) {
this.val = val;
}
}
// Interface that dictates "value"
class Valuable {
constructor() {
this._value = new Reference();
}
get value() {
return this._value;
}
set value(v) {
this._value.val = v;
}
}
// "Concrete" class that implements the Valuable interface
class ValuableHelper extends Valuable {
constructor() {
super();
}
}
// Class that will deal with a ValuableHelper
class Helper {
constructor(n) {
this.options = {
number: n
}
}
getData() {
return this.options.number;
}
setData(n) {
this.options.number = n;
}
}
// Create our instances
const vh = new ValuableHelper(),
hh = new Helper(vh.value);
// Do our stuff
console.log(hh.getData().val);
vh.value = 5;
console.log(hh.getData().val);
hh.setData(vh.value);
vh.value = 5;
I have an object that contains a getter.
myObject {
id: "MyId",
get title () { return myRepository.title; }
}
myRepository.title = "MyTitle";
I want to obtain an object like:
myResult = {
id: "MyId",
title: "MyTitle"
}
I don't want to copy the getter, so:
myResult.title; // Returns "MyTitle"
myRepository.title = "Another title";
myResult.title; // Should still return "MyTitle"
I've try:
$.extend(): But it doesn't iterate over getters. http://bugs.jquery.com/ticket/6145
Iterating properties as suggested here, but it doesn't iterate over getters.
As I'm using angular, using Angular.forEach, as suggested here. But I only get properties and not getters.
Any idea? Thx!
Update
I was setting the getter using Object.defineProperty as:
"title": { get: function () { return myRepository.title; }},
As can be read in the doc:
enumerable true if and only if this property shows up during
enumeration of the properties on the corresponding object. Defaults to
false.
Setting enumerable: true fix the problem.
"title": { get: function () { return myRepository.title; }, enumerable: true },
$.extend does exactly what you want. (Update: You've since said you want non-enumerable properties as well, so it doesn't do what you want; see the second part of this answer below, but I'll leave the first bit for others.) The bug isn't saying that the resulting object won't have a title property, it's saying that the resulting object's title property won't be a getter, which is perfect for what you said you wanted.
Example with correct getter syntax:
// The myRepository object
const myRepository = { title: "MyTitle" };
// The object with a getter
const myObject = {
id: "MyId",
get title() { return myRepository.title; }
};
// The copy with a plain property
const copy = $.extend({}, myObject);
// View the copy (although actually, the result would look
// the same either way)
console.log(JSON.stringify(copy));
// Prove that the copy's `title` really is just a plain property:
console.log("Before: copy.title = " + copy.title);
copy.title = "foo";
console.log("After: copy.title = " + copy.title);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
Syntax fixes:
Added missing variable declarations, =, and ;
Removed duplicate property title
Corrected the getter declaration syntax
If you want to include non-enumerable properties, you'll need to use Object.getOwnPropertyNames because they won't show up in a for-in loop, Object.keys, or $.extend (whether or not they're "getter" or normal properties):
// The myRepository object
const myRepository = { title: "MyTitle" };
// The object with a getter
const myObject = {
id: "MyId",
};
Object.defineProperty(myObject, "title", {
enumerable: false, // it's the default, this is just for emphasis,
get: function () {
return myRepository.title;
},
});
console.log("$.extend won't visit non-enumerable properties, so we only get id here:");
console.log(JSON.stringify($.extend({}, myObject)));
// Copy it
const copy = {};
for (const name of Object.getOwnPropertyNames(myObject)) {
copy[name] = myObject[name];
}
// View the copy (although actually, the result would look
// the same either way)
console.log("Our copy operation with Object.getOwnPropertyNames does, though:");
console.log(JSON.stringify(copy));
// Prove that the copy's `title` really is just a plain property:
console.log("Before: copy.title = " + copy.title);
copy.title = "foo";
console.log("After: copy.title = " + copy.title);
.as-console-wrapper {
max-height: 100% !important;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
First of all, fix your syntax, though it probably is good in your actual code:
myObject = {
id: "MyId",
get title () { return myRepository.title; }
}
Now, to the answer. :)
You can just use a for..in loop to get all the properties, then save them as-is:
var newObj = {};
for (var i in myObject) {
newObj[i] = myObject[i];
}
No jQuery, Angular, any other plugins needed!
I had the same issue but in TypeScript and the method mentioned by T.J. Crowder didnt work.
What did work was the following:
TypeScript:
function copyObjectIncludingGettersResult(originalObj: any) {
//get the properties
let copyObj = Object.getOwnPropertyNames(originalObj).reduce(function (result: any, name: any) {
result[name] = (originalObj as any)[name];
return result;
}, {});
//get the getters values
let prototype = Object.getPrototypeOf(originalObj);
copyObj = Object.getOwnPropertyNames(prototype).reduce(function (result: any, name: any) {
//ignore functions which are not getters
let descriptor = Object.getOwnPropertyDescriptor(prototype, name);
if (descriptor?.writable == null) {
result[name] = (originalObj as any)[name];
}
return result;
}, copyObj);
return copyObj;
}
Javascript version:
function copyObjectIncludingGettersResult(originalObj) {
//get the properties
let copyObj = Object.getOwnPropertyNames(originalObj).reduce(function (result, name) {
result[name] = originalObj[name];
return result;
}, {});
//get the getters values
let prototype = Object.getPrototypeOf(originalObj);
copyObj = Object.getOwnPropertyNames(prototype).reduce(function (result,name) {
let descriptor = Object.getOwnPropertyDescriptor(prototype, name);
if (descriptor?.writable == null) {
result[name] = originalObj[name];
}
return result;
}, copyObj);
return copyObj;
}
Its easy to load JSON into an object in javascript using eval or JSON.parse.
But if you have a proper "class" like function, how do you get the JSON data into it?
E.g.
function Person(name) {
this.name=name;
this.address = new Array();
this.friendList;
this.promote = function(){
// do some complex stuff
}
this.addAddress = function(address) {
this.address.push(address)
}
}
var aPersonJSON = '{\"name\":\"Bob\",\"address\":[{\"street\":\"good st\",\"postcode\":\"ADSF\"}]}'
var aPerson = eval( "(" + aPersonJSON + ")" ); // or JSON.parse
//alert (aPerson.name); // Bob
var someAddress = {street:"bad st",postcode:"HELL"};
//alert (someAddress.street); // bad st
aPerson.addAddress(someAddress); // fail!
The crux is I need to be able to create proper Person instances from JSON, but all I can get is a dumb object. Im wondering if its possible to do something with prototypes?
I dont want to have to parse each line of the JSON and assign each variable to the coresponding functions attributes, which would be too difficult. The actualy JSON and functions I have are much more complicated than the example above.
I am assuming one could JSONify the functions methods into the JSON string, but as I need to keep the resultant data as small as possible this is not an option - I only want to store and load the data, not the javascript code for the methods.
I also dont want to have to put the data loaded by JSON as a sub object if I can help it (but might be the only way), e.g.
function Person(name) {
this.data = {};
this.data.name=name;
}
var newPerson = new Person("");
newPerson.data = eval( "(" + aPersonJSON + ")" );
alert (newPerson.data.name); // Bob
Any ideas?
You need to use a reviver function:
// Registry of types
var Types = {};
function MyClass(foo, bar) {
this._foo = foo;
this._bar = bar;
}
Types.MyClass = MyClass;
MyClass.prototype.getFoo = function() {
return this._foo;
}
// Method which will provide a JSON.stringifiable object
MyClass.prototype.toJSON = function() {
return {
__type: 'MyClass',
foo: this._foo,
bar: this._bar
};
};
// Method that can deserialize JSON into an instance
MyClass.revive = function(data) {
// TODO: do basic validation
return new MyClass(data.foo, data.bar);
};
var instance = new MyClass('blah', 'blah');
// JSON obtained by stringifying an instance
var json = JSON.stringify(instance); // "{"__type":"MyClass","foo":"blah","bar":"blah"}";
var obj = JSON.parse(json, function(key, value) {
return key === '' && value.hasOwnProperty('__type')
? Types[value.__type].revive(value)
: this[key];
});
obj.getFoo(); // blah
No other way really...
Many frameworks provide an 'extend' function that will copy fields over from one object to another. You can combine this with JSON.parse to do what you want.
newPerson = new Person();
_.extend(newPerson, JSON.parse(aPersonJSON));
If you don't want to include something like underscore you can always copy over just the extend function or write your own.
Coffeescript example because I was bored:
JSONExtend = (obj, json) ->
obj[field] = value for own field, value of JSON.parse json
return obj
class Person
toString: -> "Hi I'm #{#name} and I'm #{#age} years old."
dude = JSONExtend new Person, '{"name":"bob", "age":27}'
console.log dude.toString()
A little late to the party, but this might help someone.
This is how I've solved it, ES6 syntax:
class Page
{
constructor() {
this.__className = "Page";
}
__initialize() {
// Do whatever initialization you need here.
// We'll use this as a makeshift constructor.
// This method is NOT required, though
}
}
class PageSection
{
constructor() {
this.__className = "PageSection";
}
}
class ObjectRebuilder
{
// We need this so we can instantiate objects from class name strings
static classList() {
return {
Page: Page,
PageSection: PageSection
}
}
// Checks if passed variable is object.
// Returns true for arrays as well, as intended
static isObject(varOrObj) {
return varOrObj !== null && typeof varOrObj === 'object';
}
static restoreObject(obj) {
let newObj = obj;
// At this point we have regular javascript object
// which we got from JSON.parse. First, check if it
// has "__className" property which we defined in the
// constructor of each class
if (obj.hasOwnProperty("__className")) {
let list = ObjectRebuilder.classList();
// Instantiate object of the correct class
newObj = new (list[obj["__className"]]);
// Copy all of current object's properties
// to the newly instantiated object
newObj = Object.assign(newObj, obj);
// Run the makeshift constructor, if the
// new object has one
if (newObj.__initialize === 'function') {
newObj.__initialize();
}
}
// Iterate over all of the properties of the new
// object, and if some of them are objects (or arrays!)
// constructed by JSON.parse, run them through ObjectRebuilder
for (let prop of Object.keys(newObj)) {
if (ObjectRebuilder.isObject(newObj[prop])) {
newObj[prop] = ObjectRebuilder.restoreObject(newObj[prop]);
}
}
return newObj;
}
}
let page = new Page();
let section1 = new PageSection();
let section2 = new PageSection();
page.pageSections = [section1, section2];
let jsonString = JSON.stringify(page);
let restoredPageWithPageSections = ObjectRebuilder.restoreObject(JSON.parse(jsonString));
console.log(restoredPageWithPageSections);
Your page should be restored as an object of class Page, with array containing 2 objects of class PageSection. Recursion works all the way to the last object regardless of depth.
#Sean Kinsey's answer helped me get to my solution.
Easiest way is to use JSON.parse to parse your string then pass the object to the function. JSON.parse is part of the json2 library online.
I;m not too much into this, but aPerson.addAddress should not work,
why not assigning into object directly ?
aPerson.address.push(someAddress);
alert(aPerson.address); // alert [object object]
Just in case someone needs it, here is a pure javascript extend function
(this would obviously belong into an object definition).
this.extend = function (jsonString){
var obj = JSON.parse(jsonString)
for (var key in obj) {
this[key] = obj[key]
console.log("Set ", key ," to ", obj[key])
}
}
Please don't forget to remove the console.log :P
TL; DR: This is approach I use:
var myObj = JSON.parse(raw_obj_vals);
myObj = Object.assign(new MyClass(), myObj);
Detailed example:
const data_in = '{ "d1":{"val":3,"val2":34}, "d2":{"val":-1,"val2":42, "new_val":"wut?" } }';
class Src {
val1 = 1;
constructor(val) { this.val = val; this.val2 = 2; };
val_is_good() { return this.val <= this.val2; }
get pos_val() { return this.val > 0; };
clear(confirm) { if (!confirm) { return; }; this.val = 0; this.val1 = 0; this.val2 = 0; };
};
const src1 = new Src(2); // standard way of creating new objects
var srcs = JSON.parse(data_in);
// ===================================================================
// restoring class-specific stuff for each instance of given raw data
Object.keys(srcs).forEach((k) => { srcs[k] = Object.assign(new Src(), srcs[k]); });
// ===================================================================
console.log('src1:', src1);
console.log("src1.val_is_good:", src1.val_is_good());
console.log("src1.pos_val:", src1.pos_val);
console.log('srcs:', srcs)
console.log("srcs.d1:", srcs.d1);
console.log("srcs.d1.val_is_good:", srcs.d1.val_is_good());
console.log("srcs.d2.pos_val:", srcs.d2.pos_val);
srcs.d1.clear();
srcs.d2.clear(true);
srcs.d3 = src1;
const data_out = JSON.stringify(srcs, null, '\t'); // only pure data, nothing extra.
console.log("data_out:", data_out);
Simple & Efficient. Compliant (in 2021). No dependencies.
Works with incomplete input, leaving defaults instead of missing fields (particularly useful after upgrade).
Works with excessive input, keeping unused data (no data loss when saving).
Could be easily extended to much more complicated cases with multiple nested classes with class type extraction, etc.
doesn't matter how much data must be assigned or how deeply it is nested (as long as you restore from simple objects, see Object.assign() limitations)
The modern approach (in December 2021) is to use #badcafe/jsonizer : https://badcafe.github.io/jsonizer
Unlike other solutions, it doesn't pollute you data with injected class names,
and it reifies the expected data hierarchy.
below are some examples in Typescript, but it works as well in JS
Before showing an example with a class, let's start with a simple data structure :
const person = {
name: 'Bob',
birthDate: new Date('1998-10-21'),
hobbies: [
{ hobby: 'programming',
startDate: new Date('2021-01-01'),
},
{ hobby: 'cooking',
startDate: new Date('2020-12-31'),
},
]
}
const personJson = JSON.stringify(person);
// store or send the data
Now, let's use Jsonizer 😍
// in Jsonizer, a reviver is made of field mappers :
const personReviver = Jsonizer.reviver<typeof person>({
birthDate: Date,
hobbies: {
'*': {
startDate: Date
}
}
});
const personFromJson = JSON.parse(personJson, personReviver);
Every dates string in the JSON text have been mapped to Date objects in the parsed result.
Jsonizer can indifferently revive JSON data structures (arrays, objects) or class instances with recursively nested custom classes, third-party classes, built-in classes, or sub JSON structures (arrays, objects).
Now, let's use a class instead :
// in Jsonizer, a class reviver is made of field mappers + an instance builder :
#Reviver<Person>({ // 👈 bind the reviver to the class
'.': ({name, birthDate, hobbies}) => new Person(name, birthDate, hobbies), // 👈 instance builder
birthDate: Date,
hobbies: {
'*': {
startDate: Date
}
}
})
class Person {
constructor( // all fields are passed as arguments to the constructor
public name: string,
public birthDate: Date
public hobbies: Hobby[]
) {}
}
interface Hobby {
hobby: string,
startDate: Date
}
const person = new Person(
'Bob',
new Date('1998-10-21'),
[
{ hobby: 'programming',
startDate: new Date('2021-01-01'),
},
{ hobby: 'cooking',
startDate: new Date('2020-12-31'),
},
]
);
const personJson = JSON.stringify(person);
const personReviver = Reviver.get(Person); // 👈 extract the reviver from the class
const personFromJson = JSON.parse(personJson, personReviver);
Finally, let's use 2 classes :
#Reviver<Hobby>({
'.': ({hobby, startDate}) => new Hobby(hobby, startDate), // 👈 instance builder
startDate: Date
})
class Hobby {
constructor (
public hobby: string,
public startDate: Date
) {}
}
#Reviver<Person>({
'.': ({name, birthDate, hobbies}) => new Person(name, birthDate, hobbies), // 👈 instance builder
birthDate: Date,
hobbies: {
'*': Hobby // 👈 we can refer a class decorated with #Reviver
}
})
class Person {
constructor(
public name: string,
public birthDate: Date,
public hobbies: Hobby[]
) {}
}
const person = new Person(
'Bob',
new Date('1998-10-21'),
[
new Hobby('programming', new Date('2021-01-01')),
new Hobby('cooking', new Date('2020-12-31')
]
);
const personJson = JSON.stringify(person);
const personReviver = Reviver.get(Person); // 👈 extract the reviver from the class
const personFromJson = JSON.parse(personJson, personReviver);