发布于 2026-01-06 8 阅读
0

JavaScript 中的不可变性 DEV 全球展示挑战赛,由 Mux 呈现:展示你的项目!

JavaScript 中的不可变性

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

本文最初发表于 Medium,链接在此。

简而言之, ECMAScript 提供了一些工具来保持数据不可变性。但这些工具在处理嵌套结构时存在局限性。immutadot是一个专门处理嵌套不可变结构的库。不妨试用一下


根据维基百科的定义,不可变对象(或称不可更改对象)是指创建后其状态无法修改的对象。这条规则很简单:如果要修改对象的某个属性,必须修改其副本。稍后我们将看到它为我们的开发带来了哪些改进和强大的功能。

ECMAScript

EcmaScript 提供了一系列实用工具来确保数据的不可变性。数组和对象 API 包含创建副本的方法,甚至可以阻止实例被更新。最近,EcmaScript 还引入了一种新的语法来创建对象和数组的副本。

对象.赋值

我们想在对象中添加一个 name 属性。

const lutraLutra = {
  commonNames: ['eurasian otter'],
}
Enter fullscreen mode Exit fullscreen mode

我们可以用一个小技巧来实现Object.assign。它的基本原理是将一个对象的所有属性复制到另一个对象中,从而改变目标对象。我们使用一个小技巧,将一个空对象作为第一个参数传递,这样就能创建一个新的引用。

const newLutraLutra = Object.assign(
  {}, 
  lutraLutra,
  { name: 'Lutra lutra' },
)
Enter fullscreen mode Exit fullscreen mode

现在我们得到了一个新对象,它包含一个新name属性,而另一个commonNames属性保持不变。使用此方法,您可以同时创建/覆盖多个属性。

数组.连接

现在我们用数组来实现。我们想以不可变的方式向数组中添加两个新元素。

const commonNames = ['eurasian otter']
Enter fullscreen mode Exit fullscreen mode

与此不同Array.pushArray.concat它不会改变我们的数组。相反,它会返回一个新数组。

const newCommonNames = commonNames.concat(
  'european otter',
  'common otter'
)
Enter fullscreen mode Exit fullscreen mode

这种方法很灵活,可以接受任意数量的元素,元素可以是数值,也可以是数组。

对象冻结

Object.freeze不太为人所知。它可以让对象变为不可变!它可以防止所有类型的变更(创建、修改、删除),这些变更都是由使用 setter 方法引起的。

let lutraLutra = {
  commonNames: ['eurasian otter', 'european otter', 'common otter'],
  name: 'Lutra lutra'
}
Enter fullscreen mode Exit fullscreen mode

name我们将在对象冻结后尝试删除该属性。

lutraLutra = Object.freeze(lutraLutra)

delete lutraLutra.name
Enter fullscreen mode Exit fullscreen mode

由于作为参数传递的对象已被设置为不可变,因此无需重新分配内存Object.freeze。此方法有两种可用模式:

  • 一种不应用突变的非严格模式
  • 严格模式,TypeError如果尝试应用变更,则会抛出异常。

注意,它不是递归的。我们的属性commonNames不是不可变的。

价差运营商

扩展运算符语法在 ES2015 中引入用于数组,在 ES2018 中引入用于对象。它将给定对象的所有属性复制到一个新的对象字面量中。

const newLutraLutra = {
  ...lutraLutra,
  name: 'Lutra lutra',
}
Enter fullscreen mode Exit fullscreen mode

对于数组,它会将一个数组中的所有值复制到一个新数组中。

const newCommonNames = [
  ...commonNames,
  'common otter',
]
Enter fullscreen mode Exit fullscreen mode

它能很好地替代 ` assign&&` concat,而且易于阅读,并在数组和对象之间建立起一种约定。可以在同一个字面量中展开多个数组和对象。

为什么要使用不可变性?

你已经了解了如何使用 JavaScript 使对象和数组不可变,但我们还没有解释为什么在当今时代使用不可变性如此必要。作为开发者,我们一直在寻找编写更易维护、更易读代码的方法。一些编程范式,例如函数式编程,正是专注于此。

函数式编程的目标是让我们减少思考,编写更具描述性的代码。

——亚历山大·孔多夫

它采用声明式编程方法,这意味着你关注的是描述程序必须完成什么,而不是如何完成。这让你的代码更易于理解,方便后续开发者阅读。函数式编程还引入了其他有助于实现这一目标的概念,例如不可变性。

有哪些好处?

你觉得“不可变性”这个词听起来像个炒作的词汇吗?不可变性为我们日常遇到的编程问题提供了许多解决方案:

  • 避免副作用
  • 数据变更检测变得简单(浅层比较)
  • 明确的数据变更
  • 记忆化
  • 内存优化
  • 更好的渲染性能
  • 简易测试

“与 JavaScript 领域的大多数趋势不同,数据不可变性注定会长期存在,这是有充分理由的:首先,因为它不是一种趋势,而是一种编码方式(以及代码思维方式),它能提升代码的清晰度、易用性和数据流的可理解性,并降低代码出错的可能性。”

——里卡多·马加良斯

近几年,我们面临的最大挑战之一是如何高效地检测数据变化,从而判断是否需要渲染界面。检测原始值的变化很容易,但对于对象和数组来说,情况就完全不同了。虽然可以按值比较,但这需要实现递归算法,并且还要处理循环引用等潜在问题。另一种方法是使用严格相等运算符比较对象引用===。这种方法更有效,而且不会陷入无限循环。这也是现代框架强制执行这一概念的原因。

以现代框架/库为特色

现代前端框架和库都基于一个能够显著提升性能的概念,那就是众所周知的虚拟 DOM。这项技术的诞生源于一个简单的道理:DOM 操作开销巨大。

正如前文所述,前端框架和库选择使用不可变性是为了提升性能。如今,我们的应用程序需要处理越来越多的数据,因此也需要处理更多的标记。这意味着浏览器需要处理的计算量比十年前要大得多。DOM 操作开销巨大,现代框架往往会减少 DOM 更新的次数。

为什么我们需要实用工具库?

正如我们之前看到的,ECMAScript 中处理不可变性的方式虽然借助语法糖变得简单,但在嵌套结构方面却相当有限。随着诸如 之类的库的出现redux,嵌套结构变得越来越流行。

const animals = {
  weasels: {
    lutraLutra: {
      commonNames: ['eurasian otter'],
    },
  },
}

const newAnimals = {
  ...animals,
  weasels: {
    ...animals.weasels,
    lutraLutra: {
      ...animals.weasels.otter,
      name: 'Lutra lutra',
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

正如你所看到的,编写起来越来越繁琐,阅读起来也越来越困难。像覆盖数组索引这样简单的用例也不容易实现。

const lutraLutra = {
  name: 'Lutra lutra',
  commonNames: ['eurasian otter', 'european', 'common otter'],
}

const newCommonNames = [...lutraLutra.commonNames]
newCommonNames[1] = 'european otter'

const newLutraLutra = { 
  ...lutraLutra,
  commonNames: newCommonNames,
}
Enter fullscreen mode Exit fullscreen mode

这些理由足以让我们开始寻找一些工具,帮助我们专注于真正重要的东西——代码的含义。这就是我们创建immutadot 的原因,它旨在帮助我们保持 JavaScript 代码库的可读性和可维护性。不妨试用一下

文章来源:https://dev.to/zenika/immutability-in-javascript-2b6h