如何区分对象字面量和任何其他 Javascript 对象(例如 DOM 节点、日期对象等)?
简短的回答是你不能。
对象字面量类似于:
var objLiteral = {foo: 'foo', bar: 'bar'};
而使用 Object 构造函数 创建的同一对象可能是:
var obj = new Object();
obj.foo = 'foo';
obj.bar = 'bar';
我认为没有任何可靠的方法可以区分这两个对象的创建方式。
为什么重要?
一般的功能测试策略是测试传递给函数的对象的属性,以确定它们是否支持要调用的方法。这样你就不会真正关心对象是如何创建的。
您可以使用“鸭式打字”,但仅限于有限的范围。您不能仅仅因为一个对象具有例如 getFullYear() 方法就保证它是一个 Date 对象。同样,仅仅因为它有一个 nodeType 属性并不意味着它是一个 DOM 对象。
例如,jQuery isPlainObject 函数认为,如果一个对象具有 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
因此,一个更新的函数允许 ES5 之前的主机、[[Prototype]] 为 null 的对象和其他没有 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>');
});