2

I have color stored in a data attribute on my button that I wanted to use in a toggle. However, when I tried to access the data information using this, no data was available. How can I keep my access to the correct this scope?

I was trying to only toggle the given color for the elements which didn't contain Skip.

html

<div>
 <input id="toggleButton" type="button" value="Toggle" data-color="Red" />
</div>
<div id="toggleSet">
<div>Element</div>
 <div>Skip</div>
 <div>Element</div>
</div>

css

.ActivateRed{ color: red; }

js

$('#toggleButton').click(function(){
 $("#toggleSet div").each(function(index,element){
  if( element.innerHTML != "Skip" ){
   $(element).toggleClass("Activate"+$(this).data("color"));
                                       //^this has no data to access?
                                       //Why am I getting undefined?
  }
 });
});

Here is a jsFiddle of my attempt. I keep getting Activateundefined as the class name. Why isn't this accessing my toggleButton's data?

6
  • 1
    I have seen a number of questions regarding a problem where saving the value of this is the solution to the problem. However, I could not find a canonical answer which explained what was wrong with using this in that fashion. Most answers were single line answers suggesting to store var that = this and move on. In order to try to reduce the amount of time people have to answer these types of questions I hoped to provide a canonical answer here. Please edit or request changes to it if anything is either incomplete or in need of correction.
    – Travis J
    Commented Jun 11, 2014 at 22:10
  • I though How to access the correct this / context inside a callback? was the (non-jQuery-specific) canonical question. Not sure whether it would help to merge them.
    – Bergi
    Commented Jun 11, 2014 at 22:21
  • @Bergi - I was trying to specifically address the jQuery aspect of how this is used with this type of structure. jQuery tends to obfuscate it a little and so I was attempting to explain the process involved. I think that the linked question from Felix addresses the non-jQuery aspect very well.
    – Travis J
    Commented Jun 11, 2014 at 22:30
  • @TravisJ - this in JavaScript changes in different locations. It can also be changed programmatically via .apply or .call. Commented Jun 11, 2014 at 23:03
  • @Derek朕會功夫 - You are correct. In fact, that is part of what jQuery obfuscates by design. Internally jQuery will call apply on callback functions in order to bind this to the current element. I outline this in the answer below.
    – Travis J
    Commented Jun 11, 2014 at 23:08

1 Answer 1

9

Explanation of the problem

this changed

The value of thisMDN has changed and is no longer referencing the expected element or value. Often this is because the scope has changed, and as a result so has the this reference.

this is contained in an execution context

The scope refers to the current Execution ContextECMA. In order to understand why this has changed, it is important to understand the way these execution contexts operate in JavaScript.

execution contexts bind this

When control enters an execution context (code is being executed in that scope) the environment for variables are setup (Lexical and Variable Environments - essentially this sets up an area for variables to enter which were already accessible, and an area for local variables to be stored), and the binding of this occurs.

the this binding was changed for the execution context

These contexts form a logical stack. The result is that contexts deeper in the stack have access to previous variables, but their bindings may have been altered. Every time jQuery calls a callback function, it alters the this binding by using applyMDN.

callback.apply( obj[ i ] )//where obj[i] is the current element

The result of calling apply is that inside of jQuery callback functions, this refers to the current element being used by the callback function.

For example, in .each, the callback function commonly used allows for .each(function(index,element){/*scope*/}). In that scope, this == element is true. The consequence of that, is if you expected a previous this to be available, it will be bound to a different element.

Brief explanation of this in jQuery callbacks

As shown above, jQuery callbacks use the apply function to bind the function being called with the current element. This element comes from the jQuery object's element array. Each jQuery object constructed contains an array of elements which match the selectorjQuery API that was used to instantiate the jQuery object.

$(selector) calls the jQuery function (remember that $ is a reference to jQuery, code: window.jQuery = window.$ = jQuery;). Internally, the jQuery function instantiates a function object. So while it may not be immediately obvious, using $() internally uses new jQuery(). Part of the construction of this jQuery object is to find all matches of the selector. The jQuery object then contains an array-like structure of the DOM elements matching the selector.

When a jQuery api function is called, it will internally iterate over this array-like structure. For each item in the array, it calls the callback function for the api, binding the callback's this to the current element. This call can be seen in the code snippet above where obj is the array-like structure, and i is the iterator used for the position in the array of the current element.

Finding a solution

It can be hard to determine what happened since jQuery tends to fail silently. .data()jQuery API is still a valid call here, it just returns undefined. As a result, the above code produces a class name of "Activate"+undefined, "Activateundefined".

The important part to recognize here is that jQuery has changed the this binding. In order to use a previous this binding, the value must be stored in a variable. A common name for storing this is that, self, me, or in best practice, an actual description of what this represents.

The reason saving the binding works is that the callback's execution context will be deeper in the execution context stack relative to where the binding value was saved, thus having access to that stored value.

jsFiddle Demo

$('#toggleButton').click(function(){
 var toggleBtn = this;
     //^ store this into the toggleBtn variable
 $("#toggleSet div").each(function(index,element){
                           //^ binds `this` to the current element
  if( element.innerHTML != "Skip" ){
       //^ we could have used `this.innerHTML` here
     $(element).toggleClass("Activate"+$(toggleBtn).data("color"));
                                        //^ re-use the stored `this` value
  }
 });
});

Not the answer you're looking for? Browse other questions tagged or ask your own question.