@Nina 的答案实用、高效,绝对是您应该阅读的答案。但是,这些问题对我来说很有趣,我喜欢考虑以其他方式解决它们,即使这意味着进行交易。
JavaScript 中的复合数据相等性
在 JavaScript 中测试复合数据相等性可能很麻烦
console.log (1 === 1) // true
console.log ('a' === 'a') // true
console.log ([1,2] === [1,2]) // false
console.log ({a:1} === {a:1}) // false
这种简单的相等性测试也可能使处理 JavaScript 的原生 Set 和 Map 变得有些挑战
const m = new Map ()
m.set ([1,2], 'hello')
console.log (m.get ([1,2])) // undefined
console.log (m.get (Array.from (m.keys ()) [0])) // 'hello'
混蛋!复合数据相等性再次困扰我们。 m.get 找不到键 [1,2] 因为第一个键(我们设置的)[1,2] 与第二个键(获取)[1,2] 不同——即[1,2] 的两个实例位于不同的内存位置并因此被认为(由 JavaScript)不相等 (!==)
复合数据相等,取2
如果我们不想,我们不必遵守 JavaScript 的规则。在这部分答案中,我们创建了自己的Dict(字典)复合数据类型,它接受用于确定键相等性的函数
想象一下Dict 正在做这样的事情
const d = Dict (({a} => a)
d.has ({a:1}) // false
d.set ({a:1}, 'hello') .has ({a:1}) // true
d.set ({a:1}, 'hello') .get ({a:1}) // 'hello'
d.get ({a:2}) // undefined
d.set ({a:2}, 'world') .get ({a:2}) // 'world'
如果我们有一个像Dict 这样工作的数据类型,那么我们可以轻松地为我们的数据编写必要的转换
// our Dict type with custom key comparator
const DictByMake =
Dict (x => x.make)
const dict =
data.reduce((d, item) =>
d.set (item, d.has (item)
? d.get (item) .concat ([item])
: [item]), DictByMake ())
我说如果我们有像Dict 这样的数据类型,因为乐观是好事。我为什么要做出牺牲并选择一种无法满足我需求的数据类型,而我不必这样做?如果我需要的类型不存在,我可以制作一个。在此先感谢 JavaScript!
下面我实现Dict 与JS 的Set 和Map 有一些 一致性——这里最显着的区别是Dict 是持久的(不可变的)(一个偏好问题,在本例)
const Pair = (left, right) => ({
left,
right
})
const Dict = eq => (pairs=[]) => ({
equals (x, y) {
return eq (x) === eq (y)
},
has (k) {
for (const {left} of pairs)
if (this.equals (k, left))
return true
return false
},
get (k) {
for (const {left, right} of pairs)
if (this.equals (k, left))
return right
return undefined
},
set (k, v) {
for (const [i, {left, right}] of pairs.entries ())
if (this.equals (k, left))
return Dict (eq) (pairs
.slice (0, i)
.concat ([Pair (k, v)])
.concat (pairs.slice (i+1)))
return Dict (eq) (pairs.concat ([Pair (k, v)]))
},
entries () {
return {
*[Symbol.iterator] () {
for (const {left, right} of pairs)
yield [eq (left), right]
}
}
}
})
const DictByMake =
Dict (x => x.make)
const main = data => {
// build the dict
const dict =
data.reduce((d, x) =>
d.set(x, d.has (x)
? [...d.get (x), x]
: [x]), DictByMake ())
// convert dict key/value pairs to desired {key, items} shape
return Array.from (dict.entries (), ([key, items]) =>
({ key, items }))
}
const data =
[{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }]
console.log (main (data))
复合数据相等,取3
好的,发明我们自己的数据类型有点紧张!在上面的示例中,我们基于 Array(本机)对的 Dict 与其他使用二叉搜索树或哈希表来获取/设置的关联类型相比,这种幼稚的实现细节使 Dict 效率低下键。我用这个例子来展示如何从一个更原始的类型构建一个更复杂的类型,但是我们可以很容易地创建我们自己的 Tree 类型并使用它来代替。
实际上,JavaScript 给了我们Map 并且不必(得到)担心如何它是如何实现的——虽然它没有确切的 em> 我们想要的行为,我们可以稍微调整它的行为,而无需从头开始发明一种全新的类型。
值得注意的是,MapBy 没有在此处实现为持久结构
const MapBy = ord => (map = new Map ()) => ({
has: k =>
map.has (ord (k)),
get: k =>
map.get (ord (k)),
set: (k, v) =>
MapBy (ord) (map.set (ord (k), v)),
keys: () =>
map.keys (),
values: () =>
map.values (),
entries: () =>
map.entries ()
})
// the rest of the program stays exactly the same (with exception to variable names)
const MapByMake =
MapBy (x => x.make)
const main = data => {
const map =
data.reduce((m, x) =>
m.set(x, m.has (x)
? [...m.get (x), x]
: [x]), MapByMake ())
return Array.from (map.entries (), ([key, items]) =>
({ key, items }))
}
const data =
[{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }]
console.log (main (data))