An Object that returns an instance of itself
Asked Answered
B

4

11

Background: My latest project cannot use a large library, which saddens me. There are a few things that I would like to have from any library such as the missing functions addClass, hasClass, removeClass, compatible addEventListener, etc. So I created a little object which I'd like some opinions on some other time, but I'm having a little bit of trouble setting it up how I'd like.

For convenience of use, I want an object to return a new instance of itself on creation.

Given:

 $ = function() {
    this.name = "levi";

    return this;
};

console.log($());

We get DOMWindow instead of $ because of the quirky nature of this in JavaScript. What's more strange to me is that console.log(new $().name) properly returns "levi". If this is bound to window, why did the object properly get the value?. We could just add new console.log(new $()) and it works. However, I don't want to write new everytime. So I tried:

$ = function() {
    var obj = function() {
        this.name = "levi";
    };

    return new obj();
};

console.log($());

Which gives me what I want, but it seems a bit unnecessary to wrap the object inside of a function which creates it. Further more, the returned object is obj and not $. Comparison tests would fail.

What are some other ways this can be done? Is there a more elegant solution? I have no qualms about rethinking my entire process. I consider myself pretty good at using JavaScript, but creating new JavaScript is something I am very new to.


Does anyone see anything wrong with the following solution?

$a = function() {};

$ = function() {
    if (!(this instanceof $)) {
        return new $();
    }

    this.name = "levi";

    return this;
};

//helper function
var log = function(message) {
    document.write((message ? message : '') + "<br/>");
};

log("$().name == window.name: " + ($().name == window.name)); //false
log("$().name: " + $().name); //levi
log("window.name: " + window.name); //result

log();

log("$a instanceof $: " + ($a instanceof $)); //false
log("typeof $a: " + (typeof $a)); //function
log("typeof $: " + (typeof $)); //function

It appears to be working in all my tests.

Beatification answered 21/5, 2011 at 22:34 Comment(3)
You cannot use any large library, or cannot use a specific large library (jQuery)?Ire
Any, my friend. It's for very specific mobile browsers and my client does not want the large libraries. I'm comfortable enough in JavaScript to work without libraries, but these few methods are extremely helpful in this project.Beatification
Updated post to remove some reserved words that did not have the correct meaning in my usage of them.Beatification
W
6

The most simple way to do what you want would be (I think):

$ = function(){
    if (!(this instanceof $)){
     return new $;
    }
    this.name = 'levi'; 
    return this;
}

The fact that just returning this doesn't create an instance of $ is because of the way this is created executing $ as a regular function: in that case the value of this points to the global object (within a browser: window, actually calling executing $() is the same as window.$()). It's a fact of javascript life so to speak. The fact that console.log(new $().name) shows the right value is because you call the function as a constructor, which returns an instance of that constructor (i.e. an new instance of $). But console.log($().name) will also print 'levi', because it returns the global object with property name, i.e. window.name. try $(); console.log(name) and you'll see name is a global variable now. So if you don't want to use the new keyword every time, check if your function is called as a regular function, or as a constructor for an instance (=== instanceof $) within the constructor function. With the above method an instances constructor, no matter if it's instantiated with or without new will allways be $

Maybe you should rephrase the title of your question to: 'An Object [constructor] that returns an instance of itself'

Maybe this blog entry can shed extra light.

Wiretap answered 21/5, 2011 at 23:53 Comment(3)
Your code is "constructing" this, ie setting attributes, even when it returns a new instance. As for the rest, you are entirely correct.Jaborandi
@Zecc, you're right, the instanceof check should occur first. Code adapted.Wiretap
Shame that you took so long. I accepted my own post that uses this technique because it didn't exist. Edit: Decided to delete own post to accept this one.Beatification
I
5

The way jQuery does it is first tests if this is window (called as a function), and if so, returns a new instance of itself. For example:

var $ = function() {
    if(this === window) {
        return new $();
    }
    this.name = "levi";
    return this;
};

