Ian Bicking: the old part of his blog

Prototype.js and Object.prototype

I hadn't seen a problem with Prototype's Object.prototype.extend method until recently, when I added Xinha support to the Paste filebrowser example, and it broke Xinha. I would have actually been mystified about the breakage, if I hadn't read Bob's rant on it (and another critique of Object.prototype).

Here's the code in question:

Object.prototype.extend = function(object) {
  for (property in object) {
    this[property] = object[property];
  }
  return this;
}

Here's what I first replaced it with (and this didn't work!):

function extend(ob1, ob2) {
  for (property in ob2) {
    ob1[property] = ob2[property];
  }
  return ob1;
}

The problem was that methods became unbound, and Prototype uses .extend({...}) to do something subclass-like. I'm personally highly confused about how functions become bound to objects. In this case, this would be set to window instead of ob1 (window is the browser's global object and default this). But this version worked:

function extend(ob1, ob2) {
  return (function(object) {
    for (property in object) {
      this[property] = object[property];
    }
    return this;
  }).apply(ob1, [ob2]);
}

And I modified prototype to make use of the function. I don't know if I'll continue to use Prototype or not -- there's an explosion of Javascript libraries, and it's very hard to choose. Some people's priorities are different than mine (for instance, comparison hasn't come up for me as an issue, and Dojo's obsession with transports doesn't make sense to me either, and anything with server-side integration is a complete non-starter for me). I'll keep experimenting.

Explanations or pointers to exactly how and when this gets bound would be of interest to me, if anyone has seen them. It seems to be highly contextual -- there's nothing like Python's BoundMethod.

Update: Now I'm not so sure the second version of extend() works any differently than the first. I may have fixed some unrelated problem at the same time and not realized which fix was important. Blast.

Created 06 Jul '05
Modified 07 Jul '05

Comments:

Did you get the email I BCC'ed you on with the URL to the MochiKit svn repo?

Bound functions can be done there, manually, with bind(func, self), or in the constructor with bindMethods(this) to iterate over the properties in this and turn all functions into bound ones..

I'm working on an object protocol that's something like Python classes, but I haven't yet decided on what good semantics are like for JavaScript.

# Bob Ippolito

I did see that. In this case I was just trying to reform Prototype, since I'd already started using it. I remember trying this:

def extend(ob1, ob2) {
    for (property in ob2) {
        if (typeof ob2[property] == 'function') {
            ob1[property] = ob2[property].bind(ob1);
        } else {
            ob1[property] = ob2[property];
        }
    }
    return ob1;
}

And it didn't work. Prototype's Function.prototype.bind() is, I assume, like MochiKit's bind(). I don't know quite why it didn't work -- maybe because by binding to a specific object you defeat further inheritance.

# Ian Bicking

You do defeat further inheritance like that.. however, in theory, you should be able to do it like this (which is one of the things I'm considering for the object model):

function isBoundFunction(func) {
    return (typeof(func.im_func) == 'function');
}

function bind(func, self) {
    var im_func = null;
    if (isBoundFunction(func)) {
        im_func = func.im_func;
    } else {
        im_func = func;
    }
    func = function () {
        return func.im_func.apply(func.im_self, arguments);
    }
    func.im_func = im_func;
    func.im_self = self;
}
# Bob Ippolito

With return func; at the end, of course.

# Bob Ippolito

Ok, just tested it.. it works in practice. Check the new implementation in svn (which is very similar to this, but it has tests and stuff).


There is http://www.quirksmode.org/js/this.html but it doesn't go deep enough.

# Seo Sanghyeon

The answer you're looking for is that foo.bar() is special syntax in JavaScript. Run the following in a browser:

foo = {
    'bar': function () {
        alert(this);
    },
    'toString': function () {
        return 'foo';
    }
};
foo.bar();
(foo.bar)();
(foo.bar || null)();
bar = foo.bar; bar();

The first call, foo.bar() is the "call with this" syntax, so you should get an alert with "foo".

The second call, (foo.bar)() is technically "just call" syntax, but some JavaScript interpreters will interpret it as the same AST as the first one. For example, Safari will still say "foo" here, but Firefox will say "[object Window]".

The third call is unambiguously "just call" syntax, so you will get "[object Window]" here. I suppose if Safari gets any smarter, it might give the wrong answer here someday ;)

And the fourth is really really unambiguous "just call" syntax, so you will get "[object Window]" here as well.

# Bob Ippolito

That's what I was figuring, even if it's very hard for me to parse all of obj.func() at once, without thinking about any intermediate expressions. A bit of unlearning to be done.

But it still doesn't really explain why the final version of extend works... what's different about this[property] = object[property] from ob1[property] = ob2[property]? The only reason I thought to do that is that it mimicked the original Object.prototype.extend method.

# Ian Bicking

There shouldn't be any difference at all unless you pass null or undefined as ob1. They seem to do the same thing here, you didn't give enough information to reproduce any difference in behavior.

# Bob Ippolito

applay() not only simply replace 'this' with the assigned object,it also provides the executing enviroment for the function which is applied.

var obj1={
value:1,
foo:function(){
alert(this.value);

System Message: WARNING/2 (<string>, line 7)

Definition list ends without a blank line; unexpected unindent.

}

System Message: WARNING/2 (<string>, line 8)

Definition list ends without a blank line; unexpected unindent.

}

var obj2={

value:2, setFoo:function(foo){

System Message: ERROR/3 (<string>, line 13)

Unexpected indentation.
this.foo = foo;

System Message: WARNING/2 (<string>, line 14)

Block quote ends without a blank line; unexpected unindent.

}

System Message: WARNING/2 (<string>, line 15)

Definition list ends without a blank line; unexpected unindent.

}

setFoo(foo){
this.foo = foo;

System Message: WARNING/2 (<string>, line 19)

Definition list ends without a blank line; unexpected unindent.

}

setFoo.apply(obj2,[obj1.foo]) in fact equals obj2.setFoo(obj1.foo)

obj2.setFoo(obj1.foo) means setFoo will be evaluated in the obj2 scope while the expression obj1.foo = obj2.foo will be evaluted in global scope.

# wl_tony