"this & object prototypes": node.js REPL environment differences
Just started reading, great book. I got to here: https://github.com/getify/You-Dont-Know-JS/blob/master/this%20&%20object%20prototypes/ch2.md#implicitly-lost
This snippet:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // function reference/alias!
var a = "oops, global"; // `a` also property on global object
bar(); // "oops, global"
When run in node.js, it logs undefined, see http://runnable.com/me/VHKV6afPSmV9xlkJ
When run in browser environment (tested in Chrome Version 41.0.2228.1 canary (64-bit) and http://repl.it/languages/JavaScript/), logs oops, global.
Does this have to do with node's lack of window object?
This is a little more odd than nodejs lacking the window object. A node process has a global variable named global. We can say this "replaces" window for our purposes here.
The crucial point lies in the difference between node running "as a REPL" vs running a specific file. Code run on runnable.com seems to run as input piped into a REPL.
I can use both "modes" by saving the code in a file test.js and running node test.js vs cat test.js | node. The first one has node running the file directly, the second one has a node REPL open up and pipe in the code through the use of cat.
We can now look if this is the global object available in both modes by putting this line into the foo() function:
console.log( this === global );
And well, both times it logs true, the global object exists and it is this in the context of the function call here. So what's the deal?
The deal is that when you open up node as a REPL -- just typing node without a file in a terminal will also do that -- the process will already have executed a bunch of functions to actually open up that REPL, and we are now executing inside of a function somewhere in that internal node REPL code. Thus, the code you are putting into the REPL does not run directly in the global scope, while code executed via node test.js does.
You can think of comparing it to these examples, saved as a js file and run inside a browser:
var a = 'a'; // `a` is now global, and assigned to the global object (usually `window`)!
vs
(function() { var a = 'a'; })() // `a` is available only inside this function's scope
What would make both behave the same would be
- using the much-feared implicit globals -- will do the same horrible thing in both cases now. Example:
a = 'oops, global';instead ofvar a ... - using Strict mode -- will throw an error in both cases now. Example:
"use strict";at the top of the file.
@pommesgabel wow thanks very much for the detailed explanation! Didn't know REPL mode made a difference. I'll leave this open in case the author decides to note this in the next revision of the book.
I think the explanation can be extended to help everyone understand what is happening when you run a script using the node CLI. Essentially node is requirying your script as a module with an empty exports object so all your code runs the same way it will run when you require another module in the global context of that module.
When you console.log this at the top level what you get is a reference to the module exports of the script you are running which is an empty object because nothing has been export (YET).
So if this is bind to an object how can you make the code to run properly under node.js and return a "2" as a result?
function foo() {
console.log(this);
}
var a = 2;
foo();
Well, is not that simple. At first you can change var a = 2 for this.a = 2
This will populate your empty runtime object with a new property called "a".
If you now console log(this) you will see it more clearly. In fact, you can go further and console.log your module doing console.log(module) and you will see that your "a" new property is sitting there within your exports.
But the code still don't work. Why?
Because foo() is running in the context of the global object and that's because it isn't part of the exports of the empty object. The main difference here from "global" to "window" in the browser is that your top level variables and functions aren't added as inner properties of your global object. So defining "var a = 3" won't let you gain access to it by using global.a or this.a much like you would be able to do this with the window object inside the browser. So foo() is still reading the global object of node when you try to access teh value of this.
How can you solve this? You have either three ways (or maybe more but those are the one I'm thinking right now)
- You can add foo as method of your module object and now a reference to this will be a reference to the object on where it is defined. So ...
this.a = 3;
this.foo = function () {
console.log(this.a);
};
this.foo() // 2
- You can bind foo to this after first assgining this.a to the value you want to store and retrieve it later. So in this case foo won't be part of your module exports but it will run in the context of it so that your this will be a pointer to that object. So
this.a = 3;
function foo() {
console.log(this.a);
}
foo.bind(this)(); //2
- You can assign the execution of foo (not the definition) to the default module exports. That way when the node CLI requires your module it will run it and get back the return value of your foo() function as a result. So ...
function foo() {
console.log(a);
}
var a = 3;
module.exports = foo();
Hope it helps the explanation.
I think the better solution would be to explain that instead of var a = 3, the equivalent in Node is now globalThis.a = 3, and then the rest of the code should work more as expected.