JavaScript 中使用集合时应避免的 5 种反模式
在Medium上找到我
在 JavaScript 中使用集合可能会成为一项令人震惊的任务,尤其是当函数块中有很多事情要做时。
你有没有想过,为什么有些项目的代码看起来比其他项目好看得多?或者,当一个看似很难的项目最终变得如此之小,你会不禁思考,他们是如何做到既简单又健壮的?
当一个项目易于阅读同时保持良好的性能时,您可以确保代码中可能应用了相当好的实践。
当代码写得一团糟时,情况很容易相反。这时,很容易出现这样的情况:修改一小段代码最终会给你的应用程序带来灾难性的问题——换句话说,抛出一个错误,导致网页崩溃,无法继续运行。在迭代集合时,看到糟糕的代码运行起来可能会让人感到恐惧。
执行更好的实践是为了避免自己采取短期指导,从而有助于确保代码的可靠性。这意味着,从长远来看,你的代码是否易于维护取决于你自己。
本文将介绍在 JavaScript 中使用集合时应避免的 5 种反模式
本文中的许多代码示例都体现了一种称为函数式编程的编程范式。正如Eric Elliot所解释的那样,函数式编程“是通过组合纯函数来构建软件的过程,避免共享状态、可变数据和副作用”。在本文中,我们将经常提及副作用和变异。
以下是使用集合时应避免的 JavaScript 反模式:
1. 过早地将函数作为直接参数传递
我们将要讨论的第一个反模式是过早地将函数作为直接参数传递给循环集合的数组方法。
这是一个简单的例子:
function add(nums, callback) {
const result = nums[0] + nums[1]
console.log(result)
if (callback) {
callback(result)
}
}
const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]
numbers.forEach(add)
那么为什么这是一种反模式?
大多数开发者,尤其是那些更热衷于函数式编程的开发者,可能会发现它干净、简洁,性能极佳。我的意思是,看看它就知道了。你不用这样做:
const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]
numbers.forEach(function(nums, callback) {
const result = nums[0] + nums[1]
console.log(result)
if (callback) {
callback(result)
}
})
看起来,直接输入函数名然后就完事了似乎更好:
const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]
numbers.forEach(add)
在完美的世界中,这将是使用 JavaScript 中的所有功能而毫不费力的完美解决方案。
但事实证明,过早地以这种方式传递处理程序可能会导致意外错误。例如,让我们回顾一下之前的示例:
function add(nums, callback) {
const result = nums[0] + nums[1]
console.log(result)
if (callback) {
callback(result)
}
}
const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]
numbers.forEach(add)
我们的add
函数需要一个数组,其中第一个和第二个索引都是数字,并将它们相加,然后检查是否存在回调函数,如果存在则调用它。这里的问题是,callback
最终可能会被当作一个 来调用number
,从而导致错误:
2. 依赖迭代器函数的顺序,.map
例如.filter
JavaScript 的基本函数会按照元素在数组中的当前顺序来处理集合中的元素。但是,你的代码不应该依赖于此。
首先,迭代的顺序在任何语言或库中都不可能 100% 稳定。将每个迭代函数视为在多个进程中并发运行是一个好习惯。
我见过做类似这样的事情的代码:
let count = 0
frogs.forEach((frog) => {
if (count === frogs.length - 1) {
window.alert(
`You have reached the last frog. There a total of ${count} frogs`,
)
}
count++
})
在大多数情况下,这完全没问题,但仔细观察就会发现,这并非最安全的做法,因为全局范围内的任何内容都可能更新count
。如果发生这种情况,并count
最终在代码中的某个地方意外地减少了 ,那么window.alert
就永远无法运行了!
在异步操作中,情况可能会变得更糟:
function someAsyncFunc(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, timeout)
})
}
const promises = [someAsyncFunc, someAsyncFunc, someAsyncFunc, someAsyncFunc]
let count = 0
promises.forEach((promise) => {
count++
promise(count).then(() => {
console.log(count)
})
})
结果:
那些对 JavaScript 比较有经验的人可能知道为什么控制台会输出四个数字而不是。关键在于,为了避免并发,最好使用大多数函数在迭代集合时接收的第二个参数(通常称为 current ):4
1, 2, 3, 4
index
promises.forEach((promise, index) => {
promise(index).then(() => {
console.log(index)
})
})
结果:
3.过早优化
当你想要优化代码时,通常需要考虑的是在可读性还是速度之间做出选择。有时,你可能会倾向于将更多精力放在优化应用速度上,而不是提高代码的可读性。毕竟,网站速度至关重要,这是一个公认的事实。但这实际上是一种糟糕的做法。
首先,JavaScript 中的集合通常比你想象的要小,而且处理每个操作所需的时间也比你想象的要快。这里有一个好规则:除非你知道某个代码会很慢,否则不要试图让它变得更快。这被称为过早优化,换句话说,就是试图优化速度可能已经达到最优的代码。
正如唐纳德·克努斯 (Donald Knuth)所说,“真正的问题是程序员在错误的地方和错误的时间花了太多时间担心效率;过早的优化是编程中所有罪恶的根源(或至少是大部分罪恶的根源)。”
在很多情况下,应用一些更好的速度会更容易,但代码最终会变得有点慢,而不是在混乱中强调维护快速工作的代码。
我建议优先考虑可读性,然后再进行测量。如果您使用性能分析器并报告应用程序存在瓶颈,请只优化该部分,因为您现在知道它实际上是一个运行缓慢的代码,而不是试图优化您认为可能运行缓慢的代码。
4. 依赖国家
状态是编程中一个非常重要的概念,因为它使我们能够构建强大的应用程序,但如果我们没有足够注意自己,它也可能破坏我们的应用程序。
以下是处理集合中的状态时的反模式示例:
let toadsCount = 0
frogs.forEach((frog) => {
if (frog.skin === 'dry') {
toadsCount++
}
})
这是一个副作用的例子,一定要注意,因为它可能会导致如下问题:
- 产生意想不到的副作用(真的很危险!)
- 增加内存使用量
- 降低应用的性能
- 使你的代码更难阅读/理解
- 使测试代码变得更加困难
那么,有什么更好的方法可以写出这样的代码而不会产生副作用呢?或者,我们该如何用更好的做法重写它?
当处理集合时,我们需要在操作期间处理状态,请记住我们可以利用某些方法为您提供某些东西(如对象)的全新引用。
一个例子是使用该.reduce
方法:
const toadsCount = frogs.reduce((accumulator, frog) => {
if (newFrog.skin === 'dry') {
accumulator++
}
return accumulator
}, 0)
所以这里发生的事情是,我们与其块内部的某些状态进行交互,同时我们还利用了第二个参数,.reduce
该参数可以在初始化时重新创建值。这比上一个代码片段使用了一种更好的方法,因为我们没有在作用域之外改变任何东西。这使得我们成为toadsCount
一个使用不可变集合并避免副作用的示例。
5. 可变参数
变异是指改变某种事物的形式或性质。这是一个在 JavaScript 中需要特别注意的重要概念,尤其是在函数式编程的背景下。可变的事物可以被改变,而不可变的事物则不能(或不应该)被改变。
以下是一个例子:
const frogs = [
{ name: 'tony', isToad: false },
{ name: 'bobby', isToad: true },
{ name: 'lisa', isToad: false },
{ name: 'sally', isToad: true },
]
const toToads = frogs.map((frog) => {
if (!frog.isToad) {
frog.isToad = true
}
return frog
})
我们期望的值toToads
返回一个新的数组,通过将其属性frogs
翻转为,将其全部转换为蟾蜍。isToad
true
但这就是它变得有点令人不寒而栗的地方:当我们frog
通过这样做来改变某些对象时:frog.isToad = true
我们也无意中在数组内部改变了它们frogs
!
我们可以看到frogs
现在都是蟾蜍,因为它发生了变异:
这是因为JavaScript 中的对象都是通过引用传递的!如果我们在代码中的 10 个不同地方分配同一个对象会怎么样?
例如,如果我们在整个代码中将此引用分配给 10 个不同的变量,然后在代码稍后的某个时间点变异变量 7,则内存中保存对同一指针的引用的所有其他变量也将发生变化:
const bobby = {
name: 'bobby',
age: 15,
gender: 'male',
}
function stepOneYearIntoFuture(person) {
person.age++
return person
}
const doppleGanger = bobby
const doppleGanger2 = bobby
const doppleGanger3 = bobby
const doppleGanger4 = bobby
const doppleGanger5 = bobby
const doppleGanger6 = bobby
const doppleGanger7 = bobby
const doppleGanger8 = bobby
const doppleGanger9 = bobby
const doppleGanger10 = bobby
stepOneYearIntoFuture(doppleGanger7)
console.log(doppleGanger)
console.log(doppleGanger2)
console.log(doppleGanger4)
console.log(doppleGanger7)
console.log(doppleGanger10)
doppleGanger5.age = 3
console.log(doppleGanger)
console.log(doppleGanger2)
console.log(doppleGanger4)
console.log(doppleGanger7)
console.log(doppleGanger10)
结果:
我们可以做的是,每次我们想要改变它们时就创建新的引用:
const doppleGanger = { ...bobby }
const doppleGanger2 = { ...bobby }
const doppleGanger3 = { ...bobby }
const doppleGanger4 = { ...bobby }
const doppleGanger5 = { ...bobby }
const doppleGanger6 = { ...bobby }
const doppleGanger7 = { ...bobby }
const doppleGanger8 = { ...bobby }
const doppleGanger9 = { ...bobby }
const doppleGanger10 = { ...bobby }
结果:
结论
这篇文章到此结束!我发现你觉得这篇文章很有价值,以后期待更多!
在Medium上找到我
文章来源:https://dev.to/jsmanifest/5-anti-patterns-to-avoid-when-working-with-collections-in-javascript-2fe3