如何区分对象字面量和其他JavaScript对象?

36

更新: 我正在重新表述这个问题,因为对我来说重要的是识别对象字面量:

如何区分对象字面量和任何其他Javascript对象(例如DOM节点、日期对象等)?我该如何编写这个函数:

function f(x) {
    if (typeof x === 'object literal')
        console.log('Object literal!');
    else
        console.log('Something else!');
}

为了让它只在下面的第一次调用中打印Object literal!:

f({name: 'Tom'});
f(function() {});
f(new String('howdy'));
f('hello');
f(document);

原始问题

我正在编写一个JavaScript函数,旨在接受对象字面量、字符串或DOM节点作为其参数。它需要稍微处理每个参数,但是目前我无法区分DOM节点和普通的对象字面量。

这是我的函数的大大简化版本,以及我需要处理的每种参数的测试:

function f(x) {
    if (typeof x == 'string')
        console.log('Got a string!');
    else if (typeof x == 'object')
        console.log('Got an object literal!');
    else
        console.log('Got a DOM node!');
}

f('hello');
f({name: 'Tom'});
f(document);

这段代码将会对后两次调用打印相同的信息。我无法确定在else if语句中应该包含什么。我已经尝试了其他变化,比如x instanceof Object,但效果相同。

我知道这可能是我API/代码设计不好的原因。即使如此,我仍然想知道如何做到这一点。


https://dev59.com/rXRC5IYBdhLWcg3wMd5S - Ben
@Ben,现在我已经更新了问题,我不认为它是那个问题的重复。但如果它是另一个问题的重复,我很乐意关闭它! - Will McCutchen
这几乎是一个重复的问题,但是否有一种解决方案,当给定一个用户定义的对象时不会导致误报呢?https://dev59.com/tm855IYBdhLWcg3weUMQ - Will McCutchen
你之前评论中提供的链接包含了一个答案,可以对你所有的示例进行准确判断而不会出现误报。如果这个答案仍然不能满足你的需求,你能否更新你的问题并提供一个反例? - Anurag
6个回答

60

如何区分对象字面量和其他Javascript对象(例如DOM节点、Date对象等)?

简短的回答是你无法区分。

一个对象字面量是这样的:

var objLiteral = {foo: 'foo', bar: 'bar'};

而使用Object构造函数创建的相同对象可能是:

var obj = new Object();
obj.foo = 'foo';
obj.bar = 'bar';

我认为没有可靠的方法来区分这两个对象是如何创建的。
为什么这很重要?
一般的特性测试策略是测试传递给函数的对象的属性,以确定它们是否支持要调用的方法。这样,您就不需要真正关心对象是如何创建的。
你可以采用"鸭子类型",但只能在有限的范围内使用。你不能保证仅仅因为一个对象有一个getFullYear()方法就是一个日期对象。同样,仅仅因为它有一个nodeType属性并不意味着它是一个DOM对象。
例如,jQuery的isPlainObject函数认为如果一个对象有一个nodeType属性,它就是一个DOM节点,如果它有一个setInterval属性,它就是一个Window对象。这种鸭子类型非常简单化,会在某些情况下失败。
你还可以注意到,jQuery依赖于以特定顺序返回属性 - 这是另一个危险的假设,没有任何标准支持它(尽管一些支持者正在试图改变标准以适应他们假定的行为)。
编辑22-Apr-2014:在版本1.10中,jQuery包括一个基于测试单个属性(显然是为了支持IE9)的support.ownLast属性,以查看继承属性是首先枚举还是最后枚举。这仍然忽略了一个对象的属性可以以任何顺序返回,无论它们是继承还是自有,并且可能会混乱。
"纯"对象的最简单测试可能是:
function isPlainObj(o) {
  return typeof o == 'object' && o.constructor == Object;
}

对于使用对象字面量或Object构造函数创建的对象,这将始终成立,但是对于其他方式创建的对象可能会给出虚假结果并且很可能在跨帧时失败。您也可以添加instanceof测试,但我看不到它有任何比构造函数测试更好的作用。
如果您正在传递ActiveX对象,最好将其包装在try..catch中,因为它们可能返回各种奇怪的结果,甚至会抛出错误。
当然还有一些陷阱:
isPlainObject( {constructor: 'foo'} ); // false, should be true

// In global scope
var constructor = Object;
isPlainObject( this );        // true, should be false

Messing with the constructor property will cause issues. There are other traps too, such as objects created by constructors other than Object.
由于ES5现在已经普及,可以使用Object.getPrototypeOf来检查对象的[[Prototype]]。如果它是内置的Object.prototype,那么该对象就是一个纯对象。然而,一些开发人员希望创建真正的“空”对象,这些对象没有继承属性。可以使用以下方式实现:
var emptyObj = Object.create(null);

在这种情况下,[[Prototype]]属性是null。因此,仅仅检查内部原型是否为Object.prototype是不够的。
还有一个相当广泛使用的:
Object.prototype.toString.call(valueToTest)

在ECMAScript 2015中,指定返回基于内部[[Class]]属性的字符串,对于对象来说是[object Object]。然而,现在会针对其他类型的对象进行测试,默认为[object Object],因此该对象可能不是“普通对象”,仅仅是未被识别为其他类型的对象。因此规范指出:

“[使用toString进行测试]不能为其他类型的内置或程序定义的对象提供可靠的类型测试机制。”

http://www.ecma-international.org/ecma-262/6.0/index.html#sec-object.prototype.tostring

所以,下面是一个更新的函数,允许使用早期ES5主机、具有null的[[Prototype]]和其他没有getPrototypeOf的对象类型(例如null,感谢Chris Nielsen)。
请注意,无法填充getPrototypeOf,因此如果需要支持旧版浏览器(例如IE 8及以下版本,根据MDN),则可能无用。

/*  Function to test if an object is a plain object, i.e. is constructed
**  by the built-in Object constructor and inherits directly from Object.prototype
**  or null. Some built-in objects pass the test, e.g. Math which is a plain object
**  and some host or exotic objects may pass also.
**
**  @param {} obj - value to test
**  @returns {Boolean} true if passes tests, false otherwise
*/
function isPlainObject(obj) {

  // Basic check for Type object that's not null
  if (typeof obj == 'object' && obj !== null) {

    // If Object.getPrototypeOf supported, use it
    if (typeof Object.getPrototypeOf == 'function') {
      var proto = Object.getPrototypeOf(obj);
      return proto === Object.prototype || proto === null;
    }
    
    // Otherwise, use internal class
    // This should be reliable as if getPrototypeOf not supported, is pre-ES5
    return Object.prototype.toString.call(obj) == '[object Object]';
  }
  
  // Not an object
  return false;
}


// Tests
var data = {
  'Host object': document.createElement('div'),
  'null'       : null,
  'new Object' : {},
  'Object.create(null)' : Object.create(null),
  'Instance of other object' : (function() {function Foo(){};return new Foo()}()),
  'Number primitive ' : 5,
  'String primitive ' : 'P',
  'Number Object' : new Number(6),
  'Built-in Math' : Math
};

Object.keys(data).forEach(function(item) {
  document.write(item + ': ' + isPlainObject(data[item]) + '<br>');
});


感谢您的详细回答(大部分都确认了我的假设)。我认为您的“isPlainObj”函数对我的目的来说足够准确。非常感激! - Will McCutchen
请注意,typeof null === "object"Object.getPrototypeOf(null)将会抛出一个错误。如果你希望null输入返回false而不是抛出一个错误,你的isPlainObject函数应该包括一个对null的检查。 - Chris Nielsen
@ChrisNielsen,感谢你让我回到这个问题。自从 ES5 以来,使用 Object.create(null) 可以创建带有 null 作为其 [[Prototype]] 的对象,因此上述内容现在已经非常过时了,我会尽快更新它。 - RobG
鸭子类型,非常完美。有用的答案。 - King Friday

7

与@RobG的例子类似:

