ES6 object cloning using spread operator is modifying input too - javascript

I have a fairly deep interface declared that looks something like this:
export interface Job {
JobId: JobId; // type JobId = string
UserId: UserId; // type UserId = string
JobName: string;
AudioFile: JobAudioFile; // this is an interface
Status: JobStatus; // this is an enum
Tracks: JobTracks[]; // 'JobTracks' is an enum
Results: JobResults; // this is an interface
Timestamps: JobTimestamps // interface
}
Most of the members of this interface are themselves interfaces, with the general architecture following this pattern of using enums, strings, arrays and more interfaces. All code is written as TypeScript, transpiled down to JS and uploaded to AWS as JS. (Node 8.10 is running on AWS)
At one point in the code, I need to make a deep copy of a Job instantiation which was passed in as a function parameter:
export const StartPipeline: Handler = async (
event: PipelineEvent
): Promise<PipelineEvent> => {
console.log('StartPipeline Event: %o', event);
const newBucket = await copyToJobsBucket$(event.Job);
await deleteFromOriginalBucket$(event.Job);
console.log(`Job [${event.Job.JobId}] moved to Jobs bucket: ${newBucket}`);
event.Job.AudioFile.Bucket = newBucket;
event.Job.Status = Types.JobStatus.Processing;
// update the job status
// VVV PROBLEM OCCURS HERE VVV
const msg: Types.JobUpdatedMessage = new Types.JobUpdatedMessage({ Job: Object.assign({}, event.Job) });
await Send.to$(event.Job.UserId, msg);
return { ...event };
};
The definition of the JobUpdatedMessage:
export class JobUpdatedMessage extends BaseMessage {
constructor(payload: { Job: Types.Job }) {
console.log('Incoming: %o', payload);
const copy: object = { ...payload.Job };
// VVV PROBLEM ON NEXT LINE VVV
const filtered = JobUtils.FilterJobProperties(copy as Types.Job);
super(MessageTypes.JobUpdated, filtered);
}
}
The problem is after the call to JobUtils.FilterJobProperties, payload.Job has also been mutated in an undesirable and unexpected way.
Here's the implementation of JobUtils.FilterJobProperties:
export const FilterJobProperties = (from: Types.Job): Types.Job => {
const fieldsToRemove: string[] = [
'Transcripts.GSTT',
'Transcripts.WSTT',
'Transcripts.ASTT',
'TranscriptTracks',
'Transcripts.Stream.File',
'Transcripts.Stream.State',
'AudioFile.Bucket',
'AudioFile.S3Key',
];
let job: Types.Job = { ...from }; // LINE ONE
fieldsToRemove.forEach(field => _.unset(job, field)); // LINE TWO
return job;
};
(I'm using the lodash library here)
The line market 'LINE TWO' is also mutating the from function parameter, even though on 'LINE ONE' I'm doing what I think is a deep clone of from.
I know that this is the case because if I change 'LINE ONE' to:
// super hard core deep cloning
let job: Types.Job = JSON.parse(JSON.stringify(from));
... everything works as expected. from is not mutated, the resulting JobUpdatedMessage is as expected, and StartPipeline's event parameter doesn't have a bunch of properties removed from event.Job.
I struggled with hours on this, including relearning everything I believed I knew about cloning objects in Es6 using the spread operator.
Why was 'LINE ONE' mutating the input as well?

Spread operator does shallow cloning same as Object.assign()
Shallow-cloning (excluding prototype) or merging of objects is now
possible using a shorter syntax than Object.assign().
Spread operator
An example to understand spread operator and shallow cloning.
let obj = { 'a': { 'b' : 1 },'c': 2}
let copy = {...obj}
copy.c = 'changes only in copy' //shallow-cloned
copy.a.b = 'changed' // still reference
console.log('original\n',obj)
console.log('\ncopy',copy)
Using spread operator object is shallow cloned so all the first level properties will become a copy while all the deeper level properties will still remain the references.
so as you see in example c property doesn't affect the original object since it is one first level depth, on the other hand b property changes affect the parent properties because it is at deep level and is still reference.

Related

Unable to use structuredClone() on value of ref variable

I want to make use of the structuredClone() function inside my Vue app. I want to use this to create a deep clone ( instead of using workarounds like stringify and parse or external libraries ). Inside my setup function the following code is fine
const a = {
foo: {
bar: "+"
}
};
const b = structuredClone(a);
console.log(b);
But it is not possible for me to use it on values of ref variables. This example code
import { ref } from "vue";
const a = ref({ foo: { bar: "+" } });
const b = structuredClone(a.value);
throws the error
Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': # could not be cloned.
The same goes for items from ref arrays
import { ref } from "vue";
const a = ref([{ foo: { bar: "+" } }]);
for (const b of a.value) {
const c = structuredClone(b);
}
How can this be fixed?
The error means that structuredClone was executed on Proxy instance, which cannot be cloned. In order to allow this, it should be used on raw object that a proxy wraps:
const b = structuredClone(toRaw(a.value));
Notice that toRaw is used on a.value because both a and a.value are reactive objects, and toRaw works shallowly and needs to be applied to the innermost object.
Since ref and reactive allow to compose reactive objects, toRaw still may not work for them due to how it works:
ref({ foo: { bar: barRef } })
This would require to recursively use toRaw on reactive objects before using structuredClone. At this point this doesn't make it easier than cloning the objects manually, unless more exotic objects like Set, Map, etc are in use.

DeepCopy Object in JavaScript using immer

I am using immer to transform react/redux state. Can I also use immer to just deep copy an object without transforming it?
import produce, {nothing} from "immer"
const state = {
hello: "world"
}
produce(state, draft => {})
produce(state, draft => undefined)
// Both return the original state: { hello: "world"}
This is from the official immer README. Does this mean that passing an empty function to produce returns a deep copy of the original state or actually the original state?
Thank you very much for your help :)!
This is easily testable with
import { produce } from 'immer'
const state = {
hello: 'world',
}
const nextState = produce(state, draft => {})
nextState.hello = 'new world'
console.log(state, nextState)
which outputs
Object { hello: "new world" }
Object { hello: "new world" }
which means that it does NOT create a deep copy of an object.
UPDATE:
So I got interested and tested out the library a lot and here are my findings.
The code snippet I wrote above is simply an optimisation in the library which returns the old state if no changes are made. However, if you make some changes, then the library starts functioning as intended and the mutation later is made impossible. That is,
const state = {
hello: 'world',
}
const nextState = produce(state, draft => {
draft.hello = 'new world';
})
nextState.hello = 'newer world';
console.log(state, nextState)
will result in an error: TypeError: "world" is read-only
Which means that your newState is immutable and you can no longer perform mutations on it.
Another rather interesting thing I found is that immer fails when using class instances. That is,
class Cls {
prop = 10;
}
const instance = new Cls();
const obj = {
r: instance,
};
const newObj = produce(obj, draft => {
draft.r.prop = 15;
});
console.log(obj, newObj);
results in
r: Object { prop: 15 }
r: Object { prop: 15 }
So to get back to the initial question, can you get a deep copy of the initial Object by changing nothing in the draft. No you cannot, and even if you did (by changing a property created just to fool immer perhaps), the resultant cloned object will be immutable and not really helpful.
Solution :
The Immer's produce only provides a new deep cloned object on updation.
you can create your own produce function that behaves just like that of immer's produce but gives a cloned object everytime using loadash
import _ from 'lodash';
export default function produceClone(object, modifyfunction) {
let objectClone = _.cloneDeep(object);
if (!modifyfunction) return objectClone;
modifyfunction(objectClone);
return objectClone;
}
This will give you a deepCloned(or deep copied) object everytime, irrespective of whether you modify the object or not.

