2

I'm trying to determine if an object can be stringified or not. This check works in Chrome and Safari, but not in FF (25.0.1).

var good = true;
var myObj = {"param1":11, "param2": "a string", "param3": $("a")}; 
//some cyclic object, specifically I have a jQuery object I got via `$("a")` 
//which returned several anchor tags.

//try to stringify, which supposedly rejects cyclic objects 
try {
    JSON.stringify(myObj);
} catch(error){
    good = false;
}
console.log(good) //returns true.

No error thrown... or I'm not catching it properly? I've never had call to use try... catch before now, so my experience with its nuances is null.

JSON.stringify(myObj) returns a string version of the object, sans many of the object parameters which obviously can't be stringified. It should, according to MDN, error.

Thanks!

Shikiryu
  • 10,180
  • 8
  • 49
  • 75
Randy Hall
  • 7,716
  • 16
  • 73
  • 151

1 Answers1

4

You're catching the error properly, but (as you've identified) Firefox simply isn't throwing an error.

This is because Fiefox doesn't choke on JSONification of DOM objects, where other browsers do:

JSON.stringify(document.getElementById("header"))

In Chrome and Safari, this line results in an error (because in WebKit/Blink, cyclic DOM objects like siblings exist directly on each DOM object), while in Firefox with harmlessly produces the string "{}".

This is because Firefox's DOM objects do not have any of their own enumerable properties:

Object.keys(document.getElementById("header"))
> []

In WebKit/Blink browsers, this line provides an array of property names as strings, because DOM object have their own properties. JSON.stringify only captures an object's own properties, rather than prototype properties.

Bonus Info: More Than You Wanted to Know About the DOM

In Firefox, DOM objects mostly don't have their own properties; instead, property access is delegated up the prototype chain to the HTMLElement.prototype, Element.prototype, or Node.prototype (or the element's immediate prototype, like HTMLDivElement.prototype or HTMLAnchorElement.prototype).

You might wonder: if accessing a property on a DOM element results in prototype access, how can DOM elements have different property values? Don't all DOM elements have more or less the same prototype chain?

The trick here is that the prototype properties don't have values, they are getter functions. For example, when you ask for firstChild of a HTMLDivElement, the JavaScript engine takes the following steps:

  1. Look for the firstChild property on the object itself. It's not there.
  2. Look for the firstChild property on the object's prototype.
  3. Continue up the prototype chain until we find firstChild on Node.prototype.
  4. Node.prototype.firstChild is defined by an accessor property descriptor, meaning that property access results in the execution of a get function.
  5. The this value during the execution of the getter function is the particular DOM element whose firstChild value you asked for/ Firefox uses that this value to do some under-the-hood lookup of the DOM element's first child.

Thus, when you do:

var val = document.getElementById("header").firstChild;

you're really doing:

var elm = document.getElementById("header");
var nodeProto = elm.__proto__.__proto__.__proto__.__proto__;
var propDescriptor = Object.getOwnPropertyDescriptor(nodeProto, "firstChild");
var getterFunc = propDescriptor.get;
var val = getterFunc.call(elm);  // invoke the getter with `this` set to `elm`

Or (less readably):

var val = Object.getOwnPropertyDescriptor(document.getElementById("header").__proto__.__proto__.__proto__.__proto__, "firstChild").get.call(document.getElementById("header"))
Community
  • 1
  • 1
apsillers
  • 112,806
  • 17
  • 235
  • 239
  • Great answer and explanation. For extra credit, know a good easy way to make this check work in FF? – Randy Hall Dec 04 '13 at 17:32
  • @RandyHall If you want to force Firefox to refuse to stringify DOM elements, you can use a passthrough replacer function that throws an error when it tries to stringify a DOM node: `JSON.stringify(myObj, function(key, value) { if(value instanceof Node) { throw new TypeError(); } return value; });` – apsillers Dec 04 '13 at 18:38
  • I literally JUST thought of checking for instanceof Node. Difference is, you probably glanced at it and knew, I've been thinking about it the whole time and just figured it out lol. Thanks much! – Randy Hall Dec 04 '13 at 18:42
  • 1
    @apsillers There is no host object magic here. Per spec all the DOM properties (except unforgeable ones) are on the prototype, not own properties, and `JSON.stringify` only captures own properties. The fact that the DOM properties are own properties in WebKit and Blink is a known bug in those engines; the Blink engineers are working on fixing that bug. On the other hand, Firefox and IE are following the spec here. – Boris Zbarsky Dec 05 '13 at 06:05
  • 1
    @BorisZbarsky Thanks very much for that clarification; I've edited my answer extensively. Is this really spec-mandated behavior? I would have assumed that the DOM spec was too abstract to *require* a setup like this. – apsillers Dec 05 '13 at 14:37
  • 1
    @RandyHall It's useful because it allows pages to hook the prototype to modify behavior for all consumers of the property. People do this all the time for DOM methods, which _are_ on the prototype in WebKit/Blink. – Boris Zbarsky Dec 05 '13 at 17:25
  • @apsillers The spec is at http://heycam.github.io/webidl/#es-attributes and is very much concrete. It defines the exact mapping of the DOM's IDL into actual EcmaScript objects. – Boris Zbarsky Dec 05 '13 at 17:27
  • @BorisZbarsky Oh, I was thinking of the [DOM spec](http://dom.spec.whatwg.org/); I hadn't considered the Web IDL spec. Thanks again! – apsillers Dec 05 '13 at 17:46