Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weird IE8 internal [[ class ]] attribute behavior

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]] ?

like image 708
jAndy Avatar asked Mar 26 '12 12:03

jAndy


People also ask

What is yiibehaviorsattributebehavior in ActiveRecord?

Class yiibehaviorsAttributeBehavior. AttributeBehavior automatically assigns a specified value to one or multiple attributes of an ActiveRecord object when certain events happen.

What are the attributes and behaviors of a class?

Every class contains attributes and behaviors. Attributes are the characteristics of the class that help to distinguish it from other classes. Behaviors are the tasks that an object performs. A person's attributes, for example, include their age, name, and height, while their behaviors include the fact that a person can speak, run, walk, and eat.

What is the difference between attributes and behaviors?

Attributes are the characteristics of the class that help to distinguish it from other classes. Behaviors are the tasks that an object performs. A person's attributes, for example, include their age, name, and height, while their behaviors include the fact that a person can speak, run, walk, and eat.

How to set the behavior of scrolling in HTML?

The Marquee behavior attribute in HTML is used to set the behavior of scrolling. The default value is scroll.


2 Answers

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"
like image 98
jfriend00 Avatar answered Oct 03 '22 01:10

jfriend00


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

like image 32
Steffen Heil Avatar answered Oct 03 '22 02:10

Steffen Heil