John Russell Blog

JavaScript Implicit and Explicit Context

Several years ago I was having a conversation with a friend about the future of the web and if it involved Abobe Flash or if it would eventually be replaced by HTML5 / JavaScript / CSS. The argument between the two went many different directions, however by the end of the conversation it was obvious that regardless of which remained dominant I really needed to learn more about HTML5, JavaScript, and CSS.

Coming from a background in Flash (ActionScript) the part that intrigued me most was JavaScript, specifically using it as a OOP language. I read several books, scoured the web for articles and tutorials, and quickly felt that I had a decent handle on how it was commonly used. As I continued to experiment with increasingly complex interface designs and coding techniques I ran into the same problem time and time again. The context of the keyword this was very inconsistent and at times not at all what I expected it to be. If you’ve ever used this in conjunction with an event listener or timeout callback function to reference a dynamically created variable or an object property then it’s quite likely that you too have had the pleasure of seeing how JavaScript handles implicit references. Let’s take a look at some code to see exactly what is going on.

In the below example we’ll create a simple list of collapsible paragraphs.

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>laubsterboy.com Example</title>
    <style type="text/css">
        .collapsibleContent {display:none;}
    </style>
</head>
<body>
    <a href="#" class="collapsibleTitle">Title 1</a>
    <p class="collapsibleContent">The first short paragraph example.</p>
    <a href="#" class="collapsibleTitle">Title 2</a>
    <p class="collapsibleContent">The second short paragraph example.</p>
    <a href="#" class="collapsibleTitle">Title 3</a>
    <p class="collapsibleContent">The third short paragraph example.</p>

    <script type="text/javascript">
        (function (window) {
            function Collapsible(title, content) {
                this.title = title;
                this.content = content;
                this.collapsed = true;
                //
                this.title.addEventListener("click", this.titleClickListener, false);
            }
            Collapsible.prototype.titleClickListener = function (Event) {
                if (this.collapsed) {
                    this.expand();
                } else {
                    this.collapse();
                }
            }
            Collapsible.prototype.expand = function() {
                this.collapsed = false;
                this.content.style.display = "block";
            }
            Collapsible.prototype.collapse = function() {
                this.collapsed = true;
                this.content.style.display = "none";
            }

            var init = (function () {
                var anchors = document.body.getElementsByTagName("a");
                var paragraphs = document.body.getElementsByTagName("p");
                //
                var titles = new Array();
                var contents = new Array();
                //
                for (var i = 0; i < anchors.length; i++) {
                    if (anchors[i].className == "collapsibleTitle") {
                        titles.push(anchors[i]);
                    }
                }
                for (var i = 0; i < paragraphs.length; i++) {
                    if (paragraphs[i].className == "collapsibleContent") {
                        contents.push(paragraphs[i]);
                    }
                }
                for (var i = 0; i < titles.length; i++) {
                    var tempCollapsible = new Collapsible(titles[i], contents[i]);
                }
            })();
        })(window);
    </script>
</body>
</html>

In the above example we’ve created the Collapsible class which contains references to the pairs of titles and content containers, and added a click event listener to each of the titles. The problem with the above example right now is that the keyword this within the scope of the titleClickListener function refers to the collapsibleTitle anchor element rather than the instance of the Collapsible object. The click event listener is working properly, except that the listener is only operating within the scope of the collapsibleTitle anchor element and thus it doesn’t have access to any of the Collapsible object properties or prototype functions.

One way to resolve this problem is to bring the click event listener function into the scope of the Collapsible class constructor and provide it with a reference to the scope of the Collapsible class.

function Collapsible(title, content) {
    this.title = title;
    this.content = content;
    this.collapsed = true;
    var self = this; // create a variable that contains a reference to the keyword this
    this.title.addEventListener("click", function (Event) {
        if (self.collapsed) {
            self.expand();
        } else {
            self.collapse();
        }
    }, false);
}

Problem solved! Now when you click on any of the titles the corresponding content will collapse or expand. However, there are a few things left to be desired with this approach. First, the code is much more difficult to read and understand, mostly due to the self keywords and functions nested within functions. Second, the titleClickListener function is no longer a prototype of the Collapsible class but rather a property, so it is no longer shared by each Collapsible instance object which uses more memory and in larger projects would slow performance. Lastly, since the function is declared locally within the scope of the addEventListener call there is no way for it to be shared with other events if it were ever necessary.

Since the root problem is that the context of the keyword this is being implicitly set by JavaScript and changing within the scope of the titleClickListener, a better approach would be to explicitly set the context of the keyword this when calling titleClickListener. This would mean that the keyword this could reference the Collapsible instance object rather than the collapsibleTitle anchor element. Thankfully there are two methods for doing this within JavaScript, Call and Apply. Call accepts an argument list while Apply accepts an argument array.

So, let’s incorporate the Call method into our code to see just how this works. If we revert back to the first code example and replace line 25 with the code below, everything should be working properly.

this.title.addEventListener("click", function() { self.titleClickListener.call(self, arguments)}, false);

One last adjustment that we could make is to add a delegate prototype function to the Collapsible class. This could be invoked each time the context of an object needs to be adjusted when calling a function.

Collapsible.prototype.delegate = function (fnc, obj) { var that = obj; return function () { fnc.call(that, arguments); } }

Once the delegate function is added we can now make our last adjustment to the addEventListener call.

this.title.addEventListener("click", this.delegate(this.titleClickListener, this), false);

That’s it! The delegate call will return a function that will call this.titleClickListener with the context of this, which is referencing the Collapsible object.

Learning about and understanding these two methods have had a dramatic impact on what I’ve been capable of accomplishing with JavaScript, taking my skill to an entirely new level, and I genuinely hope that this post has helped you to learn something new or at least inspire you to do some research and studying of your own.


Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *