4

In an object, for nothing more than purely aesthetic reasons, I'm wondering if there's a way to allow access to 'this' members from within sub-objects.

Take this example (which I've mirrored at http://jsfiddle.net/Wzq7W/5/):

function BlahObj() {
    this.users = {};
}
BlahObj.prototype = {
    userGet: function(id) {
        if (this.users[id] !== undefined) {
            return this.users[id];
        }
    },
    userAdd: function(id, name) {
        if (this.users[id] === undefined) {
            this.users[id] = name;
        }
    },
    user: {
        get: function(id) {
            if (this.users[id] !== undefined) {
                return this.users[id];
            }
        }
    }
}

var test = new BlahObj();
test.userAdd(1, 'User A');
test.userAdd(2, 'User B');
test.userAdd(3, 'User C');
test.userGet(2);
test.user.get(1);

The userGet() method will properly return 'User B', but due to scope, user.get() cannot access the base member.

In context, this object will have many more members and methods that go along with it, so camel-casing seems so dirty; being able to separate them out by group (user.get(), queue.foo(), room.data()) seems like a better approach in theory. Is it practical? Is there a way to do what I'm asking, in the way I'm looking to do it, or would I just be better off with camelCase?

Thanks for your thoughts!

Morgon
  • 3,269
  • 1
  • 27
  • 32
  • It's not a excellent solution, but in the BlahObj constructor you can do `this.user.get = this.user.get.bind(this);` to bind it to the correct object. – Twisol Feb 13 '12 at 05:00
  • http://stackoverflow.com/questions/436120/javascript-accessing-private-member-variables-from-prototype-defined-functions You might want to check out this question – Jibi Abraham Feb 13 '12 at 05:09
  • Thanks for the response, Jibi. However, I don't believe this is quite the same, as the pattern used in that question uses private members, whereas mine are public. @Twisol: That does work, but as you mentioned is a little unwieldy to have to do that for each sub-member. Appreciated all the same! – Morgon Feb 13 '12 at 05:12
  • btw the `if (this.users[id] !== undefined)` check is useless as it will return `undefined` if it's `undefined` anyway. – qwertymk Feb 13 '12 at 05:21
  • I would personally prefer to change the design a little, using 'test.users.add()' and 'test.users.get()'. It sounds like you logically have a collection here, with the possibility of exposing other collections as "sub-objects" as well. – Twisol Feb 13 '12 at 05:21
  • qwertymk: Yeah, I know, simple late-night fail. :) -- @Twisol: You're right that the intent is a collection. Pardon my denseness, but if the method sub-object was users (users.get()), what would the users{} member be? Or am I missing something fundamental? – Morgon Feb 13 '12 at 05:26
  • @Morgon: Sorry, I wasn't very clear. :D I'm suggesting doing away with the 'users' object in the prototype itself, and creating an object within the constructor that stores entries and has .add, .get, etc. methods. That gets rid of the `this` issues, and also keeps the related methods together in one place. – Twisol Feb 13 '12 at 05:33
  • @Twisol: Interesting. At first it seemed like it prevented me from accessing other top-level members in the constructor (at least if I'm understanding you correctly). For instance, I'll be keeping a simple 'banlist' of user id's. If I make something like | this.users = { store: {}, add: (...), isBanned(userid) { return this.banlist.indexOf(userid) } | 'this.banlist' is undefined from within that object. I was able to put 'self: this' under this.users{}, which then comically makes my call 'this.self.banlist'. Huge issue? Nah, but interesting. :) – Morgon Feb 13 '12 at 05:51
  • I get the feeling you want a [Facade](http://en.wikipedia.org/wiki/Facade_pattern). You have two separate objects - a list of users, and a list of those users whom are banned. These objects logically have little to do with eachother, except insofar as they store subsets of the same data. But a facade can contain both of them and provide a unified interface, including logic to check whether a user is banned before doing something. – Twisol Feb 13 '12 at 06:04
  • @Morgon: Gist with example: https://gist.github.com/bfd22c556cfa95adaa08 - I don't know what you're actually attempting, so it could be woefully inadequate; but even if you have more complicated objects representing your user list and banlist, the same idea applies. – Twisol Feb 13 '12 at 06:09

3 Answers3

3

The userGet() method will properly return 'User B', but due to scope, user.get() cannot access the base member.

The value of a function's this key word has nothing to do with scope. In a function, this is a local variable whose value is set by the call, not by how the function is declared or initialised.

You can use ES5 bind to change that (and add Function.prototype.bind for those environments that don't have it), or you can use a closure or similar strategy instead of this.

Edit

Consider the camelCase option instead, I think it's much cleaner than creating a sub–level, i.e.

getUser: function() {...},
addUser: function() {...},
...

rather than:

user: {
  get: function(){},
  add: function(){},
  ...
}

It's one character less to type too (e.g. addUser vs user.add)

RobG
  • 142,382
  • 31
  • 172
  • 209
  • Would I have to bind() each sub-member individually, as Twisol mentioned in my my question (comment 1) ? In other words, if BlahObj had 5 sub-objects, and each of those had 5 methods, would I have to write out 25 bind() statements? – Morgon Feb 13 '12 at 05:18
  • Yes. But consider camelCase instead, you are creating a level of abstraction because of a naming scheme, not because it's good architecturally. – RobG Feb 14 '12 at 01:33
3

DEMO

Change this:

user: {
    get: function(id) {
        if (this.users[id] !== undefined) {
            return this.users[id];
        }
    }
}

To:

user: function() {
    var that = this;

    return {
        get: function(id) {
            if (that.users[id] !== undefined) {
                return that.users[id];
            }
        }
    };
}

You have to call it like test.user().get(1) instead of test.user.get(1) though

qwertymk
  • 34,200
  • 28
  • 121
  • 184
0

Another friend of mine suggested a loop in the constructor that would bind() similar to what both @RobG and @Twisol had suggested, but without having to specifically write out each line.

There are probably other ways to do that loop so it's not specific to two levels deep, but this might be good enough for now. Are there any implications I should be aware of, specifically performance, or other considerations I may not be taking into account?

function BlahObj() {
    this.users = {};

    for (sub in this) {
        for (key in this[sub]) {
            if (typeof this[sub][key] == 'function') {
                this[sub][key] = this[sub][key].bind(this);
            }
        }
    }
}
BlahObj.prototype = {
    userGet: function(id) {
        if (this.users[id] !== undefined) {
            return this.users[id];
        }
    },
    userAdd: function(id, name) {
        if (this.users[id] === undefined) {
            this.users[id] = name;
        }
    },
    user: {
        get: function(id) {
            if (this.users[id] !== undefined) {
                return this.users[id];
            }
        }
    }
}

var test = new BlahObj();
test.userAdd(1, 'User A');
test.userAdd(2, 'User B');
test.userAdd(3, 'User C');
test.userGet(2);
test.user.get(1);
Morgon
  • 3,269
  • 1
  • 27
  • 32