Object array clone with subset of properties - javascript

What is the most efficient way in JavaScript to clone an array of uniform objects into one with a subset of properties for each object?
UPDATE
Would this be the most efficient way to do it or is there a better way? -
var source = [
{
id: 1,
name: 'one',
value: 12.34
},
{
id: 2,
name: 'two',
value: 17.05
}
];
// copy just 'id' and 'name', ignore 'value':
var dest = source.map(function (obj) {
return {
id: obj.id,
name: obj.name
};
});

using Object Destructuring and Property Shorthand
let array = [{ a: 5, b: 6, c: 7 }, { a: 8, b: 9, c: 10 }];
let cloned = array.map(({ a, c }) => ({ a, c }));
console.log(cloned); // [{ a: 5, c: 7 }, { a: 8, c: 10 }]

First define a function that clone an object and return a subset of properties,
Object.prototype.pick = function (props) {
return props.reduce((function (obj, property) {
obj[property] = this[property];
return obj;
}).bind(this), {});
}
Then define a function that clone an array and return the subsets of each object
function cloneArray (array, props) {
return array.map(function (obj) {
return obj.pick(props);
});
}
Now let's say you have this array :
var array = [
{ name : 'khalid', city : 'ifrane', age : 99 },
{ name : 'Ahmed', city : 'Meknes', age : 30 }
];
you need to call the function and pass the array of properties you need to get as result
cloneArray(array, ['name', 'city']);
The result will be :
[
{ name : 'khalid', city : 'ifrane' },
{ name : 'Ahmed', city : 'Meknes' }
]

Performance-wise, that would be the most efficient way to do it, yes.

Related

JS forEach traverses the problem of modifying objects

