It's easy to trip up on the meaning of this
in JavaScript. The behavior is very different from other languages, which means we have to throw most preconceptions and intuition out the window.
The best way to think of this
in JS is as a hidden argument which is passed in a slightly awkward way. Instead of the normal passing of arguments:
fn(arg1, arg2, arg3)
this
is passed as whatever is left of the dot when the function is called.
leftOfTheDot.fn(arg1, arg2)
In this last example this
will become leftOfTheDot
inside the body of the function.
But what if nothing is left of the dot?
fn(arg1, arg2);
Then this
refers to the global object inside the body of the function (global
in node, window
in browsers). (undefined
in strict mode)
Every function has its own method named call
which also allows you to specify the value of the this
argument. When call
is used, the first argument becomes this
, the 2nd becomes the 1st argument and so on i.e. fn.call(thisArg, firstArg, secondArg, ...)
.
This means we can translate (desugar) all of the above examples to the call
method:
leftOfTheDot.fn(arg1, arg2)
desugars to the following call
version:
leftOfTheDot.fn.call(leftOfTeDot, arg1, arg2)
On the other hand, the next example
fn(arg1, arg2);
desugars to
fn.call(window, arg1, arg2)
- in browsersfn.call(global, arg1, arg2)
- in nodefn.call(undefined, arg1, arg2)
- everywhere when in strict mode ("use strict")
Consider this example:
var o1 = {identify: 'o1'},
o2 = {identify: 'o2'};
o1.fn = function() {
return "I am " + this.identify;
};
o2.fn = o1.fn;
var detached = o2.fn;
console.log(o1.fn() + " and " + o2.fn() + " and " + detached());
// Output:
// I am o1 and I am o2 and I am undefined
If we desugar the calls in the console.log line, everything becomes clear:
console.log(o1.fn.call(o1), o2.fn.call(o2), detached.call(undefined))
This example shows that it doesn't matter where the function is attached to when it's passed around. The only place where it matters is when it's being called. Therefore, at the call sites (inside console.log
), this
= whatever is left of the dot, even though the function was originally attached to o1
. Its the exact same function, called with a different this
.
What happens with callbacks? Say we have the following
fs.readFile(this.file, 'utf8', function callback(err, data) {
this.data = data;
});
On the receiving end, node gets our callback as an argument and then calls it directly:
function readFile(file, opt, callback) {
// do the work
callback(null, result); // if no errors found.
}
This is the call site of the callback. There is nothing on the left of the dot when node calls that callback. Which means, this
is the global object.
If we desugar that, we get callback.call(global, null, result)
. Its now obvious why readFile
doesn't have access to this
. It was never passed to it as an argument, so it can't even use callback.call
to pass it back to the callback function.
We have a couple of options:
-
Use a temporary variable, creating a closure:
var self = this; fs.readFile(this.file, 'utf8', function callback(err, res) { self.data = data; });
-
Bind the callback function
fs.readFile(this.file, 'utf8', bind(function callback(err, res) { this.data = data; }, this));
Bind is a function that creates a new function where the
this
argument is fixed to a certain value. Here is one possible implementation ofbind
:function bind(fn, object) { return function bound() { return fn.apply(object, arguments); } }
note 1:
fn.apply(object)
callsfn
as if it were attached to theobject
. Basicallyfn.apply(object, [arg1, arg2, arg3])
desugars to
fn.call(object, arg1, arg2, arg3);
note 2:
arguments
is an array-like hidden variable which refers to all the arguments in the current function - in this case, the returnedbound
function. For example, if we call the bound function with 3 strings:bound('s1', 's2', 's3');
then inside
bound()
,arguments
is an array-like object which has{ length: 3, 0: 's1', 1: 's2', 2: 's3' }
To enable prototypes to work their magic. Here is an example:
var o = function(id) {
this.identity = id;
};
o.prototype.fn = function() {
return "I am " + this.identity;
};
var o1 = new o('o1'), o2 = new o('o2');
console.log(o1.fn() + " and " + o2.fn());
// Output: I am o1 and I am o2
When we construct objects with new o(...)
, a hidden property is attached to them called __proto__
and it's made to point to o.prototype
. In this example, both o1
and o2
have __proto__
properties which point to o.prototype
.
Then when we try to access a property of an object it it will first be looked up inside the object, then inside that object's prototype. For example, trying to access o1.fn
will result with the following lookup:
- check inside o1 (not found)
- check inside
o1.__proto__
, which is a reference too.prototype
(found) - use
o.prototype.fn
However we don't want to call the function as if it were attached to the object o.prototype
. We want it to be called as if it were attached to the object o1
. JavaScript takes whatever is left of the dot at the call site, the place where we actually call the function. That is why this
behaves as an additional argument, passed when calling the function, with a slightly awkward syntax - being passed left of the dot.
Lets apply that logic for both objects:
-
In
o1.fn()
, the functiono.prototype.fn
is called witho1
left of the dot -
meaning,
o1.prototype.fn
is called with the argumentthis
set too1
-
therefore,
this.identity == 'o1'
, and the return result is'I am o1'
-
In
o2.fn()
, the functiono.prototype.fn
is called witho2
left of the dot -
meaning,
o1.prototype.fn
is called with the argumentthis
set too2
-
therefore,
this.identity == 'o2'
, and the return result is'I am o2'
Indeed. But ES6 will improve things a lot:
-
We're going to get classes, so we wont have to muck around with attaching things to the prototype object and creating prototype chains that emulate classical inheritance ourselves, manually.
Don't worry, prototypes will still be at work under the hood. Most of this is just sugar.
-
We'll be able to use a shortcut to write bound functions:
fs.readFile(this.file, 'utf8', (err, res) => { this.data = res; });
-
Instead of the magical
arguments
variable, we'll get "rest" arguments - a way to expess that we want the rest of the function arguments to be represented by an array:function bind(fn, object) { return function bound(...args) { return fn.apply(object, args); } }
Can't wait!
In ES7, if we get the bind operator, we'll also be able to bind any function to any object:
object::fn
get a bound version of the function attached to the object
::object.fn
and call any function on any object with a short and sweet syntax:
var {lmap, lreduce} = require('lodash');
array::lmap(...)::lreduce(...);