- The Old Way of Doing Things
- The Node.js Way of Doing Things
- Error Handling and Asynchronous Functions
- Who Am I? Maintaining a Sense of Identity
- Being Polite--Learning to Give Up Control
- Synchronous Function Calls
- Summary
Who Am I? Maintaining a Sense of Identity
Now you’re ready to write a little class to help you with some common file operations:
var fs = require('fs'); function FileObject () { this.filename = ''; this.file_exists = function (callback) { console.log("About to open: " + this.filename); fs.open(this.filename, 'r', function (err, handle) { if (err) { console.log("Can't open: " + this.filename); callback(err); return; } fs.close(handle, function () { }); callback(null, true); }); }; }
You have currently added one property, filename, and a single method, file_exists. This method does the following:
It tries to open the file specified in the filename property read-only.
If the file doesn’t exist, it prints a message and calls the callback function with the error info.
If the file does exist, it calls the callback function indicating success.
Now, run this class with the following code:
var fo = new FileObject(); fo.filename = "file_that_does_not_exist"; fo.file_exists((err, results) => { if (err) { console.log("\nError opening file: " + JSON.stringify(err)); return; } console.log("file exists!!!"); });
You might expect the following output:
About to open: file_that_does_not_exist Can't open: file_that_does_not_exist
But, in fact, you see this:
About to open: file_that_does_not_exist Can't open: undefined
What happened? Most of the time, when you have a function nested within another, it inherits the scope of its parent/host function and should have access to all the same variables. So why does the nested callback function not get the correct value for the filename property?
The problem lies with the this keyword and asynchronous callback functions. Don’t forget that when you call a function like fs.open, it initializes itself, calls the underlying operating system function (in this case to open a file), and places the provided callback function on the event queue. Execution immediately returns to the FileObject#file_exists function, and then you exit. When the fs.open function completes its work and Node runs the callback, you no longer have the context of the FileObject class any more, and the callback function is given a new this pointer representing some other execution context!
The bad news is that you have, indeed, lost your this pointer referring to the FileObject class. The good news is that the callback function for fs.open does still have its function scope. A common solution to this problem is to “save” the disappearing this pointer in a variable called self or me or something similar. Now rewrite the file_exists function to take advantage of this:
this.file_exists = function (callback) { var self = this; console.log("About to open: " + self.filename); fs.open(this.filename, 'r', function (err, handle) { if (err) { console.log("Can't open: " + self.filename); callback(err); return; } fs.close(handle, function () { }); callback(null, true); }); };
Because local function scope is preserved via closures, the new self variable is maintained for you even when your callback is executed asynchronously later by Node.js. You will make extensive use of this in all your applications. Some people like to use me instead of self because it is shorter; others still use completely different words. Pick whatever kind you like and stick with it for consistency.
The above scenario is another reason to use arrow functions, introduced in the previous chapter. Arrow functions capture the this value of the enclosing scope, so your code actually works as expected! Thus, as long as you are using =>, you can continue to use the this keyword, as follows:
var fs = require('fs'); function FileObject () { this.filename = ''; // Always use "function" for member fns, not =>, see below for why this.file_exists = function (callback) { console.log("About to open: " + this.filename); fs.open(this.filename, 'r', (err, handle) => { if (err) { console.log("Can't open: " + this.filename); callback(err); return; } fs.close(handle, () => { }); callback(null, true); }); }; }
One other thing to note is that we do not use arrow functions for declaring member functions on objects or prototypes. This is because in those cases, we actually do want the this variable to update with the context of the currently executing object. Thus, you’ll see us using => only when we’re using anonymous functions in other contexts.
The key takeaway for this section should be: If you’re using an anonymous function that’s not a class or prototype method, you should stop and think before using this. There’s a good chance it won’t work the way you want. Use arrow functions as much as possible.