学习使用代理对象滚动JavaScript不变性功能

81 阅读3分钟

image.png

虽然JavaScript允许我们对对象进行突变,但我们可能会选择不允许自己(和其他程序员)这样做。在今天的JavaScript世界中,最好的例子之一是当我们在React应用程序中设置状态时。如果我们突变我们当前的状态,而不是突变我们当前状态的新副本,我们会遇到难以诊断的问题。

在这篇文章中,我们将推出我们自己的不可变代理函数,以防止对象突变!

什么是对象突变?

作为一个快速的复习,对象突变是指我们改变一个对象或数组的属性。这与重新分配非常不同,在重新分配中,我们完全指向一个不同的对象引用。这里有几个突变与重赋的例子。

// Mutation
const person = { name: 'Bo' };
person.name = 'Jack';

// Reassignment
let pet = { name: 'Daffodil', type: 'dog' };
pet = { name: 'Whiskers', type: 'cat' };

我们必须牢记这也适用于数组。

// Mutation
const people = ['Jack', 'Jill', 'Bob', 'Jane'];
people[1] = 'Beverly';

// Reassignment
let pets = ['Daffodil', 'Whiskers', 'Ladybird'];
pets = ['Mousse', 'Biscuit'];

一个关于对象突变的意外后果的例子

现在我们已经知道了什么是突变,那么突变是如何产生意外后果的呢?让我们看一下下面的例子。

const person = { name: 'Bo' };
const otherPerson = person;
otherPerson.name = 'Finn';

console.log(person);
// { name: "Finn" }

呀,这就对了!personotherPerson 都在引用同一个对象,所以如果我们在otherPerson 上对name 进行突变,当我们访问person 时,这种变化就会反映出来。

与其让我们自己(以及我们项目中的其他开发者)像这样突变一个对象,还不如抛出一个错误呢?这就是我们的不可变代理解决方案的用武之地。

我们的不可变代理解决方案

JavaScriptProxy 对象是我们可以使用的一个方便的元编程。它允许我们用自定义的功能来包装一个对象,比如该对象的getters和setters。

对于我们的不可变代理,让我们创建一个函数,接收一个对象并为该对象返回一个新的代理。当我们试图get 该对象的一个属性时,我们检查该属性是否是一个对象本身。如果是,那么,以递归的方式,我们返回包裹在一个不可变的代理中的那个属性。否则,我们只是返回该属性。

当我们试图set 代理对象的值时,简单地抛出一个错误,让用户知道他们不能set 这个对象上的一个属性。

这就是我们的不可变代理函数的运作。

const person = {
  name: 'Bo',
  animals: [{ type: 'dog', name: 'Daffodil' }],
};

const immutable = (obj) =>
  new Proxy(obj, {
    get(target, prop) {
      return typeof target[prop] === 'object'
        ? immutable(target[prop])
        : target[prop];
    },
    set() {
      throw new Error('Immutable!');
    },
  });

const immutablePerson = immutable(person);

const immutableDog = immutablePerson.animals[0];

immutableDog.type = 'cat';
// Error: Immutable!

我们有了:我们无法突变一个不可变的对象上的属性!

我应该在生产中使用这个吗?

不,可能不用。这种练习在学术上是非常棒的,但现在有各种很棒的、强大的、经过测试的解决方案,可以做同样的事情(例如ImmutableJS和ImmerJS)。如果你想在你的应用程序中加入不可变的数据结构,我建议你去看看这些很棒的库。