javascript - La clonación de objetos en ES6 usando el operador spread también está modificando la entrada
Tengo una interface bastante profunda declarada que se ve algo así:
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
}
La mayoría de los miembros de esta interfaz son en sí mismos interfaces, con la arquitectura general siguiendo este patrón de usar enums, cadenas, arreglos y más interfaces. Todo el código está escrito en TypeScript, transpilado a JS y subido a AWS como JS. (Node 8.10 se está ejecutando en AWS)
En un punto del código, necesito hacer una copia profunda de una instanciación de Job que se pasó como un parámetro de función:
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 };
};
La definición del 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);
}
}
El problema es que después de la llamada a JobUtils.FilterJobProperties, payload.Job también ha sido mutado de una manera indeseable e inesperada.
Aquí está la implementación de 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;
};
(Estoy usando la biblioteca lodash aquí)
La línea de mercado 'LINE TWO' también está mutando el parámetro de función from, aunque en 'LINE ONE' estoy haciendo lo que creo es una clonación profunda de from.
Sé que este es el caso porque si cambio 'LINE ONE' a:
// super hard core deep cloning
let job: Types.Job = JSON.parse(JSON.stringify(from));
... todo funciona como se espera. from no se muta, el JobUpdatedMessage resultante es como se esperaba, y el parámetro event de StartPipeline no tiene un montón de propiedades eliminadas de event.Job.
Luché durante horas con esto, incluyendo volver a aprender todo lo que creía saber sobre la clonación de objetos en Es6 usando el operador spread.
¿Por qué 'LINE ONE' también estaba mutando la entrada?
Respuesta #1
El operador spread realiza una clonación superficial igual que Object.assign()
La clonación superficial (excluyendo el prototipo) o la fusión de objetos ahora es posible usando una sintaxis más corta que Object.assign().
Operador de propagación
Un ejemplo para entender el operador spread y la clonación superficial.
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)Usando el operador spread el objeto es clonado superficialmente por lo que todas las propiedades de primer nivel se convertirán en una copia mientras que todas las propiedades de nivel más profundo seguirán siendo las referencias.
como puedes ver en el ejemplo c la propiedad no afecta al objeto original ya que es de primer nivel, por otro lado los cambios en la propiedad b afectan a las propiedades padre porque está en un nivel profundo y sigue siendo una referencia.
