Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JS and OOP: abuse of `that = this` pattern

Tags:

javascript

oop

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?

like image 815
nkint Avatar asked Jul 14 '14 21:07

nkint


4 Answers

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));
};
like image 114
Jerome WAGNER Avatar answered Nov 06 '22 19:11

Jerome WAGNER


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.

like image 30
Benjamin Gruenbaum Avatar answered Nov 06 '22 19:11

Benjamin Gruenbaum


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
like image 1
elclanrs Avatar answered Nov 06 '22 20:11

elclanrs


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.

like image 1
p e p Avatar answered Nov 06 '22 20:11

p e p