console.log($());

This works because when you call a function normally (func()), this will be the same as the caller's this. (Related but not irrelevant: obj.method() will have this be obj) Since the default scope is window, this inside $ will be window when you call it as $().

When you call a function using new, what happens is JavaScript creates a new object and then calls your function with this set to that object.

This solution works because it first tests if this is window, and therefore, was called like $(). If it was called like $(), it will return new $(). Otherwise, it was called with new $(), and will work as expected.

Itin answered 21/5, 2011 at 22:47 Comment(3)
I understood your solution before your clarification, but they are nice notes regardless. Thank you.Beatification
The above works where you have control over how the constructor is called, but it can have undesirable consequences where the function might be called with some other object for this. It also means you can call that function without new and it may still be a constructor, but other functions that don't employ that pattern still require new. It also means you have to look at the code to determine if the function is a constructor or not.Horan
@Horan True, but using new in that instance doesn't hurt anything. If they know the function is a constructor then they can drop new. No harm done if they don't. Edit: I just realized that I didn't address your first statement. I only meant that you aren't penalized for using new.Beatification
H
4
>  $ = function() {
>     this.name = "levi";
> 
>     return this; };
> 
> console.log($());

We get DOMWindow instead of $

When you call $ as a funciton, then its this keyword is set to the global object, as it would be for any function called like that.

because of the quirky nature of this in JavaScript

Javascript's this keyword* works as it is specified to work. It is different to other languages, but that is how it works.

What's more strange to me is that console.log($().name) properly returns "levi".

When you call $ as a function, its this keyword is the global object, so:

this.name = 'levi';

creates a property of the global object called name with a value of 'levi'. Not strange when you know what's happening. :-)

We could just add new console.log(new $()) and it works.

That is how constructors are supposed to be called in javascript. When a function is called called with new, its this keyword is set to a newly constructed object, so this.name will create a new property of that object. Incidentally, return this is redundant, constructors return this by default.

> $ = function() {
>     var obj = function() {
>         this.name = "levi";
>     };
> 
>     return new obj(); };

console.log($()); Which gives me what I want, but it seems a bit unnecessary to wrap the object inside of a function which creates it. Further more, it's of type obj and not type $

Presumably you are using typeof, which can only return one of the values specified by ECMA-262. That short list (which includes object, number, string an so on) does not include $.

What are some other ways this can be done?

You can use the approach you already have discovered, Lasse Reichstein Nielsen's clone (also popularised by Doublas Crockford as "beget") and Richard Cornford's module pattern. Use Google, there are many, many posts about all of the above.

Horan answered 21/5, 2011 at 22:56 Comment(1)
If you create a property of this, then it will be assigned to whatever object this references. If you understand how this is set, then you know the object the property will be created on. That understanding wasn't clear in your post. If your question was really how do I use a function as a constructor without using 'new', then that is what you should have asked - I answered that question too. :-) Oh, this isn't bound to anything, it is set by the call.Horan
J
1

Try something like this:

function $(name){
    if( !(this instanceof arguments.callee) ){
        return new arguments.callee(name);
    }
    this.name = name;
    return this;
}

Update:

@Levi Morrison

Edit: Does anyone see anything wrong with the following solution:

Since you asked, I'm not exactly in love with document.write.
Try this instead:

var log = function(message){
    if(message){
        document.body.appendChild(document.createTextNode(message));
    }
    document.body.appendChild(document.createElement('br'));
}
Jaborandi answered 21/5, 2011 at 23:4 Comment(3)
JSLint says to avoid arguments.callee. This is easily solved by naming the function.Beatification
@Levi Morrison I only used arguments.callee to make it easier to rename the function. But I understand it's frowned upon.Jaborandi
I laughed at document.write() objection. I was only using them to show that the objects worked properly. They are not actually part of the code.Beatification

© 2022 - 2024 — McMap. All rights reserved.