Use vs. Mention in JScript Doesn't Come For Free

Use vs. Mention in JScript Doesn't Come For Free

  • Comments 18

Before today's entry, a quick note. Work has just gotten insanely busy as we push towards getting VSTO ready for the Whidbey release. I likely won't have much time to blog over the next couple weeks, so the blog refresh rate is going to go down for a while. I have a collection of pre-written articles -- such as this one -- that I'll dip into every now and then when I have a few spare minutes, but I likely won't be very topical or responsive for a bit.

OK, onward; a while back a reader asked what the difference was between

document.write("hello");

and

foo = document.write;
foo("hello");

from the point of view of what actually happens "behind the scenes" in the IDispatch calls.  The reader noted in particular that though this trick works in IE, it doesn't work in WSH:

foo = WScript.Echo; // Nope, sorry.
foo("hello");

Here's the deal.  The IE object model has been specially designed with JScript in mind.  Unlike VBScript, JScript makes a distinction between naming a function and calling a function.  When you say

abc = blah.baz();

that calls the baz function and results in whatever it returns.  But

abc = blah.baz;

assigns the baz function object itself to abc; it essentially makes an alias.

The IDispatch interface takes flags which determine whether the caller wants to fetch the named property or call the named function. Many object models do not honour those flags though, because no language before JScript really took advantage of the distinction.  In fact, object models designed to be called from VB or VBScript often don't even check the flag -- if it says "fetch the property" or "call the method", well, it just calls the method either way.

So here's what happens when you say foo = document.write;  First, JScript attempts to resolve "document".  It can't find a local or global variable called that, so it asks the global window object for the document property.  IE gives back the document object.  JScript then asks the document object to give back the value of the write property.  IE creates an object which has a default method.  The default method calls the write function, but no one calls the method yet -- we just have an object which, when invoked, will call the mehtod.  JScript assigns the object to foo.  Then when you call foo("hello"); JScript invokes the default method on the object, which calls the write method.

