可变 不可变 JavaScript

当我第一次潜入 JavaScript 和编程时;我从未真正考虑过不可变数据。我会说动物是熊猫,然后动物是狮子。

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

我可以自由地用我的数据做任何我想做的事!但是……事情变了……我长大了。人们开始告诉我:“如果可以,你应该总是使用 const ”。于是我乖乖的做了。但我真的不明白为什么。

为什么要使用不可变数据

因为有时代码会更改您不想更改的内容。我知道这是一个非常蹩脚的答案,让我举个例子告诉你。

假设我们有一个电子商务网站。

模块:checkout.js
// 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

一切都按预期工作,但有一天发布了新版本,包中引入了一行新代码,如下所示:

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

现在变量userAddress将始终等于地址的值!你可以看到这是一个问题。

这个特定的问题可以通过使用不变性来解决。但它也可以通过适当的范围界定来解决。试着弄清楚如何!

当然,恶意代码是一种边缘情况。但是,不可变数据可以通过多种方式帮助您编写更好的代码。例如,一个非常常见的错误是不小心改变了对象的属性。

模块:意外更改.js
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是很棒的。然而,它有时只会给人一种不变的错觉!

模块:example-checkout.js
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)
}

如果您需要灵活性,另一种解决方案是使用代理。

使用属性描述符

在许多站点中,您会看到可以修改属性描述符来创建不可变的属性。这是真的,但你必须确保configurablewriteable设置为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 之类的库来正确处理不可变代码。

觉得文章有用?

点个广告表达一下你的爱意吧 !😁