Why does the first modification work (when I reassign properties of the object) but not the second (when I reassign the whole object)?
const arr1 = [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 },
{ id: 4, value: 4 },
{ id: 5, value: 5 },
]
arr1.forEach((item, index) => {
if (item.id === 1) {
item.value *= 10 // modify success
}
});
console.log(arr1);
arr1.forEach((item, index) => {
if (item.id === 1) {
item = {id:6,value:6} // modify fail
}
});
console.log(arr1);
.as-console-wrapper { max-height: 100% !important; top: auto; }
Consider the example below:
We create an object and assign it to variable foo and then we assign foo to bar. So, now both foo and bar refer to the same object, as illustrated in the diagram below.
let foo = { id: 1, val: "foo" };
let bar = foo;
Next let's change the val field of the object i.e. assigned to bar. We notice that the change is reflected by both the variables foo and bar and this is because both the variables refer to the same object.
let foo = {id: 1,val: "foo"};
let bar = foo;
bar.val = "bar";
console.log(foo, bar);
Next we assign a new object to bar. Notice this doesn't effect the object that foo refers to, bar is simply now referring to a different object.
let foo = { id: 1, val: "foo" };
let bar = foo;
bar = { id: 1, val: "bar" };
console.log(foo, bar);
Let's relate this to the forEach example in your question. So, in every iteration of the forEach loop, the item argument in the callback function points to an object from the array and when you change a field from this item argument it changes the object in the array but when you assign item to a new object it does nothing to the object stored in the array.
If you want to replace the entire object with a new one, there are several approaches you could take, two of which are mentioned below:
Finding the index where the object is stored and replacing it with a new object.
const arr = [{ id: 1, value: 1 }, { id: 2, value: 2 }, { id: 3, value: 3 }, { id: 4, value: 4 }, { id: 5, value: 5 }];
const index = arr.findIndex((obj) => obj.id === 3);
if (index !== -1) {
arr[index] = { id: 6, value: 6 };
}
console.log(arr);
Another really common approach is to map over the array and create a new array with that one object replaced.
const arr = [{ id: 1, value: 1 }, { id: 2, value: 2 }, { id: 3, value: 3 }, { id: 4, value: 4 }, { id: 5, value: 5 }];
const newArr = arr.map((obj) => (obj.id === 3 ? { id: 6, value: 6 } : obj));
console.log(newArr);
Your confusion is natural, and it's to do with the difference between the actual object, and the variable.
In your foreach function, you have a variable "item" which points to the object in the array you are looping. But when you run
item = {id:6,value:6}
You are not modifying the object, but rather making the variable "item" point to a new object that you've just created.
If you want to change the actual object itself, you can do it manually by modifying the values one by one, or by copying from another object using Object.assign.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
In this snippet, I've made a few examples that hopefully illustrate the differences, and at the end your specific case being solved using Object.assign()
const a = { id: 1, value: 1 };
let b = a; // b now points to the same object as variable a
b = {id: 2, value: 2};
console.log('a', a, 'b', b) // output shows two different objects with different values
const c = {id: 3, value: 3};
const d = c;
Object.assign(c, {id: 4, value: 4});
console.log('c', c, 'd', d); // c and d both have id: 4 and value: 4 because they both point to the same object
const e = { id: 5, value: 5 };
const f = e;
e.id = 6;
console.log('e', e, 'f', f); // directly assigning the value also works
const arr1 = [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 },
{ id: 4, value: 4 },
{ id: 5, value: 5 },
]
arr1.forEach((item, index) => {
if (item.id === 1) {
item.value *= 10 // modify success
}
})
arr1.forEach((item, index) => {
if (item.id === 1) {
Object.assign(item, {id:6,value:6}) // success with assign
}
})
console.log(arr1)
Because item is a reference to the object that's currently being iterated over, modifying the reference will in turn modify the original object (hence the behaviour observed in your first code example). In the second forEach you just reassign the reference to a brand new object so you can no longer modify the original object you were iterating over.
Basically, when you loop over the array with forEach, item is a variable whose current value is a reference/pointer to the object currently being iterated over. Modifying this object modifies the original (as it's just a reference, not an actual cloned object). However, when you reassign it, the reference is no longer present because item has a new value that it points to. You could modify it like so, with success:
const arr1 = [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 },
{ id: 4, value: 4 },
{ id: 5, value: 5 },
];
arr1.forEach((item, index) => {
if (item.id === 1) {
item.value *= 10 // modify success
}
});
console.log(arr1);
arr1.forEach((item, index) => {
if (item.id === 1) {
item.id = 6;
item.value = 6;
}
});
console.log(arr1);
.as-console-wrapper { max-height: 100% !important; top: auto; }
You could do it dynamically as well, by looping over each property you wish to reassign like so:
const arr1 = [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 },
{ id: 4, value: 4 },
{ id: 5, value: 5 },
];
arr1.forEach((item, index) => {
if (item.id === 1) {
item.value *= 10 // modify success
}
});
console.log(arr1);
arr1.forEach((item, index) => {
if (item.id === 1) {
let toConvert = { id: 6, value: 6};
Object.entries(toConvert).forEach(([k, v]) => item[k] = v);
}
});
console.log(arr1);
.as-console-wrapper { max-height: 100% !important; top: auto; }
For the first case, it can be understood as:
const it = { value: 10 };
const item = it;
item.value = 11;
console.log(it); // item and it point to the same object, so the object's property value is modified
second case:
const it = { value: 10 };
let item = it; // Here item and it refer to the same object
item = { value: 6 }; // But here item points to the new object (direct assignment), and it still points to the original object
console.log(item, it); // So it is still { value: 10 }, and item is already { value: 6 }

Reordering an object to the keys are in the same order as an array