The WSH object model was not designed with this in mind.  It does not make a distinction between naming a function and calling it, so you can't use this trick.  WScript.Echo; does not give back a function object that can be invoked later.

  • So the WSH objects are ignoring the IDispatch call parameters? Should code like that be allowed to be called a COM object?
  • Obviously it's a COM object -- it obeys all the rules of COM. The question is whether it is a good Automation object. And sure, an automation object is allowed to implement any semantics it wants. If an automation object decides that calling a method when it is invoked as a property get is the right thing to do, that's the object author's prerogative.

    The WScript object model isn't the unusual one; most object models behave like that because most object models are called from VB/VBScript, which does not make the use-mention distinction for functions. My point is that it's the IE object model which is the weird one.
  • don't want to be picky here but aliasing is a natural behaviour of ECMAScript and this work also in WSH but only for non build-ins I suppose.

    foo = {};
    foo.bar = {};
    foo.bar.hello = function()
    {
    return "world";
    }

    myAlias = foo.bar.hello;

    WScript.Echo( myAlias() ); //aliasing work

    WScript.Echo( (foo.bar.hello === myAlias).toString() ); //true


    I don't know all the subtleties of IDispatch but it make me think of a similar problem with IE where you can't add properties using prototype to IE build-ins DOM object.
  • Sure, obviously the JScript object model implements the JScript rules! But the WSH object model does not.

  • As Eric obviously knows one problem with using functions as aliases for methods in this way is that you loose the object context. That is, when document.write is invoked 'this' is a reference to the document object. OTOH when foo is invoked 'this' reverts to the global object. In this case it doesn't matter because document.write is a "static" method. I've put static in quotes because JavaScript doesn't have such a keyword. Instead a method is made static by simply ignoring the value of 'this'. If a non-static method had been used, such as document.focus, the appropriate context would have been required, forcing the use of the call or apply Function methods.

    This is actually somewhat annoying because browser event handlers are functions. If you assign an object method to a callback such as onclick, when the callback is invoked 'this' is a reference to the object which generated the event rather than the method's owner. In the BeyondJS library we introduced the Function.from mechanism to handle this scenario:

    foo = Function.from(document, "focus");
    foo(); // it works
  • Indeed, that's an excellent point which I forgot to mention.

    The "this" semantics are thoroughly screwed up in JScript. IIRC, it gets even more messed up when you consider separate modules, but I haven't looked at that code for so long that I don't recall the exact semantics.


  • So a COM object can ignore the parameters of IDispatch::Invoke? I never knew that, I always thought that I should return E_NOTIMPLEMENTED or maybe DISP_E_MEMBERNOTFOUND when asked to do something I don't support, not just silently default to a different action than what was requested. I would've saved myself tons of work if I knew I can ignore the request parameters and just do what I thought is the easiest...
  • Hold on, I'm not saying that it's a free-for all. It depends on what you're doing.

    You'd return DISP_E_MEMBERNOTFOUND if someone tried to pass in a dispatch identifier you didn't recognize. That makes sense -- it is almost certainly an error condition.

    You'd return E_NOTIMPLEMENTED if someone called a method that you simply hadn't implemented -- like, say you don't want to return type infos.

    But what we're talking about here is that some caller passes in the flag for "call this method" when in fact there is no "method" at the given slot, there's a property getter. You could politely call the property getter rather than rudely returning an error. That's certainly well within the bounds of propriety.

    My sole point is that an object model is not _required_ to have different behaviour for "fetch me this property" and "call me this method". JScript was the first automation language that actually made a distinction between the two, and therefore object models that were designed with VB in mind unsurprisingly don't act like JScript objects.
  • Shappir: that is true for most browsers, but it's not true for IE. In IE the function reference actually maintains context, this most likely has to do with the specific handling of this situation as Eric mentioned. The newly created object knows its initial context and apparently saves it.

    Try this:

    attachEvent("onload",yourElement.focus);

    Or if that bothers you, the semantically equiv:

    var fRef=yourElement.focus;
    attachEvent("onload",fRef);

    You will see that focus is correctly moved to the appropriate element.

    This (very comfortable) feature isn't available in other browsers, which follow JS rules a little more closely in the DOM.
  • Just a note: I was only referring to native DOM function references, of course. JS function references WILL lose their "this", obviously.
  • yn:
    > You will see that focus is correctly moved to the appropriate element.

    I was surprised enough by your example to actually try it, and yes you are correct, the focus() method does maintain the appropriate context. But I think this has to do with how focus() is implemented internally rather than how attachEvent is implemented. Consider the following example that uses attachEvent with a JScript object:

    <html>
    <head>
    <script>
    text = "bye bye";
    var foo = {
    bar : function() { alert(this.text); },
    text : "hello world"
    };
    window.attachEvent("onload", foo.bar);
    </script>
    </head>
    <body>
    hello
    </body>
    </html>

    If attachEvent maintained proper context the text "hello world" would have been displayed. But instead the text "bye bye" is displayed.

    As Eric pointed out the "this" semantics are thoroughly screwed up in ECMAScript.
  • I like to think of ECMAScript function pointers as behaving just like C function pointers, i.e. without an instance pointer.

    Dan, if you follow closely what Eric explains in his post as to how getting the "property" 'write' on 'document' is implemented, you'll read that 'document.write' does _not_ return a JScript function pointer (as your foo.bar does) but a temporary COM object with a default method. Presumably this object holds an instance pointer as well, which explains the behavior observed for focus() as well. Btw I don't believe document.write() could work as a 'static' as you describe.

    What I use when I want such delegate-like functionality with JS functions is a simple anonymous function :

    window.attachEvent("onload", function() { return foo.bar() });

    Presumably that's close to what your Function.from() does... No runtime parsing is involved though.
  • :S I don't see any problem with the behaviour of loosing the object context when using function as aliases, it's the function which is aliased not the object.

    from ECMA-262:
    "The pop function is intentionally generic; it does not require that its this value be an Array object.
    Therefore it can be transferred to other kinds of objects for use as a method. Whether the pop
    function can be applied successfully to a host object is implementation-dependent."

    Imho the intentional genericity of function aliasing is the default behaviour right ?
  • Jonathan:
    JavaScript is a pure OO language - everything is an object, including functions. Thus while JavaScript function references appear similar to C function pointers, they are really very different beasts. C function pointers are basically just memory addresses, with some compile-time metadata used to validate arguments and the return value. JavaScript functions are objects that contain more information, e.g. they are closures.

    JScript is built on top of COM automation, and as a results implements objects using IDispatch. This includes function objects. So when you ask for a reference to document.write, or any JScript function, you get an IDispatch that encapsulates that function, which you invoke using DISPID_VALUE (the “this” value is passed using the named argument DISPID_THIS BTW).

    Function.from is implemented using closures. For more information on this technique see http://w3future.com/html/stories/callbacks.xml

    zwetan:
    The technique you mentioned is useful and indeed can be used to implement something sort of like aspects. OTOH I sometimes do want to keep the object context. Fortunately I can use closures for this, though I believe this technique is not well known or understood by most JavaScript programmers.
  • Shappir: Of course it doesn't have to do with attachEvent, my whole point was DOM methods. What you did in your example was show that JS function references lose their object context, that's obvious and I even commented on it right after my initial comment:

    > Just a note: I was only referring to native DOM function references, of course. JS function references WILL lose their "this", obviously.

    Again, the point was that method references to DOM methods retain object context (most likely because of the way they were implemented according to Eric - creating a new object), JS methods do not retain object context, which you just (re)showed.
Page 1 of 2 (18 items) 12