How to write a global get+set method for object - javascript

My question is easy to understand, I have an object (or class), and I want to have ONE method which can getting AND setting a property.
In fact, I have no problem to write it for "simple" properties. It becomes difficult when my class has object properties, and that I want to access or alter a nested one.
My class:
var MyClass = function() {
this.name = 'defaultName';
this.list = {
a: 1,
b: 6
};
}
Simple class, isn't it? Then, what I write for my method:
MyClass.prototype.getset = function(prop) {
let value = arguments[1];
let path = prop.split('.');
prop = this;
$(path).each(function(i) { prop = prop[this]; }
if (value) {
prop = value;
return this;
}
return prop;
}
The "get part" works (MyClass.getset('list.b') returns 6).
But the "set part"... does not work.
I want that when I execute MyClass.getset('list.b', 2), the b property of list becomes 2, and that's not the case.
I know why my version is not working (my prop variable is just a "copy" and does not affect the object itself), but I can't find solution for this...
Thanks for you help!

If you're assigning a primitive, you need to assign to a property of an object for the object to be changed as well. Check if value, and if so, navigate to and change from the next to last property, rather than the final property. Use reduce for brevity:
var MyClass = function() {
this.name = 'defaultName';
this.list = {
a: 1,
b: 6
};
}
MyClass.prototype.getset = function(prop, value) {
const props = prop.split('.');
const lastProp = props.pop();
const lastObj = props.reduce((obj, prop) => obj[prop], this);
if (value) {
lastObj[lastProp] = value;
return this;
} else return lastObj[lastProp];
}
const mc = new MyClass();
mc.getset('list.b', 2);
console.log(mc.list.b);
console.log(mc.getset('list.b'));

Related

JavaScript use original getter/setter in defineProperty

I would like to create a TypeScript decorator that can extend the logic of a property's getter/setter. I have tried to copy the original property under a symbol and call that when I redefine the property. The problem is it turns into an infinite loop.
//Find the latest version of 'attribute' getter setter in the prototype chain
let obj = _object;
while(obj && !(Object.getOwnPropertyDescriptor(obj, 'attribute'))){
obj = Object.getPrototypeOf(obj);
}
//Copy original 'attribute' logic under a symbol
const attributeDesc = Object.getOwnPropertyDescriptor(obj, 'attribute');
let id=Symbol('__attribute');
Object.defineProperty(obj, id, attributeDesc);
//Redefine 'attribute' logic
Object.defineProperty(_object, 'attribute', {
get: () => {
//call original
const attribute = obj[id]; //It crashes the page (probably infinite loop)
//extend original logic
attribute['extend'] = 'property';
return attribute;
},
enumerable: false,
configurable: true
});
If you could explain me why it ends up this way that would help me out. I thought the new getter function reference nothing to do with the original. Please suggest me a solution to achive this in JavaScript.
Thank you for your time and answers!
I don't quite see the error. In the repro you provided, it's logical that there is one: the getter for attribute property is calling itself on the line var attributes = obj[id], so there is an infinite loop. However if you edit your code to be like the snippet you provided in the question:
class A {
get attribute() {
return { a: 1 }
}
}
var _object = new A()
let obj = _object
while (obj && !Object.getOwnPropertyDescriptor(obj, 'attribute')) {
obj = Object.getPrototypeOf(obj)
}
const attributeDesc = Object.getOwnPropertyDescriptor(obj, 'attribute')
let id = Symbol('__attribute')
Object.defineProperty(obj, id, attributeDesc)
Object.defineProperty(obj, 'attribute', {
get: function () {
var attributes = obj[id]
attributes['extend'] = 'property'
return attributes
},
enumerable: false,
configurable: true,
})
console.log('result:', obj.attribute)
There is no error and it works as expected.
You don't really need the symbol though, you could do something like
function extendAttributes(_object) {
let obj = _object
while (obj && !Object.hasOwnProperty(obj, 'attributes')) {
obj = Object.getPrototypeOf(obj)
}
if(!obj) return;
const oldContainer = {}
const attributesDescriptor = Object.getOwnPropertyDescriptor(obj, 'attributes')
Object.defineProperty(oldContainer, 'attributes', attributesDescriptor)
Object.defineProperty(obj, 'attributes', {
get() {
const attribute = oldContainer.attributes;
//extend original logic
attribute['extend'] = 'property';
return attribute;
}
})
}
class A {
get attributes() { return {a: 1} }
}
const obj = new A()
extendAttributes(obj)
console.log(obj.attributes)
Which also works like expected

Is there a way to force a javascript element to redefine itself based on how it was originally defined?

I was playing around with objects and constructors and stuff like that, and I was wondering if there was a way to bind a value to a variable based on how it was originally defined. I have the following code:
typescript
let cr = "create",
ap = "apply",
$this = {
set: (prop, value) => {
this[prop] = value;
}
};
function creator() {
this.$ = (array: Object[]) => {
array.forEach((kp: Object) => {
let key = Object.keys(kp)[0];
let val = kp[Object.keys(kp)];
$this[key] = val;
creator.create(key, { value: val });
});
};
this.apply = (...objects: Object[]) => {
objects.forEach((obj: Object) => {
creator.call(obj);
});
};
}
function create(obj) {
function createValues(arr) {
let instance: Object = new obj();
let vals: any[] = [];
arr.forEach(name => {
vals.push(instance[name]);
});
return vals;
}
let names: string[] = Object.getOwnPropertyNames(new obj());
let values: string[] = createValues(names);
return combineArrays(names, values);
}
function combineArrays(arr1, arr2): { $?: any } { // the question mark removes an IDE error
let newObj: Object = {};
arr1.forEach(prop => {
newObj[prop] = arr2[arr1.indexOf(prop)];
});
return newObj;
}
Object.prototype.create = function(prop, options) {
return Object.defineProperty(this, prop, options);
};
create(creator).$([
{ hi: "hi" },
{ bye: $this["hi"] } // this is the important stuff
]);
I was wondering if there is a way, inside the set function of the $this variable, to detect how it is being set and therefore determine if that value has changed and so it's value should to, if that makes any sense? Let's say you had this:
let $this = {
set: function(prop, value) {
this[prop] = value;
}
}
let name = 'Ezra';
$this['name'] = name;
// then :
name = 'Bob';
// change $this.name too, so then:
console.log($this.name);
// >> 'Bob'
I believe this is called Data-Binding but I am unsure how to do it without creating endless numbers of proxies.
What you're describing is not really "data-binding" but pass-by-reference. In your example you expect an update to name to be reflected in $this['name']. That would only be possible if you were passing a reference (or a pointer) to the variable.
However, in this case the variable is a string, and strings in JavaScript are immutable:
no string methods change the string they operate on, they all return new strings. The reason is that strings are immutable – they cannot change, we can only ever make new strings.
So, going step-by-step through your example:
This creates a new string named 'Ezra', and assigns a variable called name to reference that string.
let name = 'Ezra';
This creates (or sets) a property in $this called 'name' that references the string in name (which is 'Ezra').
$this['name'] = name;
This creates a new string called 'Bob' and stores it in the variable called name. The variable already exists. This does not mutate the value that was previously held in there. Instead, name is being updated to point to a new reference.
// then :
name = 'Bob';
However, if you were to pass an object, you'll notice that you can actually mutate it. This is because objects are passed-by-reference and you can mutate the value at that reference.
For example:
// Create a new value that stores an object, with a property 'firstName'
let name = { firstName: 'Ezra' };
// Assign myObject to $this['name']. Now $this['name'] and name both point to the same reference.
$this['name'] = name;
// Change the value in the object pointed-to by name
name.firstName = 'Bob'
console.log($this['name'].firstName); // <- This will output 'Bob'

Javascript create reference to an object property?

I understand that in javascript, primitives are passed by value and objects are passed by reference.
I'm interested in creating a workaround of some kind that would let me get a reference to an object property containing a primitive. For example, what I wish would work is:
var someObject = {a: 1, b: 2};
var myRef = someObject.b;
myRef ++;
someObject.b #=> 3
Of course, this doesn't work. I'm aware that you could create a getter and setter function instead, or use one object to reference another object, but what I'd really like is some kind of workaround that allowed me to define a variable as a reference to the property of another object, and so far it seems this just can't be done.
So, my question is simply: is this even possible, and if so, how?
Primitive types are immutable, so no, it's not possible. You can wrap your primitive type with an object, like this:
function MyNumber(n) { this.n = n; }
MyNumber.prototype.valueOf = function() { return this.n; }
var someObject = { a: 1, b: new MyNumber(2) };
var myRef = someObject.b;
MyNumber.call(myRef, myRef + 1);
console.log(+someObject.b); // convert to number with +
OR
var someObject = {
a: { value: 1 },
b: { value: 2 },
};
var myRef = someObject.b;
my_inc(myRef); // function my_inc (obj) { obj.value++; }
// someObject.b.value == 3
The React framework uses a very simple pattern to encapsulate values.
function Link(value, requestChange)
{
this.value = value;
this.requestChange = requestChange;
}
You can pass around the object, the current value can be accessed by inspecting the value property of the object, if you want to change it you can call requestChange with a new value, you can change the value. The advantage would be to have the actual "storage location" and the logic for changing the value decoupled from the value read and write access. Note that the values can also be complex objects.
You could also achieve something similar with closures:
var someObject = {
a: 1,
b: 2
};
function property(object, prop) {
return {
get value () {
return object[prop]
},
set value (val) {
object[prop] = val;
}
};
}
var ref = property(someObject, "b");
ref.value; // 2
++ref.value; // 3
someObject.b; // 3
This works because the getter and setter functions have access to whatever bindings were in scope at the time of their creation (object and prop). You can now pass ref around, store it in a data structure, etc.
No, there isn't a nice way to do it.
You can use a work-around if you want to. Something like wrapping all your primary data types with single element arrays:
var someObject = {a: [1], b: [2]};
var myRef = someObject.b;
myRef[0]++;
someObject.b[0]; // 3
That's less than ideal though, as you have to use [0] to access the property all the time. There are some cases where it can be useful though, and the default toString of a single element array is just the toString of its element, so you can use the property directly in a string context:
console.log('My value: ' + someObject.b); // 'My value: 3'
if you want to "link" or "synchronize" two properties , each of a different object, you could do it like this:
var someObject = {
a: 1,
b: 2
};
var linkedObject = {
a:1,
b:2
}
function property(object, prop) {
return {
get value () {
return object[prop]
},
set value (val) {
object[prop] = val;
}
};
}
var s_prop = 'b'
var o_ref = property(someObject, s_prop);
var tmp = linkedObject[s_prop];
Object.defineProperty(
linkedObject,
s_prop,
{
set: function(value) {
o_ref.value = value;
},
get: function() {
return o_ref.value
}
}
);
linkedObject[s_prop] = tmp
someObject.b = 333 /// linkedObject.b is also 333 now
console.log(someObject.b) // 333
console.log(linkedObject.b)// 333
linkedObject.b = {"test": 2}
console.log(someObject.b) // {test:2}
console.log(linkedObject.b)// {test:2}
someObject.b.test = 3
console.log(someObject.b) // {test:3}
console.log(linkedObject.b)//{test:3}
I don't know how satisfying this is, but you could do it if you were ok with wrapping the desired object in an object like so:
var a = {a:{a:1},b:2};
var b = a.a;
b.a++;
a.a.a //=> 2
It isn't exactly what you asked for, but it would work.

Class inheritance and private variables in JS

Say I have this code:
function ParentClass()
{
var anArray = [ ];
this.addToArray = function(what)
{
anArray.push(what);
console.log(anArray);
};
}
FirstSubClass.prototype = new ParentClass();
FirstSubClass.prototype.constructor = FirstSubClass;
function FirstSubClass()
{
this.addToArray('FirstSubClass');
}
SecondSubClass.prototype = new ParentClass();
SecondSubClass.prototype.constructor = SecondSubClass;
function SecondSubClass()
{
this.addToArray('SecondSubClass');
}
When I run new FirstSubClass() I see a single value array in the console. And when I run new SecondSubClass(), again, I see a single value array.
However, why is it when I run them again (i.e. new FirstSubClass(); new SecondSubClass();) I then see the arrays added to rather than new ones being created?
The rationale here is that I'm creating new instances of a class, therefore why are they sharing the same private property?
How can I avoid this so when I do, for e.g., new FirstSubClass() I then see a single value array no matter how many times I create a new instance of the class?
Keep in mind that you've only called new ParentClass() once for each subclass. That means that the private array variable is part of the prototype object for those subclasses. There's only one prototype object, so there's only one array (per subclass).
Each call to new FirstSubClass() generates a new instance that shares the same prototype object. The call to addToArray() therefore adds an element to that same array that was created when the prototype object was created.
edit — if you want per-instance arrays, you'd have to do something like this:
function ParentClass() {
this.addToArray = function(value) { this.instanceArray.push(value); };
};
function FirstSubClass() {
this.instanceArray = [];
this.addToArray("First");
}
FirstSubClass.prototype = new ParentClass();
FirstSubClass.prototype.constructor = FirstSubClass;
First, sub-classing in JS is typically a bad idea, because people think that they're getting extension, where every instance has its own copy of properties and methods...
...really, they're getting public static access to the parent's stuff.
Even better, that public static stuff has no access to the encapsulated variables, so there's really no manipulation of private data, unless you're using private functions (with a public interface) to pass data to and collect return values from, the public static stuff.
var Parent = function () {
this.static_prop = 0;
this.static_method = function (num) { this.static_prop += 1; return num + this.static_prop; };
};
var Child = function (num) {
this.public_func = function () { num = this.static_method(num); };
};
Child.prototype = new Parent();
var child = new Child(13);
child.public_func();
Just calling this.static_method wouldn't help, because it would have 0 access to num, which means that you're wrapping things which you inherited to grant them access to use private data as inputs, which means that you're doing most of the writing you'd be doing anyway, regardless of inheritance, because your expectations of .prototype were backwards.
Might I suggest Dependency Injection, instead?
Component-based programs?
var Iterator = function () {
var count = 0,
min = 0,
max = 0,
reset = function () { count = min; },
next = function () { count = count >= max ? min : count; return count += 1; },
set_min = function (val) { min = val; },
set_max = function (val) { max = val; },
public_interface = { reset : reset, count : count, set_min : set_min, set_max : set_max };
return public_interface;
},
Thing = function (iter) {
var arr = [],
currentObj = null,
nextObj = function () {
currentObj = arr[iter.next()];
},
add = function (obj) {
arr.push(obj); iter.set_max(arr.length);
},
public_interface = { next : nextObj, add : add };
return public_interface;
};
var thing = Thing(Iterator());
thing.add({});
thing.next();
It's a convoluted example, but now every instance is going to be given exactly what it needs to do its job (because the constructor requires it -- or you can add the dependency later, through a public method, or as a public-property).
The interfaces for each module can now also get as simple and as clean as you'd like, as you don't have to wrap unexpected static-helpers to get private data...
Now you know what's private, you know what you're extending to the public, and you have clean ins and outs wherever you want to put them.
You are only constructing a new instance of ParentClass once per subclass and that is to apply it to your prototype. If you want each instance to have its own copy of the private array and its own copy of the function "addToArray" you will need to invoke the ParentClass constructor function within your other objects constructors:
function ParentClass(){
var anArray = [ ];
this.addToArray = function(what){
anArray.push(what);
console.log(anArray);
};
}
FirstSubClass.prototype = new ParentClass();
FirstSubClass.prototype.constructor = FirstSubClass;
function FirstSubClass(){
//call the parents constructor where "this" points to your FirstSubClass instance
ParentClass.call( this );
this.addToArray('FirstSubClass');
}
SecondSubClass.prototype = new ParentClass();
SecondSubClass.prototype.constructor = SecondSubClass;
function SecondSubClass(){
ParentClass.call( this );
this.addToArray('SecondSubClass');
}
try this:
http://jsfiddle.net/3z5AX/2/
function ParentClass()
{
var anArray = [ ];
this.addToArray = function(what)
{
anArray.push(what);
document.getElementById("i").value = anArray;
};
}
//FirstSubClass.prototype = new ParentClass();
FirstSubClass.prototype.constructor = FirstSubClass;
function FirstSubClass()
{
this.parent = new ParentClass()
this.parent.addToArray('FirstSubClass');
}
var q = new FirstSubClass();
var r = new FirstSubClass();
All Subclasses share the same parent class, thus the same private anArray
The solution is to use the Mixin pattern.
// I have the habbit of starting a mixin with $
var $AddToArray = function(obj) {
var array = [];
obj.addToArray = function(what) {
array.push(what);
console.log(array);
};
}
var FirstClass = function() {
$AddToArray(this);
}
var SecondClass = function() {
$AddToArray(this);
}

Parse JSON String into a Particular Object Prototype in JavaScript

I know how to parse a JSON String and turn it into a JavaScript Object.
You can use JSON.parse() in modern browsers (and IE9+).
That's great, but how can I take that JavaScript Object and turn it into a particular JavaScript Object (i.e. with a certain prototype)?
For example, suppose you have:
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
Again, I am not wondering how to convert a JSON string into a generic JavaScript Object. I want to know how to convert a JSON string into a "Foo" Object. That is, my Object should now have a function 'test' and properties 'a' and 'b'.
UPDATE
After doing some research, I thought of this...
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);
Will that work?
UPDATE May, 2017: The "modern" way of doing this, is via Object.assign, but this function is not available in IE 11 or older Android browsers.
The current answers contain a lot of hand-rolled or library code. This is not necessary.
Use JSON.parse('{"a":1}') to create a plain object.
Use one of the standardized functions to set the prototype:
Object.assign(new Foo, { a: 1 })
Object.setPrototypeOf({ a: 1 }, Foo.prototype)
See an example below (this example uses the native JSON object). My changes are commented in 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
Do you want to add JSON serialization/deserialization functionality, right? Then look at this:
You want to achieve this:
toJson() is a normal method.
fromJson() is a static method.
Implementation:
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);
};
Usage:
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
Note: If you want you can change all property definitions like this.title, this.author, etc by var title, var author, etc. and add getters to them to accomplish the UML definition.
A blog post that I found useful:
Understanding JavaScript Prototypes
You can mess with the __proto__ property of the Object.
var fooJSON = jQuery.parseJSON({"a":4, "b": 3});
fooJSON.__proto__ = Foo.prototype;
This allows fooJSON to inherit the Foo prototype.
I don't think this works in IE, though... at least from what I've read.
Am I missing something in the question or why else nobody mentioned reviver parameter of JSON.parse since 2011?
Here is simplistic code for solution that works:
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: Your question is confusing: >>That's great, but how can I take that JavaScript Object and turn it into a particular JavaScript Object (i.e. with a certain prototype)?
contradicts to the title, where you ask about JSON parsing, but the quoted paragraph asks about JS runtime object prototype replacement.
The currently accepted answer wasn't working for me. You need to use Object.assign() properly:
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`;
}
}
You create objects of this class normally:
let matt = new Person('matt', 12);
console.log(matt.greet()); // prints "hello my name is matt and i am 12 years old"
If you have a json string you need to parse into the Person class, do it like so:
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
An alternate approach could be using Object.create. As first argument, you pass the prototype, and for the second one you pass a map of property names to descriptors:
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);
I like adding an optional argument to the constructor and calling Object.assign(this, obj), then handling any properties that are objects or arrays of objects themselves:
constructor(obj) {
if (obj != null) {
Object.assign(this, obj);
if (this.ingredients != null) {
this.ingredients = this.ingredients.map(x => new Ingredient(x));
}
}
}
For the sake of completeness, here's a simple one-liner I ended up with (I had no need checking for non-Foo-properties):
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 }'));
I created a package called json-dry. It supports (circular) references and also class instances.
You have to define 2 new methods in your class (toDry on the prototype and unDry as a static method), register the class (Dry.registerClass), and off you go.
While, this is not technically what you want, if you know before hand the type of object you want to handle you can use the call/apply methods of the prototype of your known object.
you can change this
alert(fooJSON.test() ); //Prints 12
to this
alert(Foo.prototype.test.call(fooJSON); //Prints 12
I've combined the solutions that I was able to find and compiled it into a generic one that can automatically parse a custom object and all it's fields recursively so you can use prototype methods after deserialization.
One assumption is that you defined a special filed that indicates it's type in every object you want to apply it's type automatically (this.__type in the example).
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
//...
}
Type assignment function (it work recursively to assign types to any nested objects; it also iterates through arrays to find any suitable objects):
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);
}
A very simple way to get the desired effect is to add an type attribute while generating the json string, and use this string while parsing the string to generate the object:
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;
});
}
Here a little example of use:
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"
Here is a solution using typescript and decorators.
Objects keep their methods after deserialization
Empty objects and their children are default-initialized
How to use it:
#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!
How it works
Serialization:
Store the type name in the prototype (__typeName)
Use JSON.stringify with a replacer method that adds __typeName to the JSON.
Deserialization:
Store all serializable types in Serializable.__serializableObjects
Store a list of complex typed properties in every object (__serializedProps)
Initialize an object theObject via the type name and __serializableObjects.
Go through theObject.__serializedProps and traverse over it recursively (start at last step with every serialized property). Assign the results to the according property.
Use Object.assign to assign all remaining primitive properties.
The code:
// #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;
}
}
Comments
Since I am a total js/ts newby (< 10 days), I am more than happy to receive any input/comments/suggestions. Here are some of my thoughts so far:
It could be cleaner: Unfortunately I did not find a way to get rid of the redundant parameter of #SerializableProp.
It could be more memory friendly: After you call serializeObject() every object stores __typeName which could massively blow up memory footprint. Fortunately __serializedProps is only stored once per class.
It could be more CPU friendly: It's the most inefficient code I've ever written. But well, it's just for web apps, so who cares ;-) Maybe one should at least get rid of the recursion.
Almost no error handling: well that's a task for another day
class A {
constructor (a) {
this.a = a
}
method1 () {
console.log('hi')
}
}
var b = new A(1)
b.method1() // hi
var c = JSON.stringify(b)
var d = JSON.parse(c)
console.log(d.a) // 1
try {
d.method1() // not a function
} catch {
console.log('not a function')
}
var e = Object.setPrototypeOf(d, A.prototype)
e.method1() // hi
Olivers answers is very clear, but if you are looking for a solution in angular js, I have written a nice module called Angular-jsClass which does this ease, having objects defined in litaral notation is always bad when you are aiming to a big project but saying that developers face problem which exactly BMiner said, how to serialize a json to prototype or constructor notation objects
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

Categories

Resources