从 for() 移动到 map() - 无法理解它



想知道是否有人可以提供帮助 - 我想使用Array.mapArray.filter但我被困在我的for循环中,尽管阅读了教程等,但我似乎无法解决这个问题。

在此代码中,我有一个对象的Array,我想:

  1. 将数组中的每个项目与其他项目进行比较,并确保obj[i] != obj[i]
  2. 对当前项目执行操作:检查item.target是否null,比较itemitem+1之间的distance,如果item&item+1distance小于item&item.target那么我想用item替换item.target

法典:

for (var i = 0; i < 111; i++) {
var itm = {x:Math.random()*w, y:Math.random()*h, tgt:null};
dotArr.push(itm);
}
function findTarget(itemA, itemB){ 
var x1 = itemA.x; 
var y1 = itemA.y; 
var x2 = itemB.x; 
var y2 = itemB.y; 
var distance = Math.sqrt( (x2-=x1)*x2 + (y2-=y1)*y2 ); 
return distance; 
}
for (var i = 0; i < dotArr.length; i++) {
let itm = dotArr[i];
for (var j = 0; j < dotArr.length; j++) {
if(itm != dotArr[j]){
let itm2 = this.dotArr[j];
if(itm.tgt==null){
itm.tgt = itm2;
}else{
let newDist = findTarget(itm, itm2);
let curDist = findTarget(itm, itm.tgt);
if(newDist<curDist){
itm.tgt = itm2;
}
}
}
}
}

我阅读的教程中的所有"将每个值乘以 2"示例都是有意义的,但无法将其推断为我一直使用的方法。

预期结果:我有一堆粒子,它们正在循环通过requestAnimationFrame()循环,检查每个循环的距离。每个粒子找到最接近的粒子并将其设置为"tgt"(然后在其他代码中向它移动),但它会更新每个循环。

总结

const distance = (a, b) => 
Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2))
const findClosest = (test, particles) => particles.reduce(
({val, dist}, particle) => {
const d = distance(test, particle)
return d < dist && d != 0 ? {val: particle, dist: d} : {val, dist}
},
{val: null, dist: Infinity}
).val
const addTargets = particles => particles.map(particle => {
particle.tgt = findClosest(particle, particles)
return particle
})

(由于数据结构的循环性质,这在代码段中很难做到。 JSON 字符串化不适用于循环。

出于正确的原因改变风格

你说你想从for循环改为mapfilter等等,但你没有说为什么。 确保您出于适当的原因这样做。 我是函数式编程的强烈倡导者,我通常会推动我负责的初级开发人员进行此类更改。 但我解释了原因。

以下是我所做的解释:

"当你做一个循环时,你这样做是有原因的。 如果您希望将一个值列表逐个转换为另一个值列表,那么有一个名为map的内置函数,它可以使您的代码更清晰、更简单。 当您尝试检查应该保留的内容时,您有filter,这使您的代码更清晰、更简单。 当您想在具有特定属性的列表中查找第一项时,您有find,这同样更清晰、更简单。 如果您尝试组合元素直到将它们减少到单个值,则可以使用reduce,令人惊讶的是,它更干净、更简单。

"使用这些的原因是更好地表达代码的意图。 你的意图永远不会是"从某个值开始,到满足某个条件时结束,在每次迭代中执行一些例程,不断增加某个计数器的值。 如果您可以使用更好地表达目标的工具,那么您的代码就更容易理解。 因此,请查找代码中mapfilterfindreduce有意义的位置。

"并非每个for循环都符合其中一种模式,但其中很大一部分会。 替换那些合适的代码将使代码更容易理解,因此更易于维护。

我将从那里继续解释从不担心栅栏错误的优点,以及其中一些函数如何与更通用的类型一起使用,从而更容易重用此类代码。 但这是我在团队中使用的基本要点。

你需要决定为什么要改变,以及它在你的情况下是否有意义。 考虑到您的要求,确实有可能没有。

函数mapfindfilter仅适用于列表中的单个项目。reduce适用于一个项目和当前累积值。 看起来您的要求是对所有值进行成对单词。 这可能意味着这些功能都不适合。

或者也许他们这样做了。 请继续阅读,了解我将如何解决这个问题。

名字很重要

您包含一个名为findTarget的函数。 我假设这样的函数以某种方式找到目标。 事实上,它所做的只是为了计算两个项目之间的距离。

想象一下,来到别人的代码并通读使用findTarget的代码。 在你阅读该函数之前,你不会知道它只是在计算距离。 代码看起来很奇怪。 这比你把它命名为distance要难理解得多。

此外,使用item或缩短版本itm不会告诉读者任何关于这些是什么的信息。 (更新:对帖子的更改指出这些是"粒子",所以我将在代码中使用它而不是itm

避免棘手

findTarget/distance功能做了一些奇怪的事情,而且有些难以遵循。 它在计算过程中修改计算变量:(x2-=x1)*x2(y2-=y1)*y2。 虽然我可以看到这是一样的,但很容易编写一个非常清晰的距离函数,而没有这种棘手:

const distance = (a, b) => 
Math.sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y))

这其中有许多变体同样清楚。

