Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jQuery and "this" management? How to avoid variable collisions?

When you are writing complex jQuery/javascript, how do you manage using this without re-defining this variables that were previously defined? Do you have a rule of thumb or a personal preference for naming your this variables (as the nesting gets deeper)?

There are times where I want variables from a higher scope to be available to nested functions/callbacks, but then there are times when I'd like to have a clean slate/scope; is there a good way to call functions/callbacks without having to worry about variable collisions? If so, what technique(s) do you use?


Some super silly test code:

$(document).ready(function() {

    console.warn('start');

    var $this = $(this),

    $dog = $('#dog'),

    billy = function() {

        console.log('BILLY!', 'THIS:', $this, ' | ', 'DOG:', $dog);

        var $this = $(this);

        console.log('BILLY!', 'THIS:', $this, ' | ', 'DOG:', $dog);

    };

             // (#1)
    billy(); // BILLY! THIS: undefined | DOG: jQuery(p#dog)
             // BILLY! THIS: jQuery(Window /demos/this/) | DOG: jQuery(p#dog)

    console.log('THIS:', $this, ' | ', 'DOG:', $dog); // THIS: jQuery(Document /demos/this/) | DOG: jQuery(p#dog)

             // (#2)
    billy(); // BILLY! THIS: undefined | DOG: jQuery(p#dog)
             // BILLY! THIS:  jQuery(Window /demos/this/) | DOG: jQuery(p#dog)

    $('#foo').slideUp(function() {

                                                          // (#3)
        console.log('THIS:', $this, ' | ', 'DOG:', $dog); // BILLY! THIS: undefined | DOG: jQuery(p#dog)

        var $this = $(this); // (#10)

                                                          // (#4)
        console.log('THIS:', $this, ' | ', 'DOG:', $dog); // BILLY! THIS: jQuery(Window /demos/this/) | DOG: jQuery(p#dog)

    });

    $('#clickme').click(function() {

                                                          // (#5)
        console.log('THIS:', $this, ' | ', 'DOG:', $dog); // THIS: undefined | DOG: jQuery(p#dog)

        var $this = $(this);

                                                          // (#6)
        console.log('THIS:', $this, ' | ', 'DOG:', $dog); // THIS: jQuery(button#clickme) | DOG: jQuery(p#dog)

        $('#what').animate({
            opacity : 0.25,
            left    : '+=50',
            height  : 'toggle'
        }, 500, function() {

                                                              // (#7)
            console.log('THIS:', $this, ' | ', 'DOG:', $dog); // THIS: undefined | DOG: jQuery(p#dog)

            var $this = $(this);

                                                              // (#8)
            console.log('THIS:', $this, ' | ', 'DOG:', $dog); // THIS: jQuery(div#what) | DOG: jQuery(p#dog)

        });

    });

             // (#9)
    billy(); // THIS: undefined | DOG: jQuery(p#dog)
             // THIS: jQuery(div#foo) | DOG: jQuery(p#dog)

    console.warn('finish');

});

A full demo page can be found here (jsbin.com).

Note: As you can see, I've "marked" the comments with numbers (#XX) for easy reference.


Observation 1:

Marker (#1)

BILLY! THIS: undefined | DOG: jQuery(p#dog)

Rhetorical question: Why is $this undefined, but $dog is accessible?

Answer: Because the var within that scope is re-defining $this; it's just that I'm trying to log $this before its been defined within that scope.

If I comment out var $this = $(this);, then the marker (#1) returns:

BILLY! THIS: jQuery(Document index2.html) | DOG: jQuery(p#dog)
BILLY! THIS: jQuery(Document index2.html) | DOG: jQuery(p#dog)

The same logic applies to markers (#2), (#3), (#4), (#5), (#6), (#7) and (#8).

Based on this observation (and please correct me if I'm wrong here) I'm assuming that I could put var $this = $(this); at the bottom of the function, and the current scope would know that I want to use the current scope's $this (even though it's not defined yet), and not the parent's $this (even though it IS defined).

POSSIBLE SOLUTION TO AVOIDING $this CONFLICTS:

If one wants to cache $(this) outside/within other closures/functions/callbacks and avoid collisions, then one should use different variable labels like these (for example):

var $$ = $(this);
var $this2 = $(this);
var $t = $(this);
var $that = $(this);

QUESTION:

Is the solution above how you would avoid $this collisions? If not, what's your prefered technique?


Observation 2:

Marker (#9)

THIS: undefined | DOG: jQuery(p#dog)

... $this is undefined for reasons mentioned above, but:

THIS: jQuery(div#foo) | DOG: jQuery(p#dog)

... $this is now $('#foo')!

QUESTION(S):

Exactly why did this happen?

Is it because $this was re-defined via marker (#10)?

(Hmmm, I feel like I need to Google "garbage collection in javascript".)

Again, when writing complex jquery/javascript, what's the best way to avoid this type of variable collision?


I hope these aren't horrible questions. Thanks in advance for taking the time to help me out. :)

like image 410
mhulse Avatar asked Feb 14 '13 21:02

mhulse


2 Answers

You're actually running into variable hoisting issue:

billy = function() {

    console.log('BILLY!', 'THIS:', $this, ' | ', 'DOG:', $dog);

    var $this = $(this);

    console.log('BILLY!', 'THIS:', $this, ' | ', 'DOG:', $dog);

};

is actually interpreted at runtime like this:

billy = function() {

    var $this;

    console.log('BILLY!', 'THIS:', $this, ' | ', 'DOG:', $dog); // $this hasn't been set to anything yet...

    $this = $(this);

    console.log('BILLY!', 'THIS:', $this, ' | ', 'DOG:', $dog);

};

JavaScript hoists variable and function declarations to the top of the scope block so you can reference them before manually declaring them. This gets people into trouble when they reference vars of the same name from outside of the scope block, as you can clearly see in your example.

like image 129
AlienWebguy Avatar answered Sep 25 '22 09:09

AlienWebguy


You've just encountered the joys of scope and JavaScript. One thing a lot of people like to do is, if you need access to the current scope in a callback, define this as self, like so:

var $ = require('jquery')
  , database = require('database')

$.on('click', function(action) {
  var self = this;
  database.connect(function() {
    database.putSync(self.action.button);
  });
});

Note that when 'connect' is called, the scope of "this" is reset to that of the callback and not the click-handler, which is why you've stored your scope in an accessible variable.

like image 21
sent1nel Avatar answered Sep 25 '22 09:09

sent1nel