Related
I want to create a function inside an object. I need this function to generate setters and getters
for the properties of the caller object without generating getters or setters for the property of the function value.
I reached for something like this. But It gives me RangeError Maximum call stack size exceeded.
function Emp() {
return {
name: "Mohamed",
id: "5",
getSetGen: function() {
for (var i in this) {
if (typeof this[i] !== 'function') {
(function(j) {
Object.defineProperty(this, j, {
get: function() {
return this[j];
},
set: function(val) {
this[j] = val
}
})
})(i);
}
}
}
}
}
I want to apply getSetGen() to var user = { name:βAliβ,age:10} for example.
Is there any possible solution? Thanks in advance.
Edit:
This is the text that describes what I need ...
Create your own custom object that has getSetGen as
function value, this function should generate setters and getters
for the properties of the caller object
This object may have description property of string value if
needed
Let any other created object can use this function property to
generate getters and setters for his own properties
Avoid generating getters or setters for property of function
value
In trying to solve this I reworked some of your code, but I think this does basically what you're looking for, and you can tweak it to your preferences.
(I think the issue was that the new accessor property had the same name as the existing data property. See comments in the code for further explanation.)
const
baseObj = getBaseObj(),
user = { name: "Ali", age: 10 };
baseObj.addAccessorsForAllDataProps.call(user);
console.log("\nsetting age to 11...");
user.age = 11;
console.log("\nretrieving user age...");
console.log(user.age);
function getBaseObj(){
return {
name: "Mohamed",
id: 5,
addAccessorsForAllDataProps(){
for(let originalPropName in this){
if(!isFunction(this[originalPropName])){
// Renames the data property with an underscore
const dataPropName = `_${originalPropName}`
// Binds existing value to renamed data property
this[dataPropName] = this[originalPropName];
// Passes originalPropName to be used as accessorPropName
addAccessors(this, originalPropName, dataPropName);
}
}
// Accessor prop can't have same name as data prop
// (I think your stack overflow happened b/c getter got called infinitely)
function addAccessors(obj, accessorPropName, dataPropName){
Object.defineProperty(obj, accessorPropName, {
get: function(){
console.log("(getter invoked)"); // Just proving accessors get called
return obj[dataPropName];
},
set: function(val){
console.log("(setter invoked)");
obj[dataPropName] = val;
}
});
};
}
}
}
function isFunction(val) {
return val && {}.toString.call(val) === '[object Function]';
}
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'
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);
I would like to add key-value pairs of metadata to arbitrary JavaScript objects. This metadata should not affect code that is not aware of the metadata, that means for example
JSON.stringify(obj) === JSON.stringify(obj.WithMetaData('key', 'value'))
MetaData aware code should be able to retrieve the data by key, i.e.
obj.WithMetaData('key', 'value').GetMetaData('key') === 'value'
Is there any way to do it - in node.js? If so, does it work with builtin types such as String and even Number? (Edit Thinking about it, I don't care about real primitives like numbers, but having that for string instances would be nice).
Some Background: What I'm trying to do is cache values that are derived from an object with the object itself, so that
to meta data unaware code, the meta data enriched object will look the same as the original object w/o meta
code that needs the derived values can get it out of the meta-data if already cached
the cache will get garbage collected alongside the object
Another way would be to store a hash table with the caches somewhere, but you'd never know when the object gets garbage collected. Every object instance would have to be taken care of manually, so that the caches don't leak.
(btw clojure has this feature: http://clojure.org/metadata)
You can use ECMA5's new object properties API to store properties on objects that will not show up in enumeration but are nonetheless retrievable.
var myObj = {};
myObj.real_property = 'hello';
Object.defineProperty(myObj, 'meta_property', {value: 'some meta value'});
for (var i in myObj)
alert(i+' = '+myObj[i]); //only one property - #real_property
alert(myObj.meta_property); //"some meta value"
More information here: link
However you're not going to be able to do this on primitive types such as strings or numbers, only on complex types.
[EDIT]
Another approach might be to utilise a data type's prototype to store meta. (Warning, hack ahead). So for strings:
String.prototype.meta = {};
String.prototype.addMeta = function(name, val) { this.meta[name] = val; }
String.prototype.getMeta = function(name) { return this.meta[name]; };
var str = 'some string value';
str.addMeta('meta', 'val');
alert(str.getMeta('meta'));
However this is clearly not ideal. For one thing, if the string was collected or aliased (since simple data types are copied by value, not reference) you would lose this meta. Only the first approach has any mileage in a real-world environment, to be honest.
ES6 spec introduces Map and WeakMap. You can enable these in node by running node --harmony and by enabling the experimental javascript flag in Chrome, (it's also in Firefox by default). Maps and WeakMaps allow objects to be used as keys which can be be used to store metadata about objects that isn't visible to anyone without access to the specific map/weakmap. This is a pattern I now use a lot:
function createStorage(creator){
creator = creator || Object.create.bind(null, null, {});
var map = new Map;
return function storage(o, v){
if (1 in arguments) {
map.set(o, v);
} else {
v = map.get(o);
if (v == null) {
v = creator(o);
map.set(o, v);
}
}
return v;
};
}
Use is simple and powerful:
var _ = createStorage();
_(someObject).meta= 'secret';
_(5).meta = [5];
var five = new Number(5);
_(five).meta = 'five';
console.log(_(someObject).name);
console.log(_(5).meta);
console.log(_(five).meta);
It also facilitates some interesting uses for separating implementation from interface:
var _ = createStorage(function(o){ return new Backing(o) });
function Backing(o){
this.facade = o;
}
Backing.prototype.doesStuff = function(){
return 'real value';
}
function Facade(){
_(this);
}
Facade.prototype.doSomething = function doSomething(){
return _(this).doesStuff();
}
There is no "comment" system in JSON. The best you can hope for is to add a property with an unlikely name, and add that key contaning the metadata. You can then read the metadata back out if you know it's metadata, but other setups will just see it as another property. And if someone uses for..in...
You could just add the Metadata as a "private" variable!?
var Obj = function (meta) {
var meta = meta;
this.getMetaData = function (key) {
//do something with the meta object
return meta;
};
};
var ins_ob = new Obj({meta:'meta'});
var ins_ob2 = new Obj();
if(JSON.stringify(ins_ob) === JSON.stringify(ins_ob2))Β {
console.log('hoorai');
};
If you want object-level metadata, you could create a class that extends Object. Getters and setters are not enumerable and, obviously, neither are private fields.
class MetadataObject extends Object {
#metadata = undefined;
get metadata() { return this.#metadata; }
set metadata(value) { this.#metadata; }
}
var obj = new MetadataObject();
obj.a = 1;
obj.b = 2;
obj.metadata = { test: 123 };
console.log(obj); // { a: 1, b: 2 }
console.log(obj.metadata); // { test: 123 }
console.log(JSON.stringify(obj)); // '{"a":1,"b":2}'
You can even simplify the implementation using a Map. Without a setter on metadata, you have to use Map methods to modify it.
class MetadataObject extends Object {
#metadata = new Map();
get metadata() { return this.#metadata; }
}
var obj = new MetadataObject();
obj.a = 1;
obj.b = 2;
obj.metadata.set('test', 123);
console.log(obj); // { a: 1, b: 2 }
console.log(obj.metadata.get('test')); // 123
console.log(JSON.stringify(obj)); // '{"a":1,"b":2}'
I ran into a situation where I needed property level metadata, and used the latter implementation.
obj.id = 1;
obj.metadata.set('id', 'metadata for the id property');
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