I have an array that looks like this,
['event_tag', 'workflow_tag', 'created_timestamp', 'success']
and an array of objects where the object looks like,
{
"created_timestamp": "2022-04-01T13:14:53.028002Z",
"workflow_tag": "dj807",
"event_tag": "refresh",
"success": true
}
What I am wanting to do is make the above object and any other objects in that array match the order of the values in the first array so the finished object should look like,
{
"event_tag": "refresh",
"workflow_tag": "dj807",
"created_timestamp": "2022-04-01T13:14:53.028002Z",
"success": true
}
I have tried the following so far,
const keys = ['event_tag', 'workflow_tag', 'created_timestamp', 'success'];
newRowData = parsedRows.reduce((obj, v) => {
obj[v] = keys[v];
return obj
}, {});
But this returns,
{[object Object]: undefined}
You could order the keys by constructing a new object inside of an Array#map:
const parsedRows = [ { a: 1, c: 3, d: 4, b: 2, }, { b: 6, a: 5, c: 7, d: 8, }, { d: 12, b: 10, a: 9, c: 11, }, ];
const order = ['a', 'b', 'c', 'd'];
let newData = parsedRows.map(row => {
let newRow = {};
for (let key of order) {
newRow[key] = row[key];
}
return newRow;
});
console.log(newData);
Instead of iterating over Rows, Iterate on keys either map/reduce.
const keys = ["event_tag", "workflow_tag", "created_timestamp", "success"];
const obj = {
created_timestamp: "2022-04-01T13:14:53.028002Z",
workflow_tag: "dj807",
event_tag: "refresh",
success: true,
};
const res = Object.assign({}, ...keys.map((key) => ({ [key]: obj[key] })));
console.log(res)

How do I filter the unique objects from an array if the filter key can have any type of value?

interface FormValues {
key: string;
value: any;
}
const array: FormValues[] = [
{
key: 'A',
value: 1 // number
},
{
key: 'A',
value: 1 // number
},
{
key: 'A',
value: 'str' // string
},
{
key: 'C',
value: { a: 1, b: '2' } // object
},
{
key: 'C',
value: ['a','2'] // array
},
{
key: 'C',
value: ['a','2'] // array
}
{
key: 'B',
value: true // boolean
}
]
I want to filter the objects based on field value, which can have a value of any type.
I tried to do it like this; my solution is not working for nested object checks.
const key = 'value';
const arrayUniqueByKey = [...new Map(array.map(item => [item[key], item])).values()];
output :
[{
key: 'A',
value: 1 // number
},
{
key: 'A',
value: 'str' // string
},
{
key: 'C',
value: { a: 1, b: '2' } // object
},
{
key: 'C',
value: ['a','2'] // array
},
{
key: 'B',
value: true // boolean
}]
You need to decide what makes two distinct objects "equal". In JavaScript, all built-in comparisons of objects (which includes arrays) are by reference. That means ['a','2'] === ['a','2'] is false because two distinct array objects exist, despite having the same contents. See How to determine equality for two JavaScript objects? for more information.
I will take the approach that you would like two values to be considered equal if they serialize to the same value via a modified version of JSON.stringify() where the order of property keys are guaranteed to be the same (so {a: 1, b: 2} and {b: 2, a: 1} will be equal no matter how those are stringified). I use a version from this answer to do so:
function JSONstringifyOrder(obj: any, space?: number) {
var allKeys: string[] = [];
var seen: Record<string, null | undefined> = {};
JSON.stringify(obj, function (key, value) {
if (!(key in seen)) {
allKeys.push(key); seen[key] = null;
}
return value;
});
allKeys.sort();
return JSON.stringify(obj, allKeys, space);
}
And now I can use that to make the keys of your Map:
const arrayUniqueByKey = [...new Map(array.map(
item => [JSONstringifyOrder(item[key]), item]
)).values()];
And you can verify that it behaves as you'd like:
console.log(arrayUniqueByKey);
/* [{
"key": "A",
"value": 1
}, {
"key": "A",
"value": "str"
}, {
"key": "C",
"value": {
"a": 1,
"b": "2"
}
}, {
"key": "C",
"value": [
"a",
"2"
]
}, {
"key": "B",
"value": true
}] */
Playground link to code
This will combine any duplicate keys, creating a new property values to hold the array of combined values (from like keys).
const array = [{key: 'A', value: 1},{key: 'A', value: 'str'},{key: 'C', value: { a: 1, b: '2'}},{key: 'B',value: true}]
const arrayUniqueByKey = [array.reduce((b, a) => {
let f = b.findIndex(c => c.key === a.key)
if (f === -1) return [...b, a];
else {
b[f].values = [...[b[f].value], a.value];
return b
}
}, [])];
console.log(arrayUniqueByKey)
You can use Array.prototype.reduce() combined with JSON.stringify() and finaly get the result array of values with Object.values()
const array = [{key: 'A',value: 1,},{key: 'A',value: 1,},{key: 'A',value: 'str',},{key: 'C',value: { a: 1, b: '2' },},{key: 'C',value: ['a', '2'],},{key: 'C',value: ['a', '2'],},{key: 'B',value: true}]
const result = Object.values(array.reduce((a, c) => ((a[JSON.stringify(c)] = c), a), {}))
console.log(result)