What properties will rest skip in Object Destructuring?

I am using rest parameter to do some operations, but got some properties disappeared in result.
The data format of this.props looks like this:
```
this.props = {
data: [{...}, ...],
...
}
```
And I tried to rest it in this way:
```
let {showWaveAnimation, ...otherProps} = this.props;
console.log('data' in otherProps); // false
console.log('data' in this.props); //true
```
Why would 'data' property lost after I tried rest operation?
According to MDN, I found the description:
rest parameters are only the ones that haven't been given a separate name (i.e. formally defined in function expression), while the arguments object contains all arguments passed to the function;
What does separate name here means? Is it means properties that extends from its prototype would not be rest? But after I tried the following statements, I got the same result, I am confusing.
```
class Parent {
constructor() {
this.props = {
data: '123',
};
}
}
class Child extends Parent {
constructor() {
super();
this.props.childProp = 'test';
let {test, ...otherProps} = this.props;
console.log('data' in this.props, 'data' in otherProps);
// true true
}
}
new Child();
```
The former code behaviors abnormal after Babel's transpiling, could it be Babel's plugin problem?
ADD: I found this concept may be more precise in describing my rest operations. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Rest_in_Object_Destructuring
I figured out why 'data' disappeared in my rest result, it's the enumerable property of 'data' field!
Once, the property is set as enumerable: false, we can't get it with the operation of rest in object destructuring.
According to MDN
The Rest/Spread Properties for ECMAScript proposal (stage 3) adds the rest syntax to destructuring. Rest properties collect the remaining own enumerable property keys that are not already picked off by the destructuring pattern.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Rest_in_Object_Destructuring
I think this example can explain my ideas more precisely:
const person = {
name: 'Dave',
surname: 'Bowman'
};
Object.defineProperty(person, 'age', {
enumerable: false, // Make the property non-enumerable
value: 25
});
console.log(person['age']); // => 25
const clone = {
...person
};
console.log(clone); // => { name: 'Dave', surname: 'Bowman' }
The rest parameter allows you to get all the remaining attributes not already selected by identifier preceding it in the assignment.
Let's take a simple example:
const obj = {
a: 1,
b: 2,
c: 3
}
const { a, ...rest } = obj;
console.log(a);
console.log(rest);
Above obj has a property named a so it is destructured from obj and assigned to the variable a, then all other properties are assigned in an object to rest.
However, what you have is more like:
const obj = {
a: 1,
b: 2,
c: 3
}
const { d, ...rest } = obj;
console.log(d);
console.log(rest);
Here obj doesn't have a property named d. So it is destructured from obj as undefined and assgined to the variable a. Then all other properties not already assigned, i.e. a, b and c are given to rest.
In your code otherProps has the same properties as this.props, because object this.props doesn't have a property named test. In other words, in order to destructure an object you have to know its structure.
rest parameters are only the ones that haven't been given a separate name (i.e. formally defined in function expression), while the arguments object contains all arguments passed to the function;
What does separate name here means? Is it means properties that extends from its prototype would not be rest? But after I tried the following statements, I got the same result, I am confusing.
Given a function definition function foo(a, b, ...c) {}, arguments a, and b are the named parameters, whereas the rest of the passed arguments are spread into the last variable c. Now, c is a named variable, but any parameters passed from the third on will just be accessible in c, i.e. c[0].
Now, your console.log error is stemming from your use of the in operator. You defined an object const foo = {'data': 'bar'}. 'data' in foo is true, but 'blarg' in foo is false since foo doesn't contain a key/property named 'blarg'.
You're actually using the spread operator to destructure your props object into several named variables.
const object = {
data: 'foo',
other: 'bar',
moreData: [1,2,3],
};
// Example usage of the spread operator
const { data, ...allTheRest} = object; // data is destructured, other and moreData into allTheRest
console.log(data);
console.log(allTheRest);
// in operator
console.log('data' in object); // true
console.log('data' in allTheRest); // false
console.log('moreData' in allTheRest); // true
// This is an example of the rest operator
const foo = (...allTheArgs) => {
allTheArgs.forEach(e => console.log(e));
}
foo(1,2,3); // all the args are collected into a single array variable in the function
const bar = (a, b, ...c) => {
console.log(`a: ${a}`);
console.log(`b: ${b}`);
console.log(`c: ${c}`);
};
bar('this is first param', 42, 'foo', 'bar', '1', 2, 'stackoverflow');

