Related
I am learning the concepts of Composition in JS. Below is my demo code.
The moveBy function assigns the values correctly to x and y.
However, the setFillColor function does not assign the passed value to fillColor.
What exactly is happening when the setFillColor function is called?
const withMoveBy = (shape) => ({
moveBy: (diffX, diffY) => {
shape.dimensions.x += diffX;
shape.dimensions.y += diffY;
},
});
const withSetFillColor = (shape) => ({
setFillColor: (color) => {
console.log(shape.fillColor); // 1
shape.fillColor = color;
shape.dimensions.fillColor = color;
console.log(shape.fillColor); // 2
},
});
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shape = null;
switch (type) {
case 'rectangle': {
shape = shapeRectangle(dimensions);
break;
}
case 'circle': {
shape = shapeCircle(dimensions);
break;
}
}
if (shape) {
shape = {
...shape,
...withSetFillColor(shape),
...withMoveBy(shape),
};
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
OUTPUT:
Line marked as // 1 prints white in case of rectangle as well as circle.
Line marked as // 2 prints red for rectangle and blue for circle.
The final output is:
{
"type": "rectangle",
"fillColor": "white",
"dimensions": {
"x": 3,
"y": 4,
"width": 10,
"height": 10,
"fillColor": "red"
}
}
{
"type": "circle",
"fillColor": "white",
"dimensions": {
"x": 11,
"y": 12,
"diameter": 10,
"fillColor": "blue"
}
}
The fillColor as the property of object is still white.
However, the one inside of dimensions has taken the correct value.
The problem stems from this assignment in createShape - annotations by me:
// creating the "new object"
shape = {
...shape, // shallow copying of the "old object"
...withSetFillColor(shape),
...withMoveBy(shape),
};
Here, you create a new object that is composed of:
shallow-copied properties of the existing ...shape (type, fillcolor, dimensions which is an object)
setFillColor, a closure that is bound to shape (the old object)
moveBy, a closure that is bound to shape (the old object)
After this statement is executed, you have created two shapes:
The "old object", which the methods operate on
The "new object", which you return
Out of the properties copied from the old object, only dimensions is a non-primitive value, so it is shared between the instances.
Then, when you call:
r.moveBy(2, 3);
it changes oldShape.dimensions, but it's the same object as newShape.dimensions, so it's visible in the output.
However, this call:
r.setFillColor('red');
modifies the fillColor property of the oldShape, which you are not seeing. It also writes to oldShape.dimensions.fillColor, which, again, is shared between the objects, so that change is visible in both.
Let me illustrate the problem by re-writing your code. I have removed some of the details to focus on the issue only. Added annotations and logging to the code to show more clearly what happens:
const withSetFillColor = (shape) => ({
setFillColor: (color) => {
console.log(`now changing shape with id [${shape.id}]`);
shape.fillColor = color;
shape.dimensions.fillColor = color;
},
});
const shapeRectangle = (dimensions) => ({
id: 1, //add an ID of the created object for illustrative purpose
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
//variable is now named 1 to showcase what happens
let shape1 = null;
switch (type) {
case 'rectangle': {
shape1 = shapeRectangle(dimensions);
break;
}
}
//this is effectively what happens when you clone and reassign an object:
//a *second one* is created but the first one persists
let shape2 = null;
if (shape1) {
shape2 = {
...shape1,
...withSetFillColor(shape1),
id: 2, //make it a different ID for illustrative purpose
};
}
console.log(`Created shape1 and shape2 and they are the same: ${shape1 === shape2}`);
console.log(`The dimensions object is the same: ${shape1.dimensions === shape2.dimensions}`);
return shape2;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
r.setFillColor('red');
console.log(r);
You create and manipulate two different objects. This is the reason why the code assigns a property to the object but it appears as if it is not changed.
There are several way to deal with this.
Only create one object and assign to it
If you use Object.assign() you can directly change one object instead of having two competing ones. Thus, passing the object to the withX() functions will work as intended.
const withMoveBy = (shape) => ({
moveBy: (diffX, diffY) => {
shape.dimensions.x += diffX;
shape.dimensions.y += diffY;
},
});
const withSetFillColor = (shape) => ({
setFillColor: (color) => {
shape.fillColor = color;
shape.dimensions.fillColor = color;
},
});
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shape = null;
switch (type) {
case 'rectangle': {
shape = shapeRectangle(dimensions);
break;
}
case 'circle': {
shape = shapeCircle(dimensions);
break;
}
}
if (shape) {
//use Object assign to only manipulate one `shape` object
Object.assign(
shape,
withSetFillColor(shape),
withMoveBy(shape)
);
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
Don't use arrow functions, use this instead
Alternatively, use regular functions or the shorthand method definition syntax which lets you use this. You can then add these methods to your object and use this to refer to the object, instead of having to pass it in.
const withMoveBy = { //no need for a function to produce the object
moveBy(diffX, diffY) { //shorthand method syntax
this.dimensions.x += diffX;
this.dimensions.y += diffY;
},
};
const withSetFillColor = { //no need for a function to produce the object
setFillColor(color) { //shorthand method syntax
this.fillColor = color;
this.dimensions.fillColor = color;
},
};
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shape = null;
switch (type) {
case 'rectangle': {
shape = shapeRectangle(dimensions);
break;
}
case 'circle': {
shape = shapeCircle(dimensions);
break;
}
}
if (shape) {
shape = {
...shape,
...withSetFillColor,
...withMoveBy,
};
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
A mixed approach
This is more of an explanation of what's happening than an actual new approach.
Both of the above both work but show two sides of the same coin. Combining objects together is called mixin*. Mixins are similar to object composition because you build up more complex objects from simpler ones but also a separate category of its own since you do it via concatenation.
Traditionally, you would use Object.assign(obj, mixinA, mixinB) for adding things to obj. Which makes it similar to the first approach. However, mixinA and mixinB would be actual objects like in the second approach.
Using class syntax, there is an interesting alternative to add mixins to a class. I'm adding it here just to show it - it's totally OK to not use classes and use regular objects instead.
const withMoveBy = Base => class extends Base { //mixin
moveBy(diffX, diffY) {
this.dimensions.x += diffX;
this.dimensions.y += diffY;
}
};
const withSetFillColor = Base => class extends Base { //mixin
setFillColor(color) {
this.fillColor = color;
this.dimensions.fillColor = color;
}
};
class Shape {
constructor({type, fillColor, dimensions}) {
this.type = type;
this.fillColor = fillColor;
this.dimensions = dimensions;
}
}
const shapeRectangle = (dimensions) => ({
type: 'rectangle',
fillColor: 'white',
dimensions,
});
const shapeCircle = (dimensions) => ({
type: 'circle',
fillColor: 'white',
dimensions,
});
const createShape = (type, dimensions) => {
let shapeArgs = null;
switch (type) {
case 'rectangle': {
shapeArgs = shapeRectangle(dimensions);
break;
}
case 'circle': {
shapeArgs = shapeCircle(dimensions);
break;
}
}
let shape = null;
if (shapeArgs) {
//add mixins to the Shape class
const mixedInConstructor = withMoveBy(withSetFillColor(Shape));
//create the enhanced class
shape = new mixedInConstructor(shapeArgs);
}
return shape;
};
let r = createShape('rectangle', {
x: 1,
y: 1,
width: 10,
height: 10,
});
let c = createShape('circle', { x: 10, y: 10, diameter: 10 });
r.moveBy(2, 3);
c.moveBy(1, 2);
r.setFillColor('red');
c.setFillColor('blue');
console.log(r);
console.log(c);
* Yes, the title was a pun. You can laugh now.
I'm currently storing data as objects inside a array in the following way:
let data = [];
module.exports.init = function() {
database.pool.query("SELECT * FROM data", (error, rows) => {
if (error) {
logUtil.log.error(`Loading failed: ${ error.message }`);
}
else {
rows.forEach((row) => data.push({dimension: row.dimension, x: row.x, y: row.y, z: row.z}));
logUtil.log.info(data);
}
});
};
data will hold the following now: [{ dimension: 2, x: -973.097, y: -133.411, z: 38.2531 }, { dimension: 3, x: -116.746, y: -48.414, z: 17.226 }, { dimension: 2, x: -946.746, y: -128.411, z: 37.786 }, { dimension: 2, x: -814.093, y: -106.724, z: 37.589 }]
Now I'm trying to receive a random object from this array storing a specific dimension parameter.
For example I want to return a random object storing the dimension: 2
I've tried to filter the array using something like:
let result = jsObjects.filter(data => {
return data.dimension === 2
})
then return a random object from the result.
Question: How could I receive this random object in the best way?
You can do it in two steps.
Get all record which satisfy criteria like dimension === 2
let resultArr = jsObjects.filter(data => {
return data.dimension === 2
})
Get random object from result.
var randomElement = resultArr[Math.floor(Math.random() * resultArr.length)];
var arr = [{ dimension: 2, x: -973.097, y: -133.411, z: 38.2531 }, { dimension: 3, x: -116.746, y: -48.414, z: 17.226 }, { dimension: 2, x: -946.746, y: -128.411, z: 37.786 }, { dimension: 2, x: -814.093, y: -106.724, z: 37.589 }]
//Filter out with specific criteria
let resultArr = arr.filter(data => {
return data.dimension === 2
})
//Get random element
var randomElement = resultArr[Math.floor(Math.random() * resultArr.length)];
console.log(randomElement)
You could use Math.random() and in the range of 0 to length of array.
let result = jsObjects.filter(data => {
return data.dimension === 2
})
let randomObj = result[Math.floor(Math.random() * result.length)]
I have a javascript object literal as follows.
data: {
prop1ByZones:[{zone: "Zone1", x: 1}, {zone: "Zone2", x: 5}],
prop2ByZones:[{zone: "Zone1", y: "1302.5"}],
prop3ByZones:[{zone: "Zone2", z: 2}]
}
the output should be like -
output: [{zone: "Zone1", x: 1, y: "1302.5", z: 0}, {zone: "Zone2", x: 5, y: 0, z: 2}]
I can do it in trivial way like first add prop1ByZones to output and then loop through prop2ByZones and prop3ByZones and check for existing zone. if the zone is there then update it else add it.
I just wanted to check if there is any elegant way of doing it. Please let me know.
One possible approach:
var data = { prop1ByZones:[{zone: "Zone1", x: 1}, {zone: "Zone2", x: 5}], prop2ByZones:[{zone: "Zone1", y: "1302.5"}], prop3ByZones: [{zone: "Zone2", z: 2}] }
var arr = [...data.prop1ByZones, ...data.prop2ByZones, ...data.prop3ByZones]
var resp = arr.reduce((acc, { zone, x, y, z }) => {
var prev = acc.find(x => zone == x.zone);
if(prev) {
prev.x = x ? x : prev.x,
prev.y = y ? y : prev.y,
prev.z = z ? z : prev.z
return acc;
}
return acc.concat({zone: zone, x: x ? x : 0, y: y ? y : 0, z: z ? z : 0});
}, []);
console.log(resp)
.as-console-wrapper { max-height: 100% !important; top: 0; }
Here's a way to do this (using lodash for iterating), you could optimize the conditionals a bit to be ternaries etc, but this would give you your output:
const data = {
prop1ByZones:[{zone: "Zone1", x: 1}, {zone: "Zone2", x: 5}],
prop2ByZones:[{zone: "Zone1", y: "1302.5"}],
prop3ByZones:[{zone: "Zone2", z: 2}]
};
let list = {};
_.each(data, (d, i) => {
_.each(d, (e) => {
const zone = e.zone;
if (!list[zone]) {
list[zone] = zone;
list[zone] = {};
}
if (!list[zone].x) {
list[zone].x = e.x || 0;
}
if (!list[zone].y) {
list[zone].y = e.y || 0;
}
if (!list[zone].z) {
list[zone].z = e.z || 0;
}
});
});
// put everything in an array
let result = [];
_.each(list, (obj, k) => {
result.push({
zone: k,
x: obj.x,
y: obj.y,
z: obj.z
});
});
console.log(result);
You can get the Object.values(), flatten to a single array by spreading into Array.concat(), and then reduce the array to a Map, and spread the Map.values() iterator back to array:
const data = {
prop1ByZones:[{zone: "Zone1", x: 1}, {zone: "Zone2", x: 5}],
prop2ByZones:[{zone: "Zone1", y: "1302.5"}],
prop3ByZones:[{zone: "Zone2", z: 2}]
}
const result = [... // spread the map values iterator (see end) back to an array
[].concat(...Object.values(data)) // get the object's values and flatten to a single array
.reduce( // reduce the array to a Map
// add the zone key to the map, and include the previous and current item
(r, o) => r.set(o.zone, { ...(r.get(o.zone) || {}), ...o }),
new Map()
).values()] // get the map values iterator
console.log(result)
I was wondering if it with rest/spread is possible to only override the existing properties on an object:
let xy = {
x: 1,
y: 2,
}
let xyz = {
x: 41,
y: 23,
z: 1
}
Now i have two objects and i wish to override the existing properties on xy without getting the z property from xyz as well, so my output is the following:
xy = {
x: 41,
y: 23,
}
Is this possible?
Thanks in advance!
Rest/spread will always stuff into a destination what it finds in a source object, if you don't want to use a loop / want a functional approach, go for reduce, e.g.
const xyNew = Object.keys(xyz).reduce((res, key) =>
// if the key is contained in the accumulator, rewrite it with xyz value, else just return the accumulator (res ~ "result")
res[key] ? { ...res, [key]: xyz[key] } : res
, xy);
Here is a simple implementation :-)
let xy = {
x: 1,
y: 2,
}
let xyz = {
x: 41,
y: 23,
z: 1
}
function compute(){
let key = Object.keys(xy);
for(let i=0;i<key.length;i++){
if(key[i] in xyz) xy[key[i]] = xyz[key[i]];
}
}
compute();
console.log(xy);
Say I have this:
interface Point {
x: number;
y: number;
}
interface Line {
vertix1: Point;
vertix2: Point;
}
let v1: Point = { x: 1, y: 2 };
let v2: Point = { x: 1, y: 2 };
let line: Line = {vertix1: v1, vertix2: v2};
How can I define line directly without defining v1 and v2? I tried and that, obviously, did not work:
let line1: Line = {
vertix1: Point = { x: 1, y: 2 },
vertix2: Point = { x: 1, y: 2 },
}
It would be just :
let line1: Line = {
vertix1: { x: 1, y: 2 },
vertix2: { x: 1, y: 2 },
}
classes can easily be skipped thanksfully.
You might be better off using a class which implements an interface.
interface IPoint {
x: number;
y: number;
}
interface ILine {
vertix1: Point;
vertix2: Point;
}
class Point implements IPoint {
x: int;
y: int;
constructor(x: int, y: int) {
this.x = x;
this.y = y;
}
}
class Line implments ILine {
vertix1: Point;
vertix2: Point;
constructor(vertix1: Point, vertix2: Point) {
this.vertix1 = vertix1;
this.vertix2 = vertix2;
}
}
let v1: Point = new Point(1,2);
let v2: Point = new Point(1,2);
let line: Line = new Line(v1,v2);
You could also create the points in the constructor. Not sure if this answers your question but hope it helps.
let line: Line = new Line(new Point(1,2), new Point(1,2))