I've been editing this in response to comments below, but I will preserve the full revision history.
Referencing the function execution context via this
is fairly common in JavaScript. I suspect some people like it because they come from a language like Java, and it’s familiar. However, I would argue that it in some cases introduces brittleness into one's code without bringing much benefit.
For example, let’s consider a contrived logger module:
function CountingLogger() {
if (!(this instanceof CountingLogger)) return new CountingLogger;
this.count = 0;
}
CountingLogger.prototype.log = function (msg) {
console.log(this.count, msg);
this.count++;
}
It would be used as follows:
var logger = new CountingLogger();
logger.log('foo'); // => 1 foo
logger.log('bar'); // => 2 bar
That works fine. However, the problem is that by using this
, we’ve made an assumption about the way the method will be used. If the caller tries to use the method differently, they’re gonna have a bad time:
var logger = new CountingLogger();
['foo', 'bar'].forEach(logger.log);
// => undefined foo
// => undefined bar
Why is it acceptable to fail in this case? I would argue that it is not. We happen to not even be throwing an error, but we're violating the principle of least astonishment and requiring the user to understand implementation details of our module.
In some situations, this could cause an error:
this.someProperty.doSomething();
JavaScript doesn’t even provide a clear error message, like Error: This function requires a context that has property 'someProperty', but was called without one.
. Instead, the caller gets some random error at the first place that this
is required, and has to look into our code to figure out what’s going on. (Worse yet, imagine if someProperty
also existed on window
, which is what this
refers to if not explicitly specified - then we could get in to some truly confusing behavior.) This is not a good user experience for consumers of our module.
We can fix the above example by using bind()
(or an equivalent helper method for older browsers):
var logger = new CountingLogger();
['foo', 'bar'].forEach(logger.log.bind(logger));
// => 1 foo
// => 2 bar
This is fine, but why make our users go through this in the first place? We can easily refactor our module to use closures instead:
function createCountingLogger() {
var count = 0;
function log(msg) {
console.log(count, msg);
count++;
}
return {
log: log
};
}
Now the user is not affected by our implementation details:
var logger = createCountingLogger();
['foo', 'bar'].forEach(logger.log);
// => 1 foo
// => 2 bar
A common use of this
is to provide chaining, which can create nice APIs. Here is an example of the logger module that supports chaining:
function CountingLogger() {
this.count = 0;
this.log = function (msg) {
console.log(this.count, msg);
this.count++;
return this;
}
}
Chaining is fun:
var countingLogger = new CountingLogger();
countLogger
.log('foo') // => 1 foo
.log('bar'); // => 2 bar
We can easily use closures here as well to avoid having to use this
:
function createCountingLogger() {
var count = 0,
countingLogger = {};
function log(msg) {
console.log(count, msg);
count++;
return countingLogger;
}
countingLogger.log = log;
return countingLogger;
}
Now we get the benefits of a chained API with the robustness of this
-free code.
this
is generally treated as private, internal state, but it can be overridden with .call
, .apply
, or .bind
. If you intend for this to happen, then take whatever information this
is giving you as an (optional) argument. If you do not intend for this to happen, don’t rely on this
.
There are many good arguments for preferring composition over inheritance which I will not repeat here. That said, sometimes inheritance is truly what you need, in which case using this
may be the right path.
@getify provides an intriguing argument for delegation-oriented instead of class-oriented organization that ditches this
and may end up being less confusing.
There’s additional complexity generated to handle this
, such as q.ninvoke
, q.nbind
, and any time someone feels the need to write var self = this
. Is it really worth it?
Do you think this is ridiculous? Am I missing something? Let me know!
this
is fine and in many instances preferable to closures because prototypical programming can keep your code aligned further to the left and the state that you actually need in each function is much more explicit, narrowly-defined, and easier to refactor. For smallish things closures are great but I've found that when smallish closures grow into less smallish chunks of code it's usually much cleaner to refactor them into prototype classes.For a good example of how closures can be very confusing and unreadable (and why too many wall-of-text inline comments can really obscure the intent of some code) there's the json source.
You don't need to use
bind
to get the benefits of flexible callers, you can use this trick: https://github.com/substack/frequency-viewer/blob/master/index.js#L18inheritance is mostly a poor approach except when it isn't. A good example of when inheritance is a good idea is when you build an interface that conforms to a platform primitives such as streams or event emitters.
Every feature has some domain of applicability and every programmer has their own standard approaches for organizing functionality. It's too blunt to decry an approach universally without recognizing its appropriate domain of application. Sometimes that domain is very narrow but in the case of
this
and class prototypes, the approach can be useful and in some contexts more ideal than the alternatives.To address more concretely your example, you probably shouldn't do
this.log=...
in your constructor. Instead you could write:and then you avoid most of the downsides you talked about while getting 2 new upsides: removing one level of indentation and the ability to inherit when you really need to. For example to turn that example into an event emitter, you can just do:
versus the alternative in a closure:
I usually prefer the former prototype-based approach but it's really much more of an unimportant stylistic thing than some intrinsic airtight argument where one approach is always better than the other, although there are some cases in hot loops where for performance prototypes are more favorable. Everything is trade-offs.