Изменяемый неизменяемый 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, для исправления неизменяемого кода.