实用方法
我认为说某个特定的实现方式是“正确的”(“correct”)只是因为它与“错误”的解决方案相比而言是“正确的”,这种说法是不正确的。 Tomáš的解决方案明显比基于字符串比较数组要好,但这并不意味着它从客观上来说就是“正确的”。到底什么才是“正确”的呢?是最快的吗?最灵活的吗?易于理解吗?最快速调试的吗?使用的操作最少吗?它会有任何副作用吗?没有一种解决方案可以拥有所有优点。
Tomáš可能会说他的解决方案很快,但我也会说它过于复杂了。它试图成为一个适用于所有数组,包括嵌套数组在内的多合一解决方案。实际上,它甚至接受更多不同于数组的输入,并仍然尝试给出一个“有效”的答案。
泛型提供可重用性
我的答案将以一种不同的方式来解决这个问题。我将从一个通用的arrayCompare
过程开始,该过程只关心数组的遍历。从那里,我们将构建我们的其他基本比较函数,如arrayEqual
和arrayDeepEqual
等。
// arrayCompare :: (a -> a -> Bool) -> [a] -> [a] -> Bool
const arrayCompare = f => ([x,...xs]) => ([y,...ys]) =>
x === undefined && y === undefined
? true
: Boolean (f (x) (y)) && arrayCompare (f) (xs) (ys)
在我看来,最好的代码不需要注释,这段代码也不例外。这里发生的事情非常少,你几乎不需要花费任何精力就可以理解这个过程的行为。当然,一些ES6语法现在可能对你来说似曾相识,但那只是因为ES6相对较新。
如其类型所示,
arrayCompare
函数接受比较函数
f
和两个输入数组
xs
和
ys
。在大部分情况下,我们只是对输入数组中的每个元素调用
f(x)(y)
。如果用户定义的
f
返回
false
,我们会提前返回
false
——这要感谢
&&
的短路评估。所以是的,这意味着比较器可以在不必要时停止迭代并防止循环遍历其余的输入数组。
严格比较
接下来,使用我们的
arrayCompare
函数,我们可以轻松创建其他可能需要的函数。我们将从基本的
arrayEqual
开始...
// equal :: a -> a -> Bool
const equal = x => y =>
x === y // notice: triple equal
// arrayEqual :: [a] -> [a] -> Bool
const arrayEqual =
arrayCompare (equal)
const xs = [1,2,3]
const ys = [1,2,3]
console.log (arrayEqual (xs) (ys)) //=> true
// (1 === 1) && (2 === 2) && (3 === 3) //=> true
const zs = ['1','2','3']
console.log (arrayEqual (xs) (zs)) //=> false
// (1 === '1') //=> false
如此简单。可以使用arrayCompare
和一个比较函数定义arrayEqual
,该函数使用===
(用于严格相等性)将a
与b
进行比较。
请注意,我们还将equal
定义为自己的函数。这突出了arrayCompare
作为高阶函数在另一个数据类型(数组)的上下文中利用我们的一阶比较器的角色。
松散比较
我们也可以使用==
来定义arrayLooseEqual
,这样当比较1
(数字)和'1'
(字符串)时,结果将是true
……
// looseEqual :: a -> a -> Bool
const looseEqual = x => y =>
x == y // notice: double equal
// arrayLooseEqual :: [a] -> [a] -> Bool
const arrayLooseEqual =
arrayCompare (looseEqual)
const xs = [1,2,3]
const ys = ['1','2','3']
console.log (arrayLooseEqual (xs) (ys)) //=> true
// (1 == '1') && (2 == '2') && (3 == '3') //=> true
深层次比较(递归)
您可能已经注意到这只是浅层次的比较。Tomáš的解决方案肯定是“正确的方式™”,因为它进行了隐式的深度比较,对吗?
好吧,我们的arrayCompare
过程足够灵活,可以使用一种使深度相等测试变得轻松的方式…
// isArray :: a -> Bool
const isArray =
Array.isArray
// arrayDeepCompare :: (a -> a -> Bool) -> [a] -> [a] -> Bool
const arrayDeepCompare = f =>
arrayCompare (a => b =>
isArray (a) && isArray (b)
? arrayDeepCompare (f) (a) (b)
: f (a) (b))
const xs = [1,[2,[3]]]
const ys = [1,[2,['3']]]
console.log (arrayDeepCompare (equal) (xs) (ys)) //=> false
// (1 === 1) && (2 === 2) && (3 === '3') //=> false
console.log (arrayDeepCompare (looseEqual) (xs) (ys)) //=> true
// (1 == 1) && (2 == 2) && (3 == '3') //=> true
简单来说,我们使用另一个高阶函数构建了一个深度比较器。 这次,我们使用自定义比较器包装 arrayCompare
,以检查 a
和 b
是否为数组。 如果是,则重新应用 arrayDeepCompare
,否则将 a
和 b
与用户指定的比较器(f
)进行比较。 这使我们可以将深层比较行为与我们实际比较单个元素的方式分开。 例如,就像上面的示例所示,我们可以使用 equal
、looseEqual
或任何其他我们制作的比较器进行深度比较。
由于 arrayDeepCompare
是柯里化的,因此我们也可以像之前的示例一样部分应用它。
// arrayDeepEqual :: [a] -> [a] -> Bool
const arrayDeepEqual =
arrayDeepCompare (equal)
// arrayDeepLooseEqual :: [a] -> [a] -> Bool
const arrayDeepLooseEqual =
arrayDeepCompare (looseEqual)
对我来说,这已经是对Tomáš解决方案的明显改进,因为我可以根据需要明确地选择我的数组进行浅层比较或深层比较。
对象比较(示例)
如果你有一个对象数组或类似的东西,该怎么办?也许你想要将这些数组视为“相等”,如果每个对象具有相同的id
值…
// idEqual :: {id: Number} -> {id: Number} -> Bool
const idEqual = x => y =>
x.id !== undefined && x.id === y.id
// arrayIdEqual :: [a] -> [a] -> Bool
const arrayIdEqual =
arrayCompare (idEqual)
const xs = [{id:1}, {id:2}]
const ys = [{id:1}, {id:2}]
console.log (arrayIdEqual (xs) (ys)) //=> true
// (1 === 1) && (2 === 2) //=> true
const zs = [{id:1}, {id:6}]
console.log (arrayIdEqual (xs) (zs)) //=> false
// (1 === 1) && (2 === 6) //=> false
就这么简单。这里我使用了原始的JS对象,但是这种比较器可以适用于 任何 对象类型,甚至是您自定义的对象。Tomáš的解决方案需要完全重新设计以支持这种相等性测试。
包含对象的深层数组? 没问题。我们构建了高度通用的函数,因此它们将在各种用例中起作用。
const xs = [{id:1}, [{id:2}]]
const ys = [{id:1}, [{id:2}]]
console.log (arrayCompare (idEqual) (xs) (ys)) //=> false
console.log (arrayDeepCompare (idEqual) (xs) (ys)) //=> true
任意比较(示例)
或者,如果你想进行其他种类的完全任意比较呢?也许我想知道每个x
是否都大于每个y
……
// gt :: Number -> Number -> Bool
const gt = x => y =>
x > y
// arrayGt :: [a] -> [a] -> Bool
const arrayGt = arrayCompare (gt)
const xs = [5,10,20]
const ys = [2,4,8]
console.log (arrayGt (xs) (ys)) //=> true
// (5 > 2) && (10 > 4) && (20 > 8) //=> true
const zs = [6,12,24]
console.log (arrayGt (xs) (zs)) //=> false
// (5 > 6) //=> false
越简单越好
我们可以看到,我们确实是用更少的代码做更多的事情。 arrayCompare
本身并不复杂,我们制作的每个自定义比较器都有非常简单的实现。
轻松地,我们可以精确定义我们希望如何比较两个数组 - 浅层、深层、严格、松散、一些对象属性或某些任意计算,或这些的任何组合,全部使用一个过程arrayCompare
。甚至可以想象一个RegExp
比较器!我知道孩子们喜欢那些正则表达式…
它是最快的吗?不是。但它可能也不需要。如果速度是衡量我们代码质量的唯一指标,很多真正优秀的代码都会被丢弃 - 这就是为什么我称这种方法为“实用方式”的原因。或者更公平地说,是“一种”实用方式。这个描述适用于此答案,因为我并不是说这个答案只有在与其他答案相比时才是实用的;这是客观的事实。我们用很少的代码达到了高度的实用性,而这些代码很容易理解。没有其他代码可以说我们没有赢得这个称号。
这是否使它成为“正确”的解决方案?这取决于你。没有人能为你做这个决定;只有你知道你的需求。在几乎所有情况下,我更看重直接、实用和多功能的代码而不是聪明和快速的代码。你重视什么可能不同,所以选择适合你的。
编辑
我的旧答案更侧重于将arrayEqual
分解为小程序。这是一个有趣的练习,但并不是处理这个问题的最佳(最实用)方式。如果您感兴趣,可以查看此修订历史记录。
([] == []) == false
这样的愚蠢行为是不可原谅的。 - Alex D