Methods on an object created via composition can't access all properties

I'm building the concept of a family for a product, with members being of different types (accountHolder, payingCustomer, student, and so on). Originally I built these as sub-classes of FamilyMember, but I ended up with some repeated code and eventually bumped into a significant problem: a student of our platform can also be the sole payingCustomer and accountHolder.
Given how object composition is widely touted as a good idea in JS, I decided to go that route. However, the methods of a particular object type (e.g. accountHolder) can't access properties of the instantiated object, if the property belong to another object type (e.g. student).
To make this more objective I've decided to replicate the behaviour using the following code:
const person = (props) => {
let state = {
name: props.name,
}
state.isOfAge = () => {
// state.isAdult is always undefined because
// isAdult doesn't exist in this object
return state.isAdult === true
}
return state
}
const adult = (props) => {
return {
isAdult: true,
}
}
const factory = (props) => {
return Object.assign({}, person(props), adult(props))
}
const john = factory({
name: 'John',
})
console.clear()
console.log(john) // { isAdult: true, name: "John", isOfAge... }
console.log(john.isOfAge()) // false
I was expecting john's method isOfAge to be able to access the property isAdult, since it's in the object. However, conceptually I understand why it doesn't work: isOfAge is a method of state, not the resulting adult instance.
If I were using classes or even a traditional prototype/constructor mechanism I knew how to make it work (e.g. attaching to prototype). With object composition I've no idea how to get there, probably due to lacking experience with FP.
Thanks for the help!
You can use this instead of state inside isOfAge. That way, the this will be deduces when the method isOfAge gets called, it will be bound to whatever object it is called on. Though, you'll have to use a regular function instead of an arrow one for that to work (arrow functions don't have a this):
const person = (props) => {
let state = {
name: props.name,
}
state.isOfAge = function() { // use a regular function
return this.isAdult === true // use this here instead of state
}
return state
}
const adult = (props) => {
return {
isAdult: true,
}
}
const factory = (props) => {
return Object.assign({}, person(props), adult(props))
}
const john = factory({
name: 'John',
})
console.log(john);
console.log(john.isOfAge()); // returns 'true' because 'this' inside 'isOfAge' will be 'john'
Object Composition
All objects made from other objects and language primitives are composite objects.
The act of creating a composite object is known as composition.
...
Concatenation composes objects by extending an existing object with new properties, e.g., Object.assign(destination, a, b), {...a, ...b}.
...                      The Hidden Treasures of Object Composition
So from your pattern and use of a factory function it looks like concatenation? The demo below is a concatenation composition. Note the parenthesis wrapped around the brackets of payment:
const payment = (status) => ({...})
this allows payment to be returned as an object instead of a function. If you have data that's a little more flexible, you'll need less methods. name: string and age: number are the properties I used considering it practical or in your case name: string and adult: boolean.
Demo
const payment = (status) => ({
adult: () => status.age > 17 ? true : false,
account: () => status.adult() ? 'holder' : 'student'
});
const member = (name, age) => {
let status = {
name,
age
};
return Object.assign(status, payment(status));
};
const soze = member('Kaiser Soze', 57);
console.log(soze);
console.log(soze.adult());
console.log(soze.account());
const jr = member('Kaiser Soze Jr.', 13);
console.log(jr);
console.log(jr.adult());
console.log(jr.account());

