sendFile
To implement sendFile
, you'll have to use methods as callbacks and fix the number of arguments to a method/function.
Background
Implementing sendFile
is a little bit tricky because of:
- dealing with callbacks
- dealing with
this
Since we'll be implementing sendFile
with the fs
module and readFile
we'll have to go over:
- Using
fs.readFile
- Using a method as a callback for
fs.readFile
- Passing parameters to a callback
Using fs.readFile
We're using fs.readFile
so that we can read binary data. It'll read the entire contents of a file into memory. It works like this:
const fs = require('fs');
fs.readFile('/tmp/foo.txt', {encoding:'utf8'}, function(err, data) {
console.log(data);
});
- Note that
readFile
's second argument is a callback function.- The callback function is executed when an error occurs or the file.
- The callback receives an error object (which contains the error if an error occurred) and the data read from the file.
- If encoding was specified in the original call to
readFile
, then the data that's passed to the callback is a string - If there is no encoding, then the raw buffer is passed as the data to the callback
- This is useful for reading binary data, like images:
// leave encoding out of 2nd argument fs.readFile('/tmp/myImage.gif', {}, function(err, data) { // we have the raw buffer! console.log(data); });
- Of course, the callback doesn't have to be an anonymous function, it can be a named function as well:
// in this case, we're passing in handleRead as the callback rather // than using an anonymous function fs.readFile('/tmp/myImage.gif', {}, handlRead); function handleRead(err, data) { console.log(data); }
Using a method as a callback
It turns out that the callback to readFile
(or any function that requires a callback) can be a method plucked from an object. However, if the callback needs to access the this
property of the original object, this
has to bound explicitly. Let's see the problem:
- Imagine you have the following object that represents a redacted file…
- It takes a
fileName
and aword
as arguments to the constructor - Calling
printFile
will print out the contents of the file with all occurrences ofword
redacted (in this case, it's replaced with the string,SECRET
) - Here's a possible implementation:
const fs = require('fs'); class RedactedFile { constructor(fileName, word) { this.fileName = fileName; this.word = word; }
printFile() { fs.readFile(this.fileName, this.handleRead); }
handleRead(err, data) { // convert to string let s = data + ''; // let's try to replace every occurrence of this.word! const replacementPattern = new RegExp(this.word, "g"); s = s.replace(replacementPattern, 'SECRET'); // print out the result console.log(s); } } - Now let's try running this on a file
/tmp/sensitiveData.txt
, which contains the following lines:I went to the pizza place next door... and I ordered 1,000 slices of pineapple pizza.
- Here's the code that we write to print out a redacted version of
/tmp/sensitiveData.txt
:const redacted = new RedactedFile('/tmp/sensitiveData.txt', 'pizza'); redacted.printFile();
- However, when we run it, we don't get the result we expected!
- Instead, we get an error saying that that JavaScript cannot read the property
word
onundefined
which implies that thethis
inthis.word
isundefined
- How did this happen?
this.handleRead
was passed in tofs.readFile
as a callback…- but when the callback actually gets executed,
this
within the callback function isn't actually bound to the original object (because when the callback is invoked, it's not invoked as a method, but as a regular function call!) - additionally, ES6 classes are in strict mode, so
this
in regular function calls are actually undefined (when not in strict mode,this
in regular function calls is the global object orwindow
) - consequently
this.word
will cause an error becausethis
isundefined
- As a result, we have to explicitly set the
this
value of the callback - There are a few ways to do this… we'll use the way that we learned in class, which is to use arrow functions or
bind
- To use an arrow function, wrap the call to method in an arrow function so that
this
remains the same as thethis
inprintFile
- Replace
this.handleRead
with (err, data) => { this.handleRead(err, data); }this.handleRead.bind(this)
// fs.readFile(this.fileName, this.handleRead); fs.readFile(this.fileName, (err, data) => { this.handleRead(err, data); }); // or with bind: // fs.readFile(this.fileName, this.handleRead.bind(this));`
- What does that do?
- with arrow functions - it preserves
this
! - with bind:
- Remember that bind gives back function.
- With a specified
this
(as given by the caller). - So, it explicitly sets the
this
of thehandleRead
function to the currentthis
, which refers to theRedactedFile
object
- with arrow functions - it preserves
- Here's an SO article to read more about it! This shows a few ways to use a method as a callback by somehow correctly setting
this
.
Passing arguments to a callback
Imagine if our handleRead
function took an extra argument, a disclaimer.
handleRead(disclaimer, err, data) {
let s = data + '';
const replacementPattern = new RegExp(this.word, "g")
s = s.replace(replacementPattern, 'SECRET');
console.log(disclaimer);
console.log(s);
};
Now… we have an issue, because the callback that should be supplied to readFile
should only have err
and data
as its two arguments (but now our callback has 3!). How can we transform our callback so that it only takes 2 arguments like it did before? Once again, we'll rely on arrow functions or bind
!
- our arrow function can have only 2 arguments, but pass in disclaimer as the 1st argument when calling the original method
printFile() { const disclaimer = 'This file has been redacted'; // bind disclaimer as the first parameter fs.readFile(this.fileName, (err, data) => { this.handleRead(disclaimer, err, data); }); };
bind
allows us to "fix" a parameter or parameters of a function to specific values- (so we can create a new function with less parameters)
- for example:
const parseInt100 = parseInt.bind(null, "100")
… - binds "100" to the first argument of
parseInt
, and returns a function that takes only one argument, theradix
parseInt100(2)
… gives us 4 (because the only argument is the radix)- Consequently, the fix for a callback that requires a parameter is to use bind to fix the initial parameters:
RedactedFile.prototype.printFile = function() { const disclaimer = 'This file has been redacted';</br> // bind disclaimer as the first parameter fs.readFile(this.fileName, this.handleRead.bind(this, disclaimer)); };