Comics: "Is task ready?" Driven Development


Stworzenie kopii obiekt jest proste:
const originalObj = {
title: 'Foo',
value: 100,
tags: ['a', 'b', 'c']
};
const copyObj = {
...originalObj
};
copyObj.title = 'Bar';
copyObj.value = 50;
copyObj.tags.push('d');
console.log('originalObj: ', originalObj); // { title: 'Foo', value: 100, tags: ['a', 'b', 'c'] }
console.log('copyObj: ', copyObj); // { title: 'Bar', value: 50, tags: ['a', 'b', 'c', 'd'] }
Za pomocą operatora spread: ... tworzymy kopię obiektu. Następnie zmieniamy w skopiowanym obiekcie jego parametry. Oryginał się nie zmieniał. I tak miało być :)
Niestety to nie zadziała dla obiektu zagnieżdżonego.
Najpierw przykład:
const originalObj = {
name: 'Janusz Kowalski',
age: 57,
address: {
city: 'New York',
street: '99 East Golf Street'
}
};
const copyObj = {
...originalObj
};
copyObj.name = 'Grażyna Kowalska';
copyObj.address.city = 'Chicago';
copyObj.address.street = '3109 15th St';
console.log('originalObj: ', originalObj);
/*
originalObj:
{
name: 'Janusz Kowalski',
age: 57,
address: {
city: 'Chicago',
street: '3109 15th St'
}
}
*/
console.log('copyObj: ', copyObj);
/*
copyObj:
{
name: 'Grażyna Kowalska',
age: 57,
address: {
city: 'Chicago',
street: '3109 15th St'
}
}
*/
Kopia została stworzona, ale nie dla zagnieżdżonej części obiektu jak: address. Po zmianie danych w kopii zmieniły się też dane w oryginale!
Jak sobie z tym poradzić?
const originalObj = {
name: 'Janusz Kowalski',
age: 57,
address: {
city: 'New York',
street: '99 East Golf Street'
}
};
const copyObj = JSON.parse(JSON.stringify(originalObj))
copyObj.name = 'Grażyna Kowalska';
copyObj.address.city = 'Chicago';
copyObj.address.street = '3109 15th St';
console.log('originalObj: ', originalObj);
/*
originalObj:
{
...,
address: {
city: 'New York',
street: '99 East Golf Street'
}
}
*/
console.log('copyObj: ', copyObj);
/*
copyObj:
{
...,
"address": {
"city": "Chicago",
"street": "3109 15th St"
}
}
*/
Kombinacja: JSON.parse i JSON.stringify rozwiązuje ten problem.
To rozwiązanie nie zadziała jeśli obiekt zawiera funkcje. Funkcje nie zostaną skopiowane.
Czysta funkcja (ang.: pure function) jest to termin z programowania funkcjonalnego, który mówi, że funkcja jest "czysta" jeśli są spełnione następujące warunki:
Przyjrzyjmy się obu warunkom bliżej.
Te same dane przekazane do funkcji zawsze dają taki sam wynik.
Czyli: dla taki samych parametrów funkcja zawsze powinna zwrócić ten sam wynik:
const multiply = (num, by) => num * by;
console.log(multiply(2, 3)); // 6
console.log(multiply(2, 4)); // 8
console.log(multiply(2, 3)); // 6
Podajemy dwa razy te same parametry: 2 i 3 i dostajemy ten sam wynik: 6. To jest czysta funkcja.
Inny przykład:
console.log(Math.random()); // 0.2755161550452777
console.log(Math.random()); // 0.5888948222087572
console.log(Math.random()); // 0.2976254575123847
To nie jest czysta funkcja! Dla tych samych danych wejściowych dostajemy różne wyniki. Pierwszy warunek nie został spełniony w tym przypadku.
Funkcja nie tworzy efektów ubocznych.
To znaczy, że czysta funkcja nie zmienia stanu danych do niej przekazanych.
const setProperty = (property, value, object) => {
object[property] = value;
}
const obj = { name: 'Object' };
setProperty('size', 100, obj); // { name: 'Object', size: 100 }
console.log(obj);
Funkcja setProperty nie można nazwać czystą, ponieważ tworzy efekt uboczny. Zmienia stan obiektu, który jest przekazany do funkcji.
Poprawiona wersja:
const setProperty = (property, value, object) => {
return ({
...object,
[property]: value
});
}
const obj = { name: 'Object' };
const updatedObject = setProperty('size', 100, obj);
console.log(obj); // { name: 'Object' }
console.log(updatedObject); // { name: 'Object', size: 100 }
Czysta funkcja nie polega na stanie z zewnątrz:
const counterValue = 10;
const updateCounter = (value) => value + counterValue;
console.log(updateCounter(5)); // 15
console.log(updateCounter(10)); // 20
console.log(updateCounter(10)); // 20
Tutaj niby wszystko jest ok. Daje takie same wyniki na przykład dla parametru 10. Problem jest taki, że działamy na zmiennej z zewnątrz.
A co jeśli zmienimy nieco kod:
let counterValue = 10;
const updateCounter = (value) => value + counterValue;
console.log(updateCounter(5)); // 15
console.log(updateCounter(10)); // 20
counterValue += 15;
console.log(updateCounter(10)); // 35
Ups... mamy już inne wyniki...
Załóżmy, że mam taki przykład:
const original = [1, 2, 3, 4, 5];
const copy = original;
copy[0] = 7;
console.log('original: ', original); // [7, 2, 3, 4, 5]
console.log('copy: ', copy); // [7, 2, 3, 4, 5]
Stworzyliśmy kopię tablicy, a następnie zmieniliśmy jej pierwszy element. Niestety zmienił się także oryginał. Jest tak dlatego, że w ten sposób skopiowaliśmy także referencje z macierzystej tablicy.
Jak temu zapobiec?
const original = [1, 2, 3, 4, 5];
const copyArray = [...original];
copyArray[0] = 7;
console.log('original: ', original); // [1, 2, 3, 4, 5]
console.log('copyArray: ', copyArray); // [7, 2, 3, 4, 5]
const original = [1, 2, 3, 4, 5];
const copyArray = original.slice();
copyArray[0] = 7;
console.log('original: ', original); // [1, 2, 3, 4, 5]
console.log('copyArray: ', copyArray); // [7, 2, 3, 4, 5]
const original = [1, 2, 3, 4, 5];
const copyArray = [].concat(original);
copyArray[0] = 7;
console.log('original: ', original); // [1, 2, 3, 4, 5]
console.log('copyArray: ', copyArray); // [7, 2, 3, 4, 5]
Jeśli zależy nam na stworzeniu tablicy, której zawartości nie chcemy zmieniać w ogóle, możemy użyć: Object.freeze. Pisałem już o niej w tym wpisie.
Przykład:
const array = Object.freeze([1, 2, 3]);
array[0] = 7; // Błąd!
array.push(12); // Błąd!
array.pop(); // Błąd!
Jak widać każda próba modyfikacji tablicy, powoduje błąd w kodzie.