Lodash isEqual fails because of constructor defined by angular

I am using Lodash _.isEqual to deep-compare a local javascript object with another javascript object retrieved through angular $get.
This code says that the objects are different:
$get({...}, function (data) {
cleanupAngularProps(data);
if (_.isEqual(data, {name: 'Someone'}) {
...
}
});
but chaging it a little as follows it now says that they are equal (as expected):
$get({...}, function (data) {
cleanupAngularProps(data);
if (_.isEqual(JSON.parse(JSON.stringify(data)), {name: 'Someone'}) {
...
}
});
I debugged into the Lodash code and it seems to fail because both objects have different constructors.
How can I solve this without cloning the data?
I know this question is three years old at this point, but I have found what I believe is a more acceptable answer. Simply exclude the prototype comparison between the two objects:
var result = _.isEqual(
_.omit(data, ['__proto__']),
_.omit({name: 'Someone'}, ['__proto__'])
);
I am not sure if this is a good practice but I solved it by resetting the prototype, hence the constructor.
$get({...}, function (data) {
cleanupAngularProps(data);
data.__proto__ = Object.prototype;
if (_.isEqual(data, {name: 'Someone'}) {
...
}
});
If you need deep comparison and ignore constructors, then you can use this code:
const customizer = (a: any, b: any, key: any) => {
if (_.isObject(a) && _.isObject(b)) {
// #ts-ignore
const aProto = a.__proto__.constructor.name
// #ts-ignore
const bProto = b.__proto__.constructor.name
if (aProto != bProto) {
return _.isEqualWith(_.omit(a, ['__proto__']), _.omit(b, ['__proto__']), customizer)
}
}
return undefined
}
_.isEqualWith({foo:1}, {foo:1}, customizer)

Categories

Resources