如何可靠地检查一个对象是否为 EcmaScript 6 的 Map/Set?

24

我只想检查一个对象是MapSet而不是Array

为了检查一个Array,我使用lodash的_.isArray

function myFunc(arg) {
  if (_.isArray(arg)) {
    // doSomethingWithArray(arg)
  }

  if (isMap(arg)) {
    // doSomethingWithMap(arg)
  }

  if (isSet(arg)) {
    // doSomethingWithSet(arg)
  }
}

如果我要实现isMap/isSet,它需要是什么样子?我希望它能够尽可能地捕获Map/Set的子类。

5个回答

31

这种情况类似于ES5之前检测数组的方法,需要准确可靠。查看这篇优秀文章了解实现isArray可能出现的问题。

我们可以使用:

  • obj.constructor == Map/Set,但这不适用于子类实例(并且容易被欺骗)
  • obj instanceof Map/Set,但这仍然不能跨域进行工作(并且可以被原型篡改欺骗)
  • obj[Symbol.toStringTag] == "Map"/"Set",但这又很容易被欺骗。

要确保真正可靠,我们需要测试一个对象是否具有[[MapData]]/[[SetData]]内部插槽。这不太容易访问-它是内部的。不过,我们可以使用一个技巧:

function isMap(o) {
    try {
        Map.prototype.has.call(o); // throws if o is not an object or has no [[MapData]]
        return true;
    } catch(e) {
        return false;
    }
}
function isSet(o) {
    try {
        Set.prototype.has.call(o); // throws if o is not an object or has no [[SetData]]
        return true;
    } catch(e) {
        return false;
    }
}

对于一般用途,我建议使用 instanceof - 它简单易懂,性能也很好,在大多数情况下都可以正常工作。或者你可以直接使用鸭子类型,并只检查对象是否具有 has/get/set/delete/add/delete 方法。


为什么不使用 Object.prototype.toString.call(o) == '[object Set]'?除了某些人覆盖 Object.prototype.toString 之外,还有其他情况会导致失败吗?(我认为覆盖 Object.prototype.toString 不值得担心 - 这是一种糟糕的做法,会破坏许多现有的库。) - Matt Browne
@MattBrowne Object.prototype.toString 相当于使用 Symbol.toStringTag 方法 - 你不需要覆盖全局,它可以在每个对象的基础上进行操作。 - Bergi

9
您可以使用 instanceof 运算符:
function isSet(candidate) {
  return candidate instanceof Set;
}

如果候选对象在其原型链中具有Set.prototype,那么instanceof运算符将返回true
编辑 - 虽然instanceof通常有效,但在Bergi的回答中描述的某些情况下可能无法正常工作。

即使该集合是Set的子类,这是否仍然有效?我应该指出这一点。 - nackjicholson
1
@nackjicholson,这取决于你如何创建子类。如果构造函数的.prototype在对象的原型链中,instanceof运算符将返回true - Pointy
1
只要你这样做 class MySet extends Set,是的。 - Felix Kling
1
请注意,如果您正在编写将被重新分发的库的代码,则最好首先检查是否定义了 window.Set(除非您的库包括旧浏览器的 shim)。 (或者为了使其在服务器上也能正常工作,您可以检查 typeof Set!= 'undefined')。 - Matt Browne
1
是的。实际上,在这种情况下更糟糕,因为与数组不同,集合只存在于最新的规范中。通常在旧浏览器中使用shim,我刚意识到Object.prototype.toString检查对它们无效。我可能会在这里发布一个答案,说明我最终采用了什么方法。 - Matt Browne
显示剩余3条评论

1
您可以简单地使用:

export function isMap(item) {
  return !!item && Object.prototype.toString.call(item) === '[object Map]';
}

export function isSet(item) {
  return !!item && Object.prototype.toString.call(item) === '[object Set]';
}

除非该方法的原型被覆盖。

布尔检查是否严格需要?似乎没有也可以使用假值。 - Brett Zamir
是的,这是必要的,因为如果item未定义或为空,则会在其上调用object.toString并引发错误。 - Melchia
在Chrome和Firefox中,对于Object.prototype.toString.call(null),我得到的是"[object Null]",而对于对undefined的调用,则为"[object Undefined]" - Brett Zamir

0

或者检查原型:

Object.getPrototypeOf(i) === Map.prototype

0
以下方法在Vue源代码中使用。你可以从这里借鉴灵感。

export const isMap = (val: unknown): val is Map<any, any> =>
  toTypeString(val) === '[object Map]'
export const isSet = (val: unknown): val is Set<any> =>
  toTypeString(val) === '[object Set]'


export const objectToString = Object.prototype.toString
export const toTypeString = (value: unknown): string =>
  objectToString.call(value)

这很容易理解。将对象转换为字符串后,您只需要检查字符串的值是否为'[object Map]''[object Set]'

我强烈建议前端开发者阅读源代码中的实用函数。您总是可以找到最佳实践。

这是Vue源代码的链接:https://github.com/vuejs/vue-next/blob/master/packages/shared/src/index.ts

您可以在此处找到我引用的代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接