Skip to content

Instantly share code, notes, and snippets.

@RobertFischer
Last active April 27, 2018 13:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RobertFischer/50de3993bce3e6382abfb8f21a4a2527 to your computer and use it in GitHub Desktop.
Save RobertFischer/50de3993bce3e6382abfb8f21a4a2527 to your computer and use it in GitHub Desktop.
The Mystery of the Vanishing Self: A deep dive into a JavaScript functional programming gotcha and the meaning of "this".
"use strict";
import Promise from 'bluebird';
import _ from 'lodash';
Promise.resolve({a:'a',b:2,c:True}).then(_.values); // This will work, returning ['a',2,True] (or some permutation thereof)
// Now let's create our own object implementing _.values(obj).
class Valuator {
values(obj) {
return this._Values(obj);
}
_Values(obj) { // Just call the lodash impl
return _.values(obj);
}
}
// Now let's do exactly what we were doing above, but with our own implementation.
const valuator = new Valuator();
Promise.resolve({a:'a',b:2,c:True}).then(valuator.values); // This will fail with a dereference-undefined error
Promise.resolve({a:'a',b:2,c:True}).then(obj => valuator.values(obj)); // But this will work as above!
// The reason why it will fail is because of the context of "this".
// To understand why, let's look at exactly what's going on in the two cases above.
// First, let's see how it works when it wworks.
new Valuator().values(obj)
// In this case, we're instantiating the Valuator, and then calling the method "values" on
// that object, passing the value of "obj" into that method. The method will have "this" set
// to the newly-instantiated Valuator, so we then execute this code:
return this._Values(obj);
// That's calling the method "_Values" on the "this" object (which is set to the
// newly-instantiated Valuator), passing in the value of "obj". And then we're good.
// Now let's see the degenerate case.
new Valuator().values
// In this case, we are insantiating the Valuator, and then returning the value of the "values"
// field attached to that object. The value of that field is a function. To see that, let's look
// at the code in the Node.js REPL:
// > new Valuator().values
// [Function: values]
// > typeof new Valuator().values
// 'function'
// So, we're now taking that function and executing it as a function (not a method). So we execute
// that function, meaning that we execute this code:
return this._Values(obj);
// But "this" is "undefined", because we are executing a function, not a method. And so we can't
// dereference "undefined", and so it explodes.
// JavaScript only gives you "this" when you are calling a function as an object dereference --
// that is, when you are doing foo.bar(). So, if you extract the function "foo.bar" and then call "bar()"
// on its own, you no longer have "this". (In non-strict JS, you actually have the function itself
// or the global object as "this", depending on where it is defined and called. This makes the
// situation much worse, not better, and it is one of the nicest things that "use strict" fixes for you.)
// This issue with losing "this" is exactly why you used to see a lot of code that looked like:
foo.bar.bind(foo);
// What that is doing is getting the "bar" function off of "foo", and then fixing the "this" value
// for that function to be "foo". This code has been replaced by the more modern JavaScript:
(args...) => foo.bar(args...);
// ...which, when you know the arguments (let's say, 2), looks like this:
(thing1,thing2) => foo.bar(thing1,thing2);
// The other interesting implication (and the reason for this odd behavior, insofar as it can be
// construed as "reasonable") is that you can move methods between objects, and as long as they
// implement the necessary methods, the method calls will resolve properly. eg:
class Foo {
print() { console.log("From Foo") }
doIt() { this.print(); }
}
class Bar {
print() { console.log("From Bar") }
}
const foo = new Foo();
const bar = new Bar();
bar.doIt = foo.doIt;
foo.doIt(); // Logs "From Foo" to the console.
bar.doIt(); // Logs "From Bar" to the console.
// Grokking why this works is left as an exercise to the reader. For extra credit, understand this behavior:
bar.doIt = foo.doIt.bind(foo);
bar.doIt(); // Logs "From Foo" to the console.
// But wait! What about the Lodash version? That's extracting "values" off of "_" and then calling the
// extracted function as a function, not a method. Why doesn't that explode?
//
// It's because of the very careful implementation by Lodash to enable the usage that we have above.
// To see that implementation, let's take a look at the source:
// https://github.com/lodash/lodash/blob/4.17.10/lodash.js#L13927
function values(object) {
return object == null ? [] : baseValues(object, keys(object));
}
// Note the careful lack of "this". Lodash calls all of its methods as functions, not as methods. This means
// that it doesn't care what "this" is assigned to, and it also means that any Lodash function can be passed
// around without having to do the .bind() or anonymous function trick.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment