35

更新:我重新表述这个问题,因为对我来说重要的一点是识别对象文字:

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

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/代码设计。即使是这样,我仍然想知道如何做到这一点。

4

6 回答 6

58

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

简短的回答是你不能。

对象字面量类似于:

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

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

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

我认为没有任何可靠的方法可以区分这两个对象的创建方式。

它为什么如此重要?

一般的特性测试策略是测试传递给函数的对象的属性,以确定它们是否支持要调用的方法。这样你就不会真正关心对象是如何创建的。

您可以使用“鸭子打字”,但仅限于有限的范围。你不能保证仅仅因为一个对象有一个getFullYear()方法,例如,它是一个 Date 对象。同样,仅仅因为它具有nodeType属性并不意味着它是一个 DOM 对象。

例如,jQueryisPlainObject函数认为,如果一个对象有 nodeType 属性,它就是一个 DOM 节点,如果它有一个setInterval属性,它就是一个 Window 对象。这种鸭式打字非常简单,在某些情况下会失败。

您可能还注意到 jQuery 依赖于以特定顺序返回的属性 - 任何标准都不支持的另一个危险假设(尽管一些支持者试图更改标准以适应他们的假设行为)​​。

2014 年 4 月 22 日编辑:在 1.10 版中,jQuery 包含一个support.ownLast属性,该属性基于测试单个属性(显然这是为了 IE9 支持)来查看继承的属性是先枚举还是最后枚举。这继续忽略了一个事实,即对象的属性可以以任何顺序返回,无论它们是继承的还是拥有的,并且可能是混乱的。

对“普通”对象最简单的测试可能是:

function isPlainObj(o) {
  return typeof o == 'object' && o.constructor == Object;
}

对于使用对象字面量或 Object 构造函数创建的对象,这始终是正确的,但可能会给以其他方式创建的对象提供虚假结果,并且可能(可能会)跨帧失败。您也可以添加一个instanceof测试,但我看不出它做了构造函数测试没有做的任何事情。

如果您要传递 ActiveX 对象,最好将其包装在 try..catch 中,因为它们会返回各种奇怪的结果,甚至抛出错误。

编辑 2015 年 10 月 13 日

当然也有一些陷阱:

isPlainObject( {constructor: 'foo'} ); // false, should be true

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

弄乱构造函数属性会导致问题。还有其他陷阱,例如由 Object 以外的构造函数创建的对象。

由于 ES5 现在几乎无处不在,有Object.getPrototypeOf来检查[[Prototype]]对象的。如果它是内置的Object.prototype,那么该对象就是一个普通对象。但是,一些开发人员希望创建没有继承属性的真正“空”对象。这可以使用以下方法完成:

var emptyObj = Object.create(null);

在这种情况下,[[Prototype]]属性为null。所以仅仅检查内部原型是否是Object.prototype是不够的。

还有合理广泛使用的:

Object.prototype.toString.call(valueToTest)

被指定为返回一个基于内部[[Class]]属性的字符串,对于 Objects 是 [object Object]。然而,这在 ECMAScript 2015 中发生了变化,因此可以对其他类型的对象执行测试,默认值为 [object Object],因此该对象可能不是“普通对象”,只是一个不被识别为其他对象的对象。因此,规范指出:

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

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

因此,下面是一个更新的函数,它允许 pre-ES5 主机、具有[[Prototype]]null 的对象和其他没有getPrototypeOf的对象类型(例如null,感谢Chris Nielsen)。

请注意,无法填充getPrototypeOf,因此如果需要对旧浏览器的支持(例如,根据MDN ,IE 8 和更低版本)可能没有用。

/*  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>');
});

于 2011-05-04T03:17:00.813 回答
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
}

测试:

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>');
});

于 2016-07-24T19:23:38.177 回答
2

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

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

但我不确定这是否适用于旧版本的 Internet Explorer

于 2011-05-03T22:47:50.930 回答
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;
};
于 2014-02-24T14:13:33.820 回答
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 节点,您需要使用传入对象本身以外的其他内容。

于 2011-05-03T22:21:00.057 回答
1

如果您不介意使用软件包,我建议您为此使用 lodash:

https://lodash.com/docs/4.17.15#isPlainObject

于 2020-07-07T09:02:53.950 回答