Last active
April 27, 2018 13:39
-
-
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".
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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