Iām transforming data I receive from an API. The frontend requires some calculations to be displayed.
What is the proper way to handle the data transformation?
Should I be defining a property to the object being passed? If so, why
Is this a good use case to use setters and getters or would that be unnecessary?
const dogData = {
dog_name: "filo",
born_time: 1530983852,
coat_color: "brown"
};
class Dog {
constructor(data) {
//do I need to set this.dog to the data object, what's the benefit of doing so?
this.dog = data;
this.name = this.dog.dog_name;
// vs
this.name = data.dog_name;
//Should I use setters and getters?
this.color = this.dog.coat_color;
// vs
this._color = this.dog.coat_color;
this.age = this.calculateAge();
}
calculateAge() {
return Date.now().getTime() - this.dog.born_time;
}
//Is this a good case where I should using getters to access the properties or would that be superfluous?
//should I be setting the properties with setters in this case?
get color() {
return this._color;
}
}
const dog = new Dog(dogData)
Your don't need to make a copy of data into your class.
You can assign the class fields directly (using object destructuring to be more readable).
const data = {
dog_name: 'filo',
born_time: 1530983852,
coat_color: 'brown'
}
class Dog {
// Directly assign values
constructor({ dog_name, born_time, coat_color }) {
this.name = dog_name
this.bornAt = born_time
this.color = coat_color
}
// Getter for computed properties
get age() {
return Date.now() - this.bornAt
}
}
const dog = new Dog(data)
Getters are needed only for computed property (dynamic or formatted values).
Good exemple:
class Person {
constructor({ firstname, lastname }) {
this.firstname = firstname
this.lastname = lastname
}
get fullname() {
return `${this.firstname} ${this.lastname}`
}
}
class Dog {
constructor(data) {
const {
dog_name: name,
born_time: age,
coat_color: color
} = data;
Object.assign(this, {
name,
age,
color
});
}
}
const dogData = {
dog_name: "filo",
born_time: 1530983852,
coat_color: "brown"
};
const dog = new Dog(dogData);
console.log(dog.name);
Q:
Shall I nevertheless throw in a possible read only approach? ā Peter Seliger
A:
It wouldn't hurt. I appreciate the different approaches. ā Matthew Moran
... here we go ...
// module start ... e.g. file: "Dog.js"
// locally scoped helper function
function calculateAge(dateOfBirth) {
return (Date.now() - dateOfBirth);
}
/*export default */class Dog {
constructor(initialValue) {
Object.defineProperties(this, {
valueOf: { // just in order to hint what `initialValue` might still be good for.
value: function () {
return Object.assign({}, initialValue);
}
},
name: {
value: initialValue.dog_name,
enumerable: true
},
color: {
value: initialValue.coat_color,
enumerable: true
},
age: {
get() {
return calculateAge(initialValue.born_time);
},
enumerable: true,
}
});
}
}
// module end.
// test
const dogData = {
dog_name: "filo",
born_time: 1530983852,
coat_color: "brown"
};
const dog = new Dog(dogData);
console.log('Object.keys(dog) : ', Object.keys(dog));
console.log('dog.valueOf() : ', dog.valueOf());
console.log('dog.age : ', dog.age);
console.log('dog.name : ', dog.name);
console.log('dog.color : ', dog.color);
console.log('(dog.age = 298146912) : ', (dog.age = 298146912) && dog.age);
console.log('(dog.name = "spot") : ', (dog.name = "spot") && dog.name);
console.log('(dog.color = "black") : ', (dog.color = "black") && dog.color);
.as-console-wrapper { max-height: 100%!important; top: 0; }
Related
The JavaScript code below has a getter and setter for name using global symbols, and age_ using a dangling underscore.
JavaScript Code
function Human(name, age) {
this.name = name;
this.age_ = age;
}
Human.prototype.greet = function() {
console.log(`Hello ${this.name}! You are ${this.age} years old.`);
};
Human.prototype[Symbol.iterator] = function* () {
yield* Object.getOwnPropertyNames(this);
yield* Object.getOwnPropertySymbols(this);
};
Object.defineProperties(
Human.prototype,
{
name: {
get() {
return this[Symbol.for('name')];
},
set(value) {
this[Symbol.for('name')] = value;
},
},
age: {
get() {
return this.age_;
},
set(value) {
this.age_ = value;
},
},
},
);
let mary = new Human('Mary Smith', 18);
mary.greet();
console.dir(mary);
let john = new Human('John Doe', 25);
john.greet();
console.dir(john);
Console Output
I want to know is there any performance drawbacks to using global symbols for a getter and setter methods because I find it much cleaner code.
This is the form of constructor which Douglas Crockford suggests in his book "How Javascript works" and in his lectures.
const constructor_x = function (spec) {
let { a } = spec // private state
// methods can modify private state
const method_x = function () { a = '...' }
// methods are exposed as public interface
return Object.freeze({ method_x })
}
He suggests the following pattern for composition:
const constructor_y = function (spec) {
let { b } = spec // private state
// we can call other constructor and borrow functionality
const { method_x } = constructor_x(spec)
// we define new methods
const method_y = function () { b = '...' }
// we can merge borrowed and new functionality
// and expose everything as public interface
return Object.freeze({ method_x, method_y })
}
So here we see how to compose constructor_x and constructor_y. But my problem with this example (and all examples used when this pattern is presented) is that constructor_x and constructor_y make separate private states. constructor_x works on variable a, while constructor_y works on variable b. What if we want our constructors to share state? What if constructor_y also wants to work with variable a?
const constructor_y = function (spec) {
let { a, b } = spec
const { method_x } = constructor_x(spec)
const method_y = function () { b = '...' }
const method_z = function () {
// we may want to read `a` and maybe write to it
a = '...'
}
return Object.freeze({ method_x, method_y, method_z })
}
Of course this doesn't achieve what I want because a which constructor_y sees is not the same a constructor_x sees. If I used this, I could have achieved that maybe like so:
const constructor_x = function (spec) {
return {
_a: spec.a,
method_x () { this._a = '...' }
}
}
const constructor_y = function (spec) {
return {
...constructor_x(spec),
_b: spec.b
method_y () { this._b = '...' },
method_z () { this._a = '...' }
}
}
But here I have lost privacy of variables _a and _b since they are attached to instance and are accessible just like methods. The best I can do is add underscore prefix which Douglas Crockford calls a sign of incompetence. I also lost instance's rigidness because it can no longer be frozen.
I could have exposed accessors for variable a in constructor_x like so:
const constructor_x = function (spec) {
let { a } = spec // private state
// methods can modify private state
const method_x = function () { a = '...' }
// methods are exposed as public interface
return Object.freeze({
method_x,
get_a () { return a },
set_a (val) { a = val }
})
}
const constructor_y = function (spec) {
let { a, b } = spec
const { method_x, get_a, set_a } = constructor_x(spec)
const method_y = function () { b = '...' }
const method_z = function () { set_a('...') }
return Object.freeze({ method_x, method_y, method_z })
}
These accessors can now be used by constructor_y to access private state of constructor_x. They are something like protected members in classical inheritance model. This makes constructor_x in some way special: It is not to be used as normal constructor, but only for composition inside other constructors. Another problem is that if we had another constructor like constructor_x which works on private variable a, we couldn't use them together in composition:
// another constructors which wants to work on `a`
const constructor_x2 = function (spec) => {
let { a } = spec
const method_z = function () { a = '...' }
return Object.freeze({
method_z,
get_a () { return a },
set_a (val) { a = val }
})
}
const constructor_y = function (spec) {
let { a, b } = spec
const { method_x, get_a, set_a } = constructor_x(spec)
const { method_x2, get_a: get_a2, set_a: set_a2 } = constructor_x2(spec)
// How do I use variable a now? There are two of them
// and constructors x and x2 don't share them.
}
All of this would not be a problem if I used this and modified state on the instance.
From my comments above ...
"1/2 ... First, all this creator functions should be referred to as factories or factory functions. They are not constructors. ... What if we want our constructors to share state? " ... then just implement the factories in a way that they can share each their entire inner/encapsulated state object and/or that they aggregate a shared state object while running the object creation process (the chained invocation of related functions during the composition process)."
What the OP wants to achieve can not be entirely covered by closure creating factory functionality according to Crockford / provided by the OP.
Encapsulated but shared (thus also mutable) state amongst "Function based Composable Units of Reuse" gets achieved best by a single factory which takes care of the composition process by invoking one or more mixin like functions which in addition to the to be shaped / aggregated type (the latter should carry public methods only) also need to get passed the type's local state(which will be accessed by the types's public methods).
function withActionControl(type, actionState) {
actionState.isInAction = false;
return Object.assign(type, {
monitorActions() {
const {
isInAction,
...otherActions } = actionState;
return { ...otherActions };
},
});
}
function withCanSwimIfNotBlocked(type, state) {
state.isSwimming = false;
return Object.assign(type, {
startSwimming() {
if (!state.isInAction) {
state.isInAction = true;
state.isSwimming = true;
console.log({ startSwimming: { state } })
}
},
stopSwimming() {
if (state.isSwimming) {
state.isInAction = false;
state.isSwimming = false;
console.log({ stopSwimming: { state } })
}
},
});
}
function withCanFlyIfNotBlocked(type, state) {
state.isFlying = false;
return Object.assign(type, {
startFlying() {
if (!state.isInAction) {
state.isInAction = true;
state.isFlying = true;
console.log({ startFlying: { state } })
}
},
stopFlying() {
if (state.isFlying) {
state.isInAction = false;
state.isFlying = false;
console.log({ stopFlying: { state } })
}
},
});
}
function withLaysEggsIfNotBlocked(type, state) {
state.isLayingEggs = false;
return Object.assign(type, {
startLayingEggs() {
if (!state.isInAction) {
state.isInAction = true;
state.isLayingEggs = true;
console.log({ startLayingEggs: { state } })
}
},
stopLayingEggs() {
if (state.isLayingEggs) {
state.isInAction = false;
state.isLayingEggs = false;
console.log({ stopLayingEggs: { state } })
}
},
});
}
function createSeabird(type) {
const birdState = {
type,
actions: {},
};
const birdType = {
valueOf() {
return JSON.parse(
JSON.stringify(birdState)
);
},
};
const { actions } = birdState;
withActionControl(birdType, actions)
withLaysEggsIfNotBlocked(birdType, actions);
withCanFlyIfNotBlocked(birdType, actions);
withCanSwimIfNotBlocked(birdType, actions);
return birdType;
}
const wisdom = createSeabird({
family: 'Albatross',
genus: 'North Pacific albatross',
species: 'Laysan albatross',
name: 'Wisdom',
sex: 'female',
age: 70,
});
console.log({ wisdom });
console.log('wisdom.valueOf() ...', wisdom.valueOf());
console.log('wisdom.monitorActions() ...', wisdom.monitorActions());
console.log('wisdom.startFlying();')
wisdom.startFlying();
console.log('wisdom.startFlying();')
wisdom.startFlying();
console.log('wisdom.startSwimming();')
wisdom.startSwimming();
console.log('wisdom.startLayingEggs();')
wisdom.startLayingEggs();
console.log('wisdom.stopFlying();')
wisdom.stopFlying();
console.log('wisdom.stopFlying();')
wisdom.stopFlying();
console.log('wisdom.startSwimming();')
wisdom.startSwimming();
console.log('wisdom.startSwimming();')
wisdom.startSwimming();
console.log('wisdom.startLayingEggs();')
wisdom.startLayingEggs();
console.log('wisdom.startFlying();')
wisdom.startFlying();
console.log('wisdom.stopSwimming();')
wisdom.stopSwimming();
console.log('wisdom.stopSwimming();')
wisdom.stopSwimming();
console.log('wisdom.startLayingEggs();')
wisdom.startLayingEggs();
console.log('wisdom.startLayingEggs();')
wisdom.startLayingEggs();
console.log('wisdom.valueOf() ...', wisdom.valueOf());
console.log('wisdom.monitorActions() ...', wisdom.monitorActions());
.as-console-wrapper { min-height: 100%!important; top: 0; }
Close with one of the above initial comments ...
"2/2 ... Just take advantage of the language's flexibility and expressiveness. Just be aware of the advantages, pitfalls and comprehensibility (to others) of your modeling approach(es). And once this is checked don't worry about [too strict]* Crockford disciples (or any other school / religion / cult). A good teacher shows you a [path]* and allows / encourages you to discover or follow your own, once you understood what the base/basics are good for."
I want to add functions to JSON Objects, but I can't find function for assigning to all objects, only to one.
This code works with Arrays:
Object.defineProperty(Array.prototype, 'random', {
value: () => {
return this[Math.floor(Math.random() * this.length)];
},
});
I've also found this code:
const obj = {name: 'Bob'};
obj.fullName = function() { return this.name }
But that one only works for specific object, not all of them.
Is it event possible to write global functions for all JSON Objects, and if is, then how to do it?
You could add the function to Object.prototype. Note that this is not considered a very good practice because it could impact the rest of the code (like shown in the comments):
Object.prototype.fullName = function() { return this.name; };
const obj = { name: 'Bob' };
console.log(obj.fullName());
You should consider doing this instead:
const baseObject = { fullName: function() { return this.name; } };
const obj = Object.create(baseObject, { name: { value: 'Bob', writable: true } });
console.log(obj.fullName());
And if your target runtime (browser?) supports ECMAScript 6, you could also create a dedicated class for this:
class MyClass {
constructor(name) {
this.name = name;
}
fullName() { return this.name; }
}
const bob = new MyClass('Bob');
console.log(bob.fullName());
Finally, the class syntax for ECMAScript 5:
function MyClass(name) {
this.name = name;
}
MyClass.prototype.fullName = function() { return this.name; }
const bob = new MyClass('Bob');
console.log(bob.fullName());
I have a class "House" like :
class House{
constructor(params){
this.clear();
// this = {...params} // I know that don't work !!!
//--
// if(params.address !== undefined) this.address = {...params.address}
//...
}
clear(){
this.address = {
number: null,
street: null,
zipcode: null,
ton: null,
}
this.access = {
doorcode: null,
stair: null,
}
}
}
I want to create a new instance of House and inject in constructor multiple json like :
const h = new House({address: { /* json */ }, access: { /* json */});
Or only one like :
const h = new House({access: { /* json */});
In constructor, am i obliged to check all values in "params" to insert in good properties (nested object)
I would like to avoid to create other classes like address and access and in the house constructor create new instance of each.
What's the best practice ?
Regards
Using Object.assign() and object destructuring with default parameters in the constructor, you can achieve this quite easily:
class House {
static get defaultAddress () {
return {
number: null,
street: null,
zipcode: null,
town: null
}
}
static get defaultAccess () {
return {
doorcode: null,
stair: null
}
}
constructor({ address = House.defaultAddress, access = House.defaultAccess } = {}) {
this.clear()
Object.assign(this.address, address)
Object.assign(this.access, access)
}
clear () {
const { defaultAddress, defaultAccess } = House
Object.assign(this, { address: defaultAddress, access: defaultAccess })
}
}
// no object
console.log(new House())
// empty object
console.log(new House({}))
// partial object
console.log(new House({ address: { number: 1, street: 'street', zipcode: 12345, town: 'town' } }))
// empty sub-objects
console.log(new House({ address: {}, access: {} }))
// partial sub-objects
console.log(new House({ address: { number: 1, street: 'street' }, access: { doorcode: 321 } }))
// complete object
console.log(new House({ address: { number: 1, street: 'street', zipcode: 12345, town: 'town' }, access: { doorcode: 321, stair: 3 } }))
.as-console-wrapper{min-height:100%!important}
You can loop through the parameters and set them manually. Then, to clear, remove all own properties (properties that aren't inherited).
class House {
constructor(params) {
// set data
Object.assign(this, params);
}
clear() {
for (let key in this) {
if (this.hasOwnProperty(key))
this[key] = undefined; // or `delete this[key];`
}
}
}
let house = new House({type: "normal", height: 40});
console.log(house, house instanceof House);
Of course, you probably want to limit the input keys to a predefined set. You could store those keys in a static class variable and use them to loop through the properties in constructor and clear.
class House {
constructor(params) {
// check for invalid properties
Object.keys(params).forEach(key => {
if (!House.keys.includes(key))
throw `Invalid paramater ${key}`;
});
// set data
Object.assign(this, params);
}
clear() {
for (let key in House.keys) {
if (this.hasOwnProperty(key))
this[key] = undefined; // or `delete this[key];`
}
}
}
House.keys = ['type', 'height'];
let house = new House({type: 'normal', height: 40});
console.log(house, house instanceof House);
let error = new House({helloWorld: true});
I think you want a common namespace for your instance properties - similar to React's props pattern - you can also specify defaults for each instance you are creating:
const defaultProps = { address: {}, access: {} };
class House {
constructor(props = {}) {
this.props = {...defaultProps, ...props};
}
clear() {
this.props = {...defaultProps};
}
}
Look at the below example:
class Parent {
constructor({ parentOnlyArg = 'default value' } = {}) {
this.parentOnlyArg = parentOnlyArg;
}
}
class Child extends Parent {
// this class and also any class inheriting from it
constructor({ visibleStyle = 'inline' } = {}) {
// I want to pass argument to super as an object
super(/** args **/);
this.visibleStyle = visibleStyle;
}
}
class Child2 extends Parent {
// Specifying parentOnlyArg as default will change the behaviour
constructor({ parentOnlyArg = 'another parent value',
someOther = 'value' } = {}) {
// I want to pass argument to super as an object
super(/** args **/);
this.someOther = someOther;
}
}
Is it possible to pass on the constructor argument to super?
Seems like it was simpler than I thought
super(...arguments);
I can then create Child using
var c1 = new Child(); // c.parentOnlyArg = 'default value'
var c2 = new Child2(); // c.parentOnlyArg = 'another parent value'
var c3 = new Child({ parentOnlyArg: 'changed again' }); // c.parentOnlyArg = 'changed again'
You could use object destructuring with rest properties. It is not yet implemented by browsers, but BabelJs can transpile it.
function assertEmpty(obj) {
if (Object.keys(obj).length > 0) {
throw new Error("Unexpected parameters");
}
}
class A {
constructor({ a = "foo", ...rest } = {}) {
assertEmpty(rest);
console.log("new A " + a);
}
}
class B extends A {
constructor({ b = "bar", ...rest } = {}) {
super(rest);
console.log("new B " + b);
}
}
new B({a:2}); // prints 'new A 2', 'new B bar'
new B({a:4, b:5, c:6}); // throws 'Unexpected parameters'
In the above snippet parent classes don't see the params consumed by the descendants. If you have problems with that you can do it either as #Bergi or #loganfsmyth suggested. For example:
class A {
constructor(params = {}) {
const { a = "foo" } = params;
console.log("a=" + a);
}
}
class B extends A {
constructor(params = {}) {
const { b = "bar" } = params;
super(params);
console.log("b=" + b);
}
}
new B({a:2}); // prints a=2 b=bar
new B({b:5}); // prints a=foo b=5
A quick-win is to use the arguments object. It is an array containing all parameters passed to a function.
More information on the MDN.
In practice, you can access to the first parameter of your function thanks to arguments[0].
class Child extends Parent {
constructor({ parentOnlyArg = 'value',
visibleStyle = 'inline' } = {}) {
super(arguments[0]);
[...]
}
}