Weird IE8 internal [[ class ]] attribute behavior

2019-04-19 07:27发布

问题:

I recently had some trouble with IE8 (I don't know about 9 at this point) with reading and comparing the value of some [[Class]] properties. Actually it's only in the case for the localStorage object.

I'm using a method like this

var ToStr = Object.prototype.toString;
Object.type = function _type( obj ) {
    var res = ToStr.call( obj ).split( ' ' )[ 1 ].replace( ']', '' );

    if( obj === window ) {
        res = 'Window';
    }
    else if( res === 'Window' || res === 'Global' ) {
        res = 'Undefined';
    }
    else if( res.indexOf( 'HTML' ) === 0 ) { 
        res = 'Node';
    }

    return ( res );
};

This method will return this values for instance:

var foo = { },
    bar = [ ],
    num = 52,
    win = window;

Object.type( foo ) === 'Object'; // true
Object.type( bar ) === 'Array'; // true
Object.type( num ) === 'Number'; // true
Object.type( win ) === 'Window'; // true

That works of course, in all browsers I'm aware of by simply checking that [[Class]] property from an object itself. Now, I'm calling this method on the localStorage object

Object.type( win.localStorage ) === 'Storage' // true (not in IE8)

IE8 just returns Object here. However, that is not the actuall problem, the problem happens when you try to compare the localStorage object with the window object. As you can see, I'm checking if the passed in argument is the current window object

if( obj === window ) { }

If obj now is the window.localStorage object, this will end up in an error

"Class does not support automation"

This only happens if you try to compare localStorage with window, you can compare it against anything else without any trouble. Is this just another bug or can I workaround this issue somehow ?

I guess basically my question is:

How do you know in IE8 (possibly IE9 too) if you're dealing with the localStorage object?

The last thing I want to do is to inner-wrap the whole method with a try-catch because it gets called fairly often.

To entirely confuse me here it comes: When you do a console.log( obj ) in IE8's console it returns you [object Storage] (nice!) but if you call Object.prototype.toString.call( obj ) it returns [object Object]. Same goes for typeof obj, will return object.

Second question:

How does the IE8 console print out the correct [[Class]] ?

回答1:

I've found a way to work around the IE8 behavior using an implicit toString() operation and the ECMAScript spec explains why the work-around makes sense. The implicit toString() is this:

"" + window.localStorage

This is implicitly forcing a call to the object's internal toString() method and, in IE, this will return the desired form you want [object Storage] and you can get your code to work without special casing window.localStorage.

So, I was looking for the minimal risk way to incorporate this into your existing code. The approach chosen was to get the type that same way you use to get it and if and only if it returns a generic "Object" type, then see if there is a better name available with the new method. So, all things that used to work just fine will continue to work the way they did and we might find a better name for some objects (like window.localStorage) that used to return a generic "Object" name. The one other change is that I felt less confident about the exact type of return we might get from the "" + obj construct so I wanted a parsing method that wouldn't throw an error on unexpected data so I switched to a regex from the split/replace method you were using. The regex also enforces that it's really the [object Type] format too which seems desirable.

Then, to protect against the odd issue of comparing localStorage === window and getting an error, you can add a type check (duck typing) that a non-window like object would not pass and this will filter out the localStorage issue and any other objects with the same issue. In this particular case, I make sure the type of the object is "object" and that it has a property named setInterval. We could have selected any well known, well supported property of the window object that is unlikely to be on any other object. In this case, I use setInterval because that's the same test that jQuery uses when it wants to know if an object is a window. Note, I also changed the code to not explicitly compare to window at all because there can be more than one window object (frames, iframes, popups, etc...) so this way, it will return "Window" for any window object.

Here's the code:

Object.type = function _type( obj ) {

    function parseType(str) {
        var split = str.split(" ");
        if (split.length > 1) {
            return(split[1].slice(0, -1));
        }
        return("");
    }

    var res = parseType(Object.prototype.toString.call(obj));

    // if type is generic, see if we can get a better name
    if (res === "Object") {
        res = parseType("" + obj);
        if (!res) {
            res = "Object";
        }
    }
    // protect against errors when comparing some objects vs. the window object
    if(typeof obj === "object" && "setInterval" in obj) {
        res = 'Window';
    }
    else if( res === 'Window' || res === 'Global' ) {
        res = 'Undefined';
    }
    else if( res.indexOf( 'HTML' ) === 0 ) { 
        res = 'Node';
    }

    return ( res );
};

See a demo with various test cases here: http://jsfiddle.net/jfriend00/euBWV

The desired value of "[object Storage]" that you were after in order to parse out the "Storage" class name comes from the internal [[Class]] property as defined in the ECMAScript spec. In section 8.6.2, the spec defines specific Class names for "Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", and "String". It does not define Class names for host objects like localStorage so that is either left to individual browsers or is found in some other spec document.

Further, the spec says this about [[Class]]:

The value of a [[Class]] internal property is used internally to distinguish different kinds of objects. Note that this specification does not provide any means for a program to access that value except through Object.prototype.toString (see 15.2.4.2).

And, it is in 15.2.4.2 that we find the specification for generating the output like [object Array] or [object String] by using the [[Class] as the second word.

So, Object.prototype.toString is how it is supposed to work. Obviously IE8 has bugs in this regard for the localStorage object. We can't know inside of IE8 whether toString() is not using [[Class]] or whether [[Class]] is not set properly. In any case, it appears that console.log() in IE8 is not directly using Object.prototype.toString() because it generates a different result.

The behavior of the "" + obj work-around is more complicated to understand. The spec describes how a type coercion of an object to a string is supposed to work. It's a bit complicated to follow the thread all the way through the spec as one part depends upon another which depends upon another and so on. But, in the end, it executes internal methods ToString(ToPrimitive(input argument, hint String)) and apparently in IE8, ToPrimitive when passed a hint that we want a string is giving us the actual class name that Object.prototype.toString() is not. There is a path through the spec that winds through [[DefaultValue]] which may be how this happens in IE8, but since we already know IE8 didn't follow the first part of the spec and it wasn't generally good at following the spec anyway, it's not a valid assumption to assume that it follows the spec in this regard. In the end, we just know that a type coercion to string in IE8 ends up giving us the [[Class]] that we wanted.

As an interesting test, I tried my test suite in the Chrome browser running all the test cases that are objects through the "" + obj work-around (normally the code only uses that path when Object.prototype.toString() doesn't return a name other than "Object". It works for everything except an array. I think this means that the [[DefaultValue]] for objects is generally [[Class]] (unless the object type decides it has a better default value which Array apparently does). So, I think we have confirmation that the work-around that fixes IE8 is actually supposed to work per the spec. So, not only is it a work-around for IE8, but it's an alternate path to get at the [[Class]] name if the object type doesn't implement a different default value.

So, really what this new code I've proposed is doing via the spec is this pseudo code:

  1. Try to get at the internal variable [[Class]] using Object.prototype.toString()
  2. If that gives us something other than "Object" then, use it
  3. Otherwise, use "" + obj to try to get at the string version of [[DefaultValue]]
  4. If that returns something useful, use it
  5. If we still don't have something more useful than "Object", then just return "Object"


回答2:

You wrote:

This only happens if you try to compare localStorage with window, you can compare it against anything else without any trouble.

Then why don't you do so?

var ToStr = Object.prototype.toString; 
Object.type = function _type( obj ) { 
    if ( window.localStorage && obj === window.localStorage )
        return 'Storage';
    if ( obj === window ) 
        return 'Window'; 
    var res = ToStr.call( obj ).split( ' ' )[ 1 ].replace( ']', '' ); 
    if ( res === 'Window' || res === 'Global' ) { 
        return 'Undefined'; 
    if ( res.indexOf( 'HTML' ) === 0 ) {  
        return 'Node'; 
    return res; 
}; 

Addition to respond to the questions directly:

  1. "... or can I workaround this issue somehow ?": You cannot. If the browser has a bug comparing two special values, you cannot fix this in javascript code....
  2. "How do you know in IE8 (possibly IE9 too) if you're dealing with the localStorage object?" You check, if you are dealing with it, by simply comparing obj === window.localStorage. It cannot get any simpler than that, can it?
  3. "How does the IE8 console print out the correct [[Class]] ?" Internal function have a very different kind of access to those objects, than javascript has... You cannot do the same things there.

Regards, Steffen



回答3:

Feature detection should be used (see below) to avoid failed attempts to access localStorage due to browser policies etc.

As for IE8-specific issue, could you confirm that the page is being served rather than opened locally? i.e. the URL is http://localhost/hello.html rather than file:///C:/somefolder/hello.html IE8 doesn't allow localStorage for files opened locally, although no official documentation could be found to back this (but there's this and this :) Also, possibly worth checking that you're not running the browser in IE7 mode.

In case your code above is supposed to detect feature availability rather than something else entirely, it would be an option to use something like the following:

// Feature test
var hasStorage = (function() {
  try {
    localStorage.setItem(mod, mod);
    localStorage.removeItem(mod);
    return true;
  } catch(e) {
    return false;
  }
}());


回答4:

This isn't pretty at all. The only way I could get the string "[object Storage]" was by using the following code:

obj.constructor.toString();

For any native constructor, the output—and the expected output in other browsers—is a string representation of a native function. However, we're talking about host objects here, where different rules apply entirely. Interestingly, despite all its improvements to the DOM, IE 9 gives the same result.

It's not an entirely resilient solution, but it's the only solution I could find in the short time I had to take a look at the problem.


It seems there's a discrepancy between IE 8 and IE 9's IE 8 document mode and localStorage.constructor doesn't actually exist in the former. In that case, I don't think there's another viable solution. Duck typing doesn't seem like it would be effective as all of the localStorage object's property names are somewhat generic. You could just use

window.localStorage === obj

But I'm not sure of IE 8's behaviour when trying to override native properties of the window object (if it doesn't allow it, then you may be OK).



回答5:

This is indeed a very weird IE8 bug! (oh what do I love IE)!

As stated in the comments for IE8 browsers there is but one solution in my opinion:

typeof obj === "object" && obj.constructor.toString() === "[object Storage]"

Be sure the current browser is IE8 before checking, doesn't work otherwise, not even in other versions of IE!

P.S. Great example of how compatible IE is with its 'siblings'!