Чтобы начать копаться в неизменяемых данных, мы должны знать, зачем нам нужно знать, что такое неизменяемые данные.

Неизменяемые данные часто применяются в проектах React, одной из причин этого является функциональное программирование.

Следование одной из лучших практик функционального программирования заключается в том, что одна функция не должна иметь побочных эффектов (чистые функции), а это означает, что функция не должна изменять значение, выходящее за пределы области действия функции, и всегда должна возвращать новые данные (например, объект ).

Кроме того, чистые функции должны возвращать один и тот же результат независимо от того, сколько раз вы их запускаете.

Существуют библиотеки, такие как ImmutableJs или Immer, которые помогут нам сделать объект неизменяемым.

В этой статье основное внимание будет уделено неизменяемым данным только с точки зрения нативного JavaScript. Изменяемый объект — это объект, который может быть изменен после инициализации объекта, в то время как неизменяемый объект не является изменяемым, и разработчику не разрешено изменять какое-либо значение объекта после его инициализации.

Изменяемые и неизменяемые данные

Давайте посмотрим на пример ниже:

let mutable = {
    a: 1
};
let test = mutable;
mutable.a = 2;
console.log(test); // { a: 2 }

ПРИМЕЧАНИЕ. Вы можете подумать, а как насчет объявления этих переменных с помощью ключевого слова const, которое должно решить проблему. Короткий ответ - нет.

В JavaScript присваивание переменной будет просто ссылаться на объект, и именно поэтому, когда вы изменяете одну из переменных, другая будет изменена вместе. При использовании JavaScript, объект, массив, функция, класс, набор, map — это изменяемые данные. (Но не строка и целое число).

Посмотрите следующий пример неизменяемого типа данных:

let testText = 'hello world';
let anotherTestText = testText;
testText = testText.slice(0, 5);
console.log(anotherTestText); // 'hello world'
console.log(testText); // 'hello'

Глубокое копирование и поверхностное копирование

Чтобы создать новый объект данных, мы должны сначала скопировать исходный объект. Есть несколько способов добиться этого в JavaScript.

  1. Object.assign
const x = { a: 1 };

const y = Object.assign({}, x);
x.a = 11;
console.log(y); // { a: 1 }

На этот раз новый объект не изменяется в соответствии с исходным объектом. Пока не горячитесь, см. следующий пример:

const x = { a: 1, b: { c: 2 } };
const y = Object.assign({}, x);

x.b.c = 22;

console.log(y); // { a: 1, b: { c: 22}}

Данные снова неожиданно изменяются. Почему это происходит? Причина этого в том, что Object.assign предоставляет поверхностную копию, поэтому поддерживает только один уровень ключей и значений. Пока в объекте существует какой-либо объект или массив, эти переменные по-прежнему будут использовать тот же адрес памяти, что и изменяемый объект.

2. Object.freeze

const x = { a: 1, b: { c: 2 } };
const y = Object.freeze(x);
x.a = 11;
console.log(y);

x.b.c = 22;

console.log(y); // { a: 1, b: { c: 22}}

Действительно интересно, когда Object.freeze не замораживает объект, не так ли?

3. Уничтожение объекта

const x = { a: 1, b: { c: 2 } };
const y = { ...x };
x.a = 11;
console.log(y);

x.b.c = 22;

console.log(y);

То же самое произойдет и с массивом.

В нативных функциях JavaScript у нас нет ничего, что могло бы обеспечить глубокую копию изменяемых данных.

Как сделать глубокое клонирование

Самый простой способ:

var x = {
  a: 1,
  b: 2,
  c: {
    d: 3
  }
};
const y = JSON.parse(JSON.stringify(x));
x.c.d = 100;
console.log(y); // { a:1, b:2 , c: { d: 3 }}

Приложение в проекте React

Если мы рассмотрим некоторые сценарии, которые могут быть в реальном проекте React, мы, вероятно, могли бы найти код, похожий на следующий код:

// Following code and output is based on React 18 without StrictMode turned on
import { useEffect, useState } from "react";
const processParam = (var1) => { var1.c.d = 100; return var1; };
const SubComponent = ({ object1 }) => <div>{JSON.stringify(object1)}</div>;
export default function App() {
  console.log('<App /> rendered');
  const [param] = useState({ a: 1, b: 2, c: { d: 4 }});
return (
    <div className="App">
      <p>{JSON.stringify(param)}</p>
      <SubComponent object1={param} />      
      <p>{JSON.stringify(processParam(param))}</p>
    </div>
  );
}
// Output:
{"a":1,"b":2,"c":{"d":4}} // from {JSON.stringify(param)}
{"a":1,"b":2,"c":{"d":100}} // from subcomponent
{"a":1,"b":2,"c":{"d":100}} // JSON.stringify(processParam(param))

Это очень плохо, не так ли? Даже если вы выполняете назначение переменной внутри функции processParam, вы все равно будете сталкиваться с этой проблемой, поскольку JavaScript выполняет только поверхностное копирование. Обработка данных без механизма Immutable может привести к большому количеству ненужных повторных рендерингов и непредвиденных проблем.