当我第一次潜入 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'
到达!
如果我们将 writable 和 configure 设置为 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 之类的库来正确处理不可变代码。