Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

An Object that returns an instance of itself

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.

like image 523
Levi Morrison Avatar asked May 21 '11 22:05

Levi Morrison


4 Answers

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.

like image 89
KooiInc Avatar answered Sep 16 '22 11:09

KooiInc


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.

like image 40
icktoofay Avatar answered Sep 17 '22 11:09

icktoofay


>  $ = 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.

like image 41
RobG Avatar answered Sep 20 '22 11:09

RobG


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'));
}
like image 40
Zecc Avatar answered Sep 17 '22 11:09

Zecc