Как я могу различать объектный литерал других объектов Javascript?

Обновление. Я перефразирую этот вопрос, потому что важным моментом для меня является идентификация литерала объекта:

Как я могу различить литерал объекта и любой другой объект Javascript (например, DOM node, объект 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 node в качестве аргумента. Он должен обрабатывать каждый аргумент несколько иначе, но на данный момент я не могу понять, как различать DOM node и простой старый литерал объекта.

Вот очень упрощенная версия моей функции, а также тест для каждого типа аргумента, который мне нужно обрабатывать:

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/кода с моей стороны. Даже если это так, я все равно хотел бы знать, как это сделать.

Ответы

Ответ 1

Как я могу рассказать о различии между литералом объекта и любым другим объектом Javascript (например, DOM node, объектом Date и т.д.)?

Короткий ответ: вы не можете.

Литерал объекта имеет значение:

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 node, и если у него есть свойство setInterval, это объект Window. Такой тип утиной печати чрезвычайно упрощен и в некоторых случаях будет терпеть неудачу.

Вы также можете заметить, что jQuery зависит от возвращаемых свойств в определенном порядке - другое опасное предположение, которое не поддерживается никаким стандартом (хотя некоторые сторонники пытаются изменить стандарт в соответствии с их предполагаемым поведением).

Изменить 22-апр-2014: в версии 1.10 jQuery включает свойство support.ownLast, основанное на тестировании одного свойства (видимо, это для поддержки IE9), чтобы увидеть, перечислены ли унаследованные свойства в первую очередь. Это продолжает игнорировать тот факт, что свойства объекта могут быть возвращены в любом порядке, независимо от того, наследуются они или принадлежат, и могут быть перемешаны.

Вероятно, самый простой тест для "простых" объектов:

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

Что всегда будет верно для объектов, созданных с использованием объектных литералов или конструктора Object, но вполне может дать ложные результаты для объектов, созданных другими способами, и может (возможно, будет) проваливаться через кадры. Вы также можете добавить тест instanceof, но я не вижу, что он делает что-то, что не делает тест конструктора.

Если вы передаете объекты ActiveX, лучше всего обернуть их в try..catch, поскольку они могут возвращать всевозможные странные результаты, даже бросать ошибки.

Редактировать 13-окт-2015

Конечно, есть некоторые ловушки:

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

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

Передача свойства конструктора вызовет проблемы. Существуют и другие ловушки, такие как объекты, созданные конструкторами, отличными от Object.

Так как ES5 теперь довольно вездесущий, Object.getPrototypeOf проверить [[Prototype]] объекта. Если это buit-in 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]] нулевого и других типов объектов, у которых нет getPrototypeOf (например, null, thanks Chris Nielsen) ниже.

Обратите внимание, что нет пути к polyfill 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 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>');
});

Ответ 2

Поскольку все DOM-узлы наследуются от интерфейса Node, вы можете попробовать следующее:

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

Но я не уверен, что это работает в более старых версиях Internet Explorer

Ответ 3

Переместите проверку для DOM node над литералом объекта. Проверьте свойство, существующее на DOM node, для обнаружения node. Я использую nodeType. Это не очень надежное, поскольку вы могли бы передать объект {nodeType: 0 }, и это сломало бы это.

if (typeof x == 'string') { /* string */ }
else if ('nodeType' in x) { /* dom node */ }
else if (typeof x == 'object') { /* regular object */ }

Все проверки на утиную печать, такие как выше, и даже теги instanceof, обязательно сбой. Чтобы действительно определить, действительно ли данный объект DOM node, вам нужно использовать нечто иное, чем сам переданный объект.

Ответ 4

Может быть, что-то вроде этого?

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

Ответ 5

Аналогично примеру @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>');
});