function isPlainObject(obj) {
    return  typeof obj === 'object' // separate from primitives
        && obj !== null         // is obvious
        && obj.constructor === Object // separate instances (Array, DOM, ...)
        && Object.prototype.toString.call(obj) === '[object Object]'; // separate build-in like Math
}

TEST:

function isPlainObject(obj) {
 return typeof obj === 'object'
  && obj !== null
  && obj.constructor === Object
  && Object.prototype.toString.call(obj) === '[object Object]';
}

var data = {
  '{}': {},
  'DOM element': document.createElement('div'),
  'null'       : null,
  'Object.create(null)' : Object.create(null),
  'Instance of other object' : new (function Foo(){})(),
  'Number primitive ' : 5,
  'String primitive ' : 'P',
  'Number Object' : new Number(6),
  'Built-in Math' : Math
};

Object.keys(data).forEach(function(item) {
  document.write(item + ':<strong>' + isPlainObject(data[item]) + '</strong><br>');
});


2

由于所有DOM节点都继承自Node接口,您可以尝试以下操作:

if(typeof x === 'string') {
    //string
} else if(x instanceof Node) {
    //DOM Node
} else {
    //everything else
}

但我不确定这是否适用于旧版的Internet Explorer浏览器。


2
你不知道这个。虽然使用原型继承来实现模仿W3C DOM规范建议的方案可能是有意义的,但没有理由期望所有浏览器都这样做。事实上,浏览器不必为宿主对象实现任何类型的继承。 - RobG
1
好的,我已经在Chrome中测试过了(所以它可能也适用于Safari),在Firefox和IE9中也测试过了,因此看起来至少所有现代浏览器都可以这样工作。 - standardModel
@standardModel @RobG 我刚刚看到了 kangax 的一篇深入文章,警告我们不要使用 instanceof Node。但我不知道有什么可靠的替代方法。 - tomekwi
1
@tomekwi - 那篇文章是2010年的,其中表达的原则以前就已经广为人知,但直到那时才被文档化得如此优美(请参见Richard Cornford在此处的回应[here](http://www.highdots.com/forums/javascript/re-client-side-dom-window-34545.html))。它们今天仍然有效。所要表达的观点是,虽然DOM规范暗示了继承,但它们并没有指定原型继承(尽管在较新的规范中有一种强烈的倾向于ECMAScript,这曾经是更加语言中立的)。而且*instanceof*可能会跨帧失败,对于本机对象也是如此。 - RobG
1
@standardModel——这是Richard Cornford链接的原始内容。它在comp.lang.javascript上,该组已被Google Groups所取代:客户端DOM:'window'和'document'是否是真实对象?。请注意Richard关于扩展主机对象原型的警告比kangax的文章早约7年。Kangax参与了prototype.js的开发,他也是通过吃亏而学习的。;-) - RobG

1

1
也许是这样的吗?
var isPlainObject = function(value){
    if(value && value.toString && value.toString() === '[object Object]')
        return true;

    return false;
};

或者采用另一种方法:
var isObject = function(value){
    var json;

    try {
        json = JSON.stringify(value);
    } catch(e){

    }

    if(!json || json.charAt(0) !== '{' || json.charAt(json.length - 1) !== '}')
        return false;

    return true;
};

isPlainObject({toString: () => 123}) //false - dy_

1
将DOM节点的检查移至对象文字上方。检查DOM节点上存在的某些属性以检测节点。我正在使用nodeType。这并不是非常可靠,因为您可以传递一个对象{nodeType: 0},这会破坏它。
if (typeof x == 'string') { /* string */ }
else if ('nodeType' in x) { /* dom node */ }
else if (typeof x == 'object') { /* regular object */ }

所有的鸭子类型检查,包括上面的例子和instanceof检查都注定会失败。要真正确定给定的对象是否实际上是DOM节点,你需要使用除传入的对象本身之外的其他东西。


很抱歉,但在你回答我的问题后我更新了它,因为我意识到需要强调准确识别对象字面量的事实,如果可能的话,这是我的实际目标。 - Will McCutchen

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