Поиск по сайту:

Изменяемый неизменяемый JavaScript


Когда я впервые погрузился в JavaScript и программирование; Я никогда не думал о неизменяемых данных. Я бы сказал, что животное — панда, тогда животное — лев.

var animal = 'panda';
animal = 'lion';

Я был свободен делать со своими данными все, что хотел! Но… все изменилось… я вырос. Люди начали говорить мне: «Вы всегда должны использовать const, если можете». Так я послушно и сделал. Но я не очень понимал, почему.

Зачем использовать неизменяемые данные

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

Допустим, у нас есть сайт электронной коммерции.

// First we import a function called validate address to check if our users entered a valid address
import validateAddress from 'address-validator'

const checkout = (user, cart) => {
 //... checkout code

 var userAddress = user.address
 // Checking if the address is valid
 const validAddress = validateAddress(userAddress);

 // If the address is valid then
 if (validAddress) {
  //... proceed to checkout
 }
}

Допустим, мы получили наш адрес-валидатор, установив пакет npm.

$ npm install address-validator

Все работает, как и ожидалось, но однажды выходит новая версия, в пакет вводится новая строка кода, которая выглядит так:

const validateAddress = (address) => {
 address = '123 My Street, Bring Me Free Goods, USA';
 return true;
}

Теперь переменная userAddress всегда будет равна значению адреса! Вы можете видеть, насколько это проблема.

Эта конкретная проблема может быть решена с помощью неизменности. Но это также может быть решено с помощью надлежащего объема. Попробуйте понять как!

Конечно, вредоносный код — это пограничный случай. Но есть много способов, которыми неизменяемые данные могут помочь вам писать лучший код. Например, очень распространенной ошибкой является случайное изменение свойства объекта.

const userJack = {name: 'Jack Misteli'};
// I want to make a copy of user to do stuff with it
const user2 = userJack
user2.name = 'Bob'

// Because we didn't do a copy:
// userJack.name === 'bob'

Этот тип ошибки может происходить очень часто.

Инструменты неизменности

Самый интуитивно понятный инструмент неизменности — использовать const.

const animal = 'panda';

// This will throw a TypeError!
panda = 'lion';

const отлично подходит. Однако это иногда только создает иллюзию неизменности!

const user = {
 name: 'Jack Misteli',
 address: '233 Paradise Road',
 bankingInfo: 'You wish!'
};

const maliciousAddressValidator = (user) => {
 user.address = 'Malicious Road';
 return true;
};

const validAddress = maliciousAddressValidator(user);
// Now user.address === 'Malicious Road' !!

Существуют разные способы решения этой проблемы, и введение неизменности — один из них.

Сначала мы можем использовать метод Object.freeze.

const user = {
 address: '233 Paradise Road'
};
Object.freeze(user)
// Uing the same dodgy validateUserAddress
const validAddress = maliciousAddressValidator(user);
// Now user.address === '233 Paradise Road' !!

Одна проблема с Object.freeze заключается в том, что вы не влияете на подсвойства. Чтобы получить доступ ко всем подсвойствам, вы можете написать что-то вроде:

const superFreeze = (obj) => {
 Object.values(obj).forEach(val =>{
  if (typeof val === 'object')
    superFreeze(val)
  })
  Object.freeze(obj)
}

Другое решение — использовать прокси, если вам нужна гибкость.

Использование дескрипторов свойств

Вы увидите, что на многих сайтах можно изменять дескрипторы свойств, чтобы создавать неизменяемые свойства. И это правда, но вы должны убедиться, что для configurable и writeable установлено значение false.

// Setting up a normal getter
const user = {
 get address(){ return '233 Paradise Road' },
};
console.log(Object.getOwnPropertyDescriptor(user, 'address'))
//{
//  get: [Function: get address],
//  set: undefined,
//  enumerable: true,
//  configurable: true
//}

const validAddress = maliciousAddressValidator(user);

// It looks like our data is immutable!
// user.address ===  '233 Paradise Road'

Но данные по-прежнему изменяемы, просто изменить их сложнее:

const maliciousAddressValidator = (user) => {
 // We don't reassign the value of address directly
 // We reconfigure the address property
  Object.defineProperty(user, "address", {
    get: function() {
     return 'Malicious Road';
   },
 });
};
const validAddress = maliciousAddressValidator(user);
// user.address === 'Malicious Road'

Арр!

Если мы установим для записи и настройки значение false, мы получим:

const user = {};
Object.defineProperty(user, "address", {value: 'Paradise Road', writeable: false, configurable: false});


const isValid = maliciousAddressValidator(user)
// This will throw:
// TypeError: Cannot redefine property: address

Неизменяемые массивы

У массивов та же проблема, что и у объектов.

const arr = ['a','b', 'c']
arr[0] = 10
// arr === [ 10, 'b', 'c' ]

Ну, вы можете использовать те же инструменты, которые мы использовали выше.

Object.freeze(arr)
arr[0] = 10
// arr[0] === 'a'

const zoo = []
Object.defineProperty(zoo, 0, {value: 'panda', writeable: false, configurable: false});
Object.defineProperty(zoo, 1, {value: 'lion', writeable: false, configurable: false});
// zoo === ['panda', 'lion']

zoo[0] = 'alligator'
// The value of zoo[0] hasn't changed
// zoo[0] === 'panda

Другие методы

Есть и другие приемы, чтобы сохранить ваши данные в безопасности. Я настоятельно рекомендую ознакомиться с нашей статьей о прокси-ловушках, чтобы узнать о других способах сделать ваши данные неизменяемыми. Мы только поцарапали поверхность здесь.

В этом посте мы рассмотрели варианты введения неизменности без изменения структуры и стиля нашего кода. В будущих постах мы рассмотрим библиотеки, такие как Immutable.js, Immer или Ramda, для исправления неизменяемого кода.