Pick specific properties from nested objects

I have javascript objects which looks similar to this:
{
id: 43,
name: 'ajajaj'
nestedObj1: {
id: 53,
name: 'ababab'
foo: 'xxxx'
nestedObj2: {
id: 90,
name: 'akakaka'
foo2: 'sdsddd'
surname: 'sdadasd'
nestedObj3: {
id: ..
name: ..
...
},
objectsArr: [
{id: .., name: ..., nestedOb4: {...} },
{id: .., name: ..., nestedOb4: {...} }
]
},
foo0: ...
}
name: 'blabla'
}
Each objects which I have, has property id and name. My goal is pick all properties in first level, so properties of object with id 43 I pick all, and from every nested objects, I just pick properties id and name, and another will be thrown away.
I am using in project lodash, so I found function pick, but I don't know how to use this function for nested objects. Also note that nested objects could be also in array. Exist some elegant way for that please? Thanks in advice.
pick function of lodash is able to fetch nested property like so:
_.pick(obj, [
'id',
'name',
'nestedObj1.id',
'nestedObj1.name',
'nestedObj1.nestedObj2.id',
'nestedObj1.nestedObj2.name',
'nestedObj1.nestedObj2.nestedObj3.id',
'nestedObj1.nestedObj2.nestedObj3.name',
'nestedObj1.nestedObj2.objectsArr[0].id',
'nestedObj1.nestedObj2.objectsArr[0].name',
'nestedObj1.nestedObj2.objectsArr[0].nestedObj4.id',
'nestedObj1.nestedObj2.objectsArr[0].nestedObj4.name',
'nestedObj1.nestedObj2.objectsArr[1].id',
'nestedObj1.nestedObj2.objectsArr[1].name',
'nestedObj1.nestedObj2.objectsArr[1].nestedObj4.id',
'nestedObj1.nestedObj2.objectsArr[1].nestedObj4.name',
])
The problem is, you must know the array length to pick all objects in them.
To mitigate this, you can use this function to iterate over arrays:
const _ = require('lodash')
const pickExtended = (object, paths) => {
return paths.reduce((result, path) => {
if (path.includes("[].")) {
const [collectionPath, itemPath] = path.split(/\[]\.(.+)/);
const collection = _.get(object, collectionPath);
if (!_.isArray(collection)) {
return result;
}
const partialResult = {};
_.set(
partialResult,
collectionPath,
_.map(collection, item => pickExtended(item, [itemPath]))
);
return _.merge(result, partialResult);
}
return _.merge(result, _.pick(object, [path]));
}, {});
};
You can then get the result you want with:
pickExtended(obj, [
'id',
'name',
'nestedObj1.id',
'nestedObj1.name',
'nestedObj1.nestedObj2.id',
'nestedObj1.nestedObj2.name',
'nestedObj1.nestedObj2.nestedObj3.id',
'nestedObj1.nestedObj2.nestedObj3.name',
'nestedObj1.nestedObj2.objectsArr[].id',
'nestedObj1.nestedObj2.objectsArr[].name',
'nestedObj1.nestedObj2.objectsArr[].nestedObj4.id',
'nestedObj1.nestedObj2.objectsArr[].nestedObj4.name',
])
Here is the associated mocha test:
describe("pickExtended", () => {
const object = {
a: 1,
b: "2",
c: { d: 3, e: 4 },
f: [
{ a: 11, b: 12, c: [{ d: 21, e: 22 }, { d: 31, e: 32 }] },
{ a: 13, b: 14, c: [{ d: 23, e: 24 }, { d: 33, e: 34 }] }
],
g: [],
h: [{ a: 41 }, { a: 42 }, { a: 43 }]
};
it("should pick properties in nested collection", () => {
expect(
pickExtended(object, ["a", "c.d", "f[].c[].d", "g[].a", "b[].a", "h[]"])
).to.deep.equal({
a: 1,
c: { d: 3 },
f: [{ c: [{ d: 21 }, { d: 31 }] }, { c: [{ d: 23 }, { d: 33 }] }],
g: []
});
});
});
Keep in mind that no performance test was done on this. It might get slow for big object.
You can do this using recursion, as long as your objects do have an end to the levels of nesting. Otherwise, you run the risk of causing a stack limit error.
function pickProperties(obj) {
var retObj = {};
Object.keys(obj).forEach(function (key) {
if (key === 'id' || key === 'name') {
retObj[key] = obj[key];
} else if (_.isObject(obj[key])) {
retObj[key] = pickProperties(obj[key]);
} else if (_.isArray(obj[key])) {
retObj[key] = obj[key].map(function(arrayObj) {
return pickProperties(arrayObj);
});
}
});
return retObj;
}

Combine object and arrays

I'm trying to write a function that takes an array of objects, and an unlimited number of arrays, and combines them to form a single object. The inputs would follow this pattern:
let x = [{ name: 'Tom' }, { name: 'John' }, { name: 'Harry' }];
let y = [[1, 2, 3], 'id'];
let z = [['a', 'b', 'c'], 'value'];
combine(x, y, z);
With the second element of y and z acting as the object key. Using these arguments, the function should return the following array:
[
{
name: 'Tom',
id: 1,
value: 'a'
},
{
name: 'John',
id: 2,
value: 'b'
},
{
name: 'Harry',
id: 3,
value: 'c'
},
]
The index of the current object should be used to get the correct element in the array. I have made an attempt at the problem:
function combine(object, ...arrays) {
return object.map((obj, index) => {
let items = arrays.map(arr => ({
[arr[1]]: arr[0][index]
}));
return Object.assign({}, obj, { items });
});
}
This almost does the job, but results in the array items being hidden inside a nested items array, How can I solve this?
You had been assigning an object of object, and the result was a new object with the element items inside (another feature of object literal).
This approach use reduce instead of map and direct assign instead of object literal.
function combine(object, ...arrays) {
return object.map((obj, index) => {
const items = arrays.reduce((acc, arr) => {
acc[arr[1]] = arr[0][index] ;
return acc;
}, {});
return Object.assign({}, obj, items);
});
}
const x = [{ name: 'Tom' }, { name: 'John' }, { name: 'Harry' }];
const y = [[1, 2, 3], 'id'];
const z = [['a', 'b', 'c'], 'value'];
combine(x, y, z);
You can also use the spread operator in the Object.assign, like this:
function combine(object, ...arrays) {
return object.map((obj, index) => {
let items = arrays.map(arr => ({
[arr[1]]: arr[0][index]
}));
return Object.assign({}, obj, ...items);
});
}
This almost does the job, but results in the array items being hidden inside a nested items array
The problem is that items is an array, whereas you only need the current item inside of that particular map callback. No need to nest loops here.
Also I would recommend avoiding multiple properties per combine call. The resulting code would look like this:
function combine(objects, [values, key]) {
return objects.map((o, i) =>
Object.assign({[key]: values[i]}, o)
);
}
combine(combine(x, y), z);
If you then have multiple extensions to do, you can also use
[y, z].reduce(combine, x)
With map and computed keys, you can achieve this.
Here's a working example:
let x = [{
name: 'Tom'
}, {
name: 'John'
}, {
name: 'Harry'
}];
let y = [[1, 2, 3], 'id'];
let z = [['a', 'b', 'c'], 'value'];
let result = [];
x.map(function (el, index) {
result.push(el);
let index = result.length -1;
result[index][y[1]] = y[0][index];
result[index][z[1]] = z[0][index];
});
console.log(result);

Categories

Resources