Search code examples
javascriptoopthisself

JavaScript OOP wrong _this value


Let's assume we have the following code:

var MyClass = (function(){
    var _this;

    function MyClass(inputVal){
        _this = this;
        this.value = inputVal;
    }

    MyClass.prototype.getValue = function(){
        return this.value;
    }

    MyClass.prototype.getValue2 = function(){
        return _this.value;
    }

    return MyClass;
})();

Let's make two instances of the class:

var instance1 = new MyClass(10);
var instance2 = new MyClass(20);

Now if we console.log() the values we see that:

instance1.getValue();   // 10
instance1.getValue2();  // 20

var MyClass = (function(){
    var _this;

    function MyClass(inputVal){
        _this = this;
        this.value = inputVal;
    }

    MyClass.prototype.getValue = function(){
        return this.value;
    }

    MyClass.prototype.getValue2 = function(){
        return _this.value;
    }

    return MyClass;
})();


var instance1 = new MyClass(10);
var instance2 = new MyClass(20);


console.log(instance1.getValue());
console.log(instance1.getValue2());

Why is that happening? It looks obviously that the _this variable gets the latest created instance properties. How to fix that? I need to keep a copy of this. Thanks!

Edit:

Here's the real situation

var HoverEffects = (function(){
    var _this;

    function HoverEffects($nav){
        _this = this;
        this._$activeNav = $nav.siblings('.active_nav');
        this._$hoverableLis = $nav.find('>li');
        this._$activeLi = $nav.find('>li.active');

        if(!$nav.length || !this._$hoverableLis.length || !this._$activeNav.length || !this._$activeLi.length) return;

        if(this._$activeNav.hasClass('bottom')){
            this._$activeNav.align = 'bottom';
            this._$activeLi.cssDefault = {
                left: this._$activeLi.position().left,
                width: this._$activeLi.width()
            };
        }
        else if(this._$activeNav.hasClass('left')){
            this._$activeNav.align = 'left';
            this._$activeLi.cssDefault = {
                top: this._$activeLi.position().top,
                height: this._$activeLi.height()
            };
        }
        else{
            return;
        }

        this._$hoverableLis.hover(
            function(){

                // How to set the correct this inside this function?
                if(this._$activeNav.align === 'bottom'){
                    this._$activeNav.css({
                        left: $(this).position().left,
                        width: $(this).width()
                    });
                }
                else if(this._$activeNav.align === 'left'){
                    this._$activeNav.css({
                        top: $(this).position().top,
                        height: $(this).height()
                    });
                }

            },

            function(){
                // Same here, wrong this
                this._$activeNav.css(this._$activeLi.cssDefault);
            }
        );
    }

    return HoverEffects;
})();

var sideNavHoverMagic = new HoverEffects($('#side-navigation'));
var primaryNavHoverMagic = new HoverEffects($('#primary-navigation'));

Solution

  • Why is that happening?

    Every time you call new MyClass, _this = this gets run. The second time overrides the first time.

    So _this refers to new MyClass(20), which means that when you call getValue2 from any MyClass instance, 20 will be returned because all MyClass instances are referring to the same _this value.


    Based on commentary on the Question:

    If you're attempting to pass a function bound to the appropriate context there are a variety of ways to make sure that this refers to the right object. Before continuing, please read "How does the 'this' keyword work?", because there's no reason for me to repeat all of it here.

    If you're binding event callbacks such as in a constructor:

    function Example(something) {
        something.addEventListener(..event.., this.callback, false);
    }
    Example.prototype.callback = function () {
        this.doStuff();
        this.doMoreStuff();
    };
    

    The callback will have the wrong this value because it's not being called as this.callback, it's just being called as:

    fn = this.callback;
    fn(); //no reference to this
    

    You can get around this in a number of ways.

    Function.prototype.bind

    You can bind the callback for every instance on their respective instance. This is very concise:

    function Example(something) {
        //generate a new callback function for each instance that will
        //always use its respective instance
        this.callback = this.callback.bind(this);
        something.addEventListener(..event.., this.callback, false);
    }
    Example.prototype.callback = function () {
        this.doStuff();
        this.doMoreStuff();
    };
    

    that = this

    You can create the callback (closure) within the constructor and reference a variable inside the constructor.

    function Example(something) {
        //every Example object has its own internal "that" object
        var that = this;
        this.callback = function () {
            //this function closes over "that"
            //every instance will have its own function rather than
            //a shared prototype function.
            that.doStuff();
            that.doMoreStuff();
        }
    
        something.addEventListener(..event.., this.callback, false);
    }
    

    () => {} (Fat Arrow Syntax)

    If you're using ES2015 you can use "fat arrow" syntax for creating lambdas that don't create a new context:

    function Example(something) {
        this.callback = () => {
            //the callback function doesn't create a new "this" context
            //so it referes to the "this" value from "Example"
            //every instance will have its own function rather than
            //a shared prototype function.
            that.doStuff();
            that.doMoreStuff();
        }
    
        something.addEventListener(..event.., this.callback, false);
    }