5

I'm relatively new to JS world.

I am used to write UI with QT (great tools to build UI!). With the QT I'm doing a class for each element: If I have a table with some elements I have a class for each element and a class for the table (maybe also for the rows). Each class contains the data and the method for manipulating its "DOM".

Now in html I'm using some ejs files that contains an empty skeleton on the divs and I'm writing a class for manipulating it via jquery.

For example:

userslist.ejs:

<script type="text/javascript">

function UsersList(path) {
    this.path = path;

    $('#userList > tbody').empty();
    this.loadAjax();
}

UsersList.prototype.loadAjax = function() {
    var that = this;
    $.getJSON(this.path, function(users) {
        that.fillUsers(users);
    });
};

UsersList.prototype.fillUsers = function(users) {
    var that = this;
    users.forEach(function(user){
        var tr = that.buildTr(user);
        $('#userList > tbody:last').append(tr);    
    });
};

UsersList.prototype.buildTr = function(user) {
    ...
};

</script>


<h1>Users list</h1>
<table id="userList">
    <thead>
        <tr>
            <th>#</th>
            <th>Name</th>
            <th>Control</th>
        </tr>
    </thead>
    <tbody></tbody>
</table>

With callback way of coding in JavaScript (and jquery dom manipulation in general) I easily lost the this keyword to refer to class methods and class property (even if I earn some control in the nested callback). And I'm find myself to really abuse of the var that = this; thing.. I have it in almost each method in the class.

How can I avoid this? I like OOP but I'm curious to explore this new trendy js, with some functional feature and with the asynch everywhere.

What is the correct way to handle this problem in JS? How can I avoid to write var that = this; in each method every time I use a callback?

nkint
  • 11,513
  • 31
  • 103
  • 174
  • 1
    it is pretty common to have closures like that when you want to register a function as an event handler because the context is usually modified. Some people use method.bind(this) to force the this inside the function to be equal to the this outside (so you don't need that). – Jerome WAGNER Jul 14 '14 at 21:39

5 Answers5

2

You can use .bind to preserve context. Newer versions of JavaScript (ES6) resolve this with new arrow notation. Moreover, some functions accept a context parameter.

Here is the ES5 solution:

UsersList.prototype.fillUsers = function(users) {
    users.forEach(function(user){
        var tr = this.buildTr(user);
        $('#userList > tbody:last').append(tr);    
    }, this); // note the context parameter
};

Here is the ES6 solution:

UsersList.prototype.fillUsers = function(users) {
    users.forEach((user) => { // arrow notation
        var tr = this.buildTr(user);
        $('#userList > tbody:last').append(tr);    
    }); 
};

Of course, the ES3 way would have been to use a normal for loop rather than a forEach.

As for $.getJSON jQuery's $.ajax also accepts a context parameter:

$.ajax({
  url: this.path,
  context: this
}).done(function(users) {
    this.fillUsers(users);
});

Or, with ES6 arrows:

$.getJSON(this.path).done((users) =>  this.fillUsers(users));

As you might have found out, "classical" oop doesn't look too great in JavaScript like that, and you might want to distribute where code is differently than you would in a classical language.

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • The ES6 stuff is only experimental though for now (just thought you should make that clear) – soktinpk Jul 14 '14 at 21:47
  • @soktinpk experimental? I've been using it successfully for some time now... it works just fine in Firefox, it's specced and agreed upon and compile to JS languages (like TypeScript) already support it ahead of time. Chrome is on the fast path to get ES6 arrows (getting there) too. You can also use traceur to compile ES6 to ES5 or ES3 today. – Benjamin Gruenbaum Jul 14 '14 at 21:48
  • Yes, it works in firefox... that's pretty much the only browser it works in by default. (In chrome, I think you have to turn on a special flag/setting). But it won't work in, even somewhat modern browsers, if you just put it in a ` – soktinpk Jul 14 '14 at 21:50
2

Very often when you use async programming in javascript (events, ajax) the context is the context of the event so you lose the context of your object.

the var that = this pattern, that you will even more often read as var self = this or var $this = this allow to add a closure in the methods in order to re-establish the context.

here is another method that will force the context to be this inside the object method when it will be called :

UsersList.prototype.loadAjax = function() {
    var that = this;
    $.getJSON(this.path, this.fillUsers.bind(this));
};
Jerome WAGNER
  • 21,986
  • 8
  • 62
  • 77
1

You could create a decorator that passes the context as an argument (ala Python), so you can access it within nested scopes. And also, you could return this for chaining:

function fluent(f) {
  return function() {
    f.apply(this, [this].concat([].slice.call(arguments)))
    return this
  }
}

function Ary(xs) {
  this.xs = xs
}

Ary.prototype.method = fluent(function(self, a, b) {
  self.xs.forEach(function(x) {
    console.log(self.xs, x + a + b)
  })
})

var a = new Ary([1,2,3])

a.method(1, 2)
//[1, 2, 3] 4
//[1, 2, 3] 5
//[1, 2, 3] 6
elclanrs
  • 92,861
  • 21
  • 134
  • 171
1

One alternative to the other answers is to use jQuery's .proxy. An example:

UsersList.prototype.loadAjax = function() {
    $.getJSON(this.path, $.proxy(function(users) {
        //jQuery.proxy ensures that 'this' referring to the object you intend to refer to, i.e. the object passed in to the second argument of .proxy
        this.fillUsers(users);
    }), this);  //Note we pass 'this' to .proxy
};

I think this similar question helps to illustrate: $(this) inside of AJAX success not working.

Community
  • 1
  • 1
p e p
  • 6,593
  • 2
  • 23
  • 32
1

Honestly I wouldn't try to avoid this approach. Maybe you could use .bind(...), .call(...) or .apply(...), or over-engineering your code to get rid of coding var that = this;.

But, after all, var that = this performs better than other solutions, since you work with clousures and there's no additional overhead to just access to the this reference representing your current object.

Best argument is assume JavaScript works this way and I'm very sure that this will be the less important problem in your projects.

You said:

What is the correct way to handle this problem in JS? How can I avoid to write var that = this; in each method every time I use a callback?

The answer is you're doing in the right way. Anyway, I would take a look at bind, call and apply functions, because there're some cases where they work better than var that = this;.

Matías Fidemraizer
  • 63,804
  • 18
  • 124
  • 206