Confusion with this and Globals in Node.js - egnomerator/misc GitHub Wiki
- Node.js
- Node.js Module
-
Top-Level Scope: Browsers vs. Node.js
- This documentation explains the difference between a node module's top-level scope versus the top-level scope in a browser
-
Node.js Module Wrapper
- This documentation explains the module wrapper, the module object, and the module.exports object
- The Node REPL
-
Activation Object and Variable Object
- This is a link to another page within this github wiki
- It might be helpful to have it open in another tab
-
Keyword this
- This is a link to another page within this github wiki
- It might be helpful to have it open in another tab
Given a sample of code demonstrating general behavior of the this
keyword in JavaScript, unexpected output was produced
Note: It might be helpful to have read the other page Keyword this
- The code in example.js is copied from Example 1 in Keyword this (added line numbers)
1 // illustrates default binding and implicit binding
2
3 function foo(){
4 console.log(this.bar);
5 }
6
7 var bar = "bar";
8 var o2 = { bar: "bar2", foo: foo };
9 var o3 = { bar: "bar3", foo: foo };
10
11 foo(); // "bar" (undefined if in strict mode) <-- this is default binding
12 o2.foo(); // "bar2" <-- this is implicit binding
13 o3.foo(); // "bar3" <-- this is implicit binding
- Per line 11 comment, output was expected to be "bar". However, when executed in Node.js, output was
undefined
- The default binding mechanism IS applying as expected
- Therefore, when the code within
foo()
is executing due to the call tofoo()
on line 11,this
is pointing to the global object
- Therefore, when the code within
- On line 7, where
bar
is declared,bar
becomes attached to the Node.js module wrapper'sVariable Object
rather than the global object (see Activation Object and Variable Object)- This is due to the way Node.js handles globals in modules (see Node.js Module Wrapper)
- This means that when
foo()
(called on line 11) tries to accessthis.bar
, the global object does not contain a propertybar
, because the declaration ofbar
attachedbar
to the module wrapper's variable object
Note: In example.js, if bar
were declared without the var
keyword, bar
would be attached to the global object resulting in the expected output
- The comment on this line indicates that the call to
foo()
will output "bar" - The comment on this line also indicates that the output would be
undefined
if instrict mode
-
The line 11 comment is not arbitrary; it is an expectation based on the behavior of the
this
keyword mechanism in JavaScript -
Specifically, the expectation is based on the default binding mechanism--the 4th rule covered in Keyword this which says,
Default Binding Rule
- If none of the above rules apply, then
this
will point to the global object by default - UNLESS in strict mode which will cause
this
to be undefined
- If none of the above rules apply, then
-
So, based on the above information, we expect line 11 to output "bar"
- ... and that IS the output IF the code is ran in a browser (or if the code is ran in the Node REPL) ...
... well, if we run the example file in Node.js (by calling node example.js
from a command shell or by requiring example.js in another file we run), line 11 produces undefined
as the output.
What is going on?
- The default binding rule does apply in example.js (if this is not clear, read Keyword this)
- At this point, it seems that Node.js might default to
strict mode
(which would explain the output ofundefined
) ... is this true? No, Node.js does not default tostrict mode
.
What we know at this point:
- We know that the default binding rule applies
- We know that we are not in
strict mode
- So, we know that when
foo()
is called from line 11, and the code withinfoo()
is being executed,this
refers to the global object
... And yet we are seeing the output of undefined
- So, this must mean that, contrary to expectations,
bar
is not attached to the global object
- Question: since,
bar
is not attached to the global object like we expected, what isbar
attached to? - Answer:
bar
is attached to the Variable object of the previous execution context
-
Variable Object: see Activation Object and Variable Object
- Every execution context has an internal container object, a Variable object, which holds the variables local to that execution context
-
Activation Object: see Activation Object and Variable Object
- Whenever a function is called, an Activation object is created as the function's execution context; this object is used as the Variable object for that function's execution context
-
Node.js Module: see Node.js Module
- Basically, a Node.js module is a file containing code. (e.g. our example.js file)
- (it's not just a file; there's more to this in the documentation)
-
Node.js Module Wrapper: see Node.js Module Wrapper
-
Every module's code is automatically wrapped in a function called a module wrapper for scoping purposes
the Node.js module wrapper
(function (exports, require, module, __filename, __dirname) { // Your module code actually lives in here });
-
-
Top-Level Scope: see Top-Level Scope: Browsers vs. Node.js
- In short, top-level scope is the outer-most scope (scopes are nested) in the JavaScript scope chain
- Node.js's top-level scope is the module (i.e. the scope of the module wrapper function) rather than the global object
In browsers (and in the Node REPL), the top-level scope is the global scope.
But in a Node.js module, the top-level scope is the module, thanks to the Node.js module wrapper function. What does that mean ("the top-level scope is the module")?
Remember, every function in JavaScript has its own execution context (i.e. its Activation object) and this Activation object is used as the Variable object for that function, the Node.js module wrapper function is no exception. When Node.js loads a module (e.g. when you execute a file from a command shell calling node example.js
) Node.js wraps the module in a module wrapper function. When that module wrapper function is called, the Activation object is created. Then, that Activation object is used as the Variable object for that module wrapper function's execution context; so, the arguments passed to the module wrapper function including the loaded module (i.e. your file) are attached to this Variable object.
To summarize, the module wrapper's Variable object (rather than the global object) is the top-level scope--this is what is meant by the Node.js documentation (Node.js Module Wrapper) that says,
... It [the module wrapper] keeps top-level variables (defined with
var
,const
orlet
) scoped to the module rather than the global object.
So, after foo()
is called on line 11, and the code within foo()
is being executed, this
points to the global object as expected (per the JavaScript default binding rule); but, the global object does not contain bar
. In fact, bar
is contained by the Variable object of the module wrapper. So, the call to this.bar
in foo()
returns undefined
.
Since, the top-level scope in a Node.js module is the module itself rather than the global scope, variables declared in the top-level scope of a Node.js module will not be accessible via the global object. This explains why, when we run example.js in Node.js, the output for this.bar
is undefined
.
Note: As mentioned in the "Quick Explanation" above, in example.js, if bar
were declared without the var
keyword, bar
would be attached to the global object resulting in the expected output