const distance = (a, b) =>
Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2))

总有一天,我们将能够做到

const distance = (a, b) => Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2)

这些中的任何一个都会使代码更清晰。 您还可以使用中间变量,例如dx/dydeltaX/deltaY,如果这样可以让您更清楚的话。

仔细查看您的要求

我花了太长时间查看您的代码来确定您到底要做什么。

如果你能把你需要的部分分解成命名函数,那么编写起来通常要容易得多,而且其他人通常更容易理解(甚至几周后你自己)。

因此,如果我现在正确理解了这个问题,那么您有一个定位对象列表,对于每个对象,您希望使用目标更新它们,即最接近它们的对象。 这听起来很像map.

鉴于此,我认为代码应如下所示:

const addTargets = particles => particles.map(item => ({
x: item.x,
y: item.y,
tgt: findClosest(item, particles)
}))

现在我还不知道findClosest将如何工作,但我希望这符合目标,只要我能写出来。

请注意,这个版本认真对待我对不变性函数式编程概念的信念。 但它不会完全按照你想要的去做,因为粒子的目标将是旧列表中的目标,而不是它自己的列表中的目标。 我个人可能会考虑更改数据结构来解决此问题。 但相反,让我们放宽这种限制,而不是返回新项目,我们可以就地更新项目。

const addTargets = particles => particles.map(particle => {
particle.tgt = findClosest(particle, particles)
return particle
})

因此,请注意我们在这里所做的工作:我们将一个没有目标(或有null目标)的项目列表变成一个有目标的项目列表。 但是我们将其分为两部分:一部分将没有目标的元素转换为具有目标的元素;第二个为给定元素找到适当的目标。 这更清楚地反映了要求。

我们仍然需要弄清楚如何为元素找到合适的目标。 抽象地说,我们正在做的是获取一个元素列表并将其转换为单个元素。 那是reduce. (这不是find操作,因为它必须检查列表中的所有内容。

那么,让我们写一下:

const findClosest = (test, particles) => particles.reduce(
({val, dist}, particle) => {
const d = distance(test, particle)
return d < dist && d != 0 ? {val: particle, dist: d} : {val, dist}
},
{val: null, dist: Infinity}
).val

我们在这里将距离用于双重目的。 首先,当然,我们要看两个粒子相距多远。 但其次,我们假设同一确切位置的另一个粒子是同一个粒子。 如果这不准确,则必须稍作更改。

在每次迭代中,我们都有一个具有valdist属性的新对象。 这总是代表我们迄今为止发现的最接近的粒子以及它与我们当前粒子的距离。 最后,我们只返回val属性。 (Infinity的前提是每个粒子都会比这更接近,所以我们不需要特定的逻辑来测试第一个粒子。

结论

最后,我们能够使用mapreduce。 请注意,在此示例中,我们有两个可重用的帮助程序函数,但每个函数只使用一次。 如果不需要重用它们,可以将它们折叠到调用它们的函数中。 但我不推荐它。 这段代码相当可读。 折叠起来,这些会不那么富有表现力。

dotArr.map(itemI => {
const closestTarget = dotArr.reduce((currentMax, itemJ) => {
if(currentMax === null){
const targetDistance = findTarget(itemI, itemJ)}
if(targetDistance !== null){
return {item:itemJ, distance:targetDistance};
}
return null;
}
const newDistance = findTarget(itemI, itemJ);
if((currentMax.distance - newDistance) < 0){ //No need to check if it is the same item, because distance is 0
return {item:itemJ, distance: newDistance};
}
return sum;
}, null);
itemI.tgt = closestTarget.item;
return itemI;
}

在构建此示例后,我发现您正在使用一个非常复杂的示例来弄清楚 map 的工作原理。

Array.map通常用于一个值,所以我们可以用它做[i],那么我们需要用[j]迭代数组中的所有其他值,但是我们不能用map来做这件事,因为我们只关心最近的[j],所以我们可以使用Array.reduce,这也是一个像Array.map一样的累加器, 但最终结果是你想要的,而Array.map的最终结果总是一个相同长度的数组。

我的reduce函数的作用是遍历整个列表,类似于[j]。我初始化currentMaxnull,所以当j==0然后currentMax===null,然后我弄清楚[j][i]相比是什么状态。返回语句是currentMax[j+1]中等于的内容

当我终于找到最近的目标时,我可以将其添加得如此itemI.tgt并且我必须返回它,以便新地图知道当前indexitem的外观。

不看Array.map这就是我想象的实现方式

function myMap(inputArray, callback){
const newArray = [];
for(let i=0;i<inputArray.length;i++){
newArray.push(callback(inputArray[i], i, inputArray));  
}
return newArray;
}

所以这就是为什么你总是需要写回

我认为在这种情况下您想使用reduce而不是map.

reduce允许您将项目数组"减少"为单个项目。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce

let largestThing = arrayOfThings.reduce(function (largest, nextItem) {
if (largest == null) {
return nextItem;
}
if (largest.prop > nextItem.prop){
return largest;
}
return nextItem;
}, null);

null作为回调的参数是回调中的起始"最大"。

最新更新