JavaScript Junior Developer Interview Questions #1 - Superman
Today we learn about JS string methods like indexOf, and how to use promises and modules to eliminate callback hell!
Hansford's Answers is all about questions, ladies and gents. Questions. Why? Because we have answers, of course. There's no one more prepared for an interview than the master of Hansford's Answers himself--Shaquil Hansford--and today he will use his mighty powers to help you yourself get ready for junior dev interviews.
They've got questions? Well here are Hansford's Answers!
Question 1
You've been tasked with identifying a string that contains the word "superman" (case insensitive). You've written the following code:
function validateString(str) {
if (!str.toLowerCase().indexOf('superman')) {
throw new Error('String does not contain superman');
}
}
QA has come to you and said that this works great for strings like "I love superman", but an exception is generated for strings like "Superman is awesome!", which should not happen. Explain why this occurs, and show how you would solve this issue (you must use indexOf() in your answer).
Solution Process:
If you knew the answer as soon as you saw the question, give this post a like. You're already in pretty good shape.
Now, if you didn't immediately figure out the answer, don't worry--we'll work it out together.
My first instinct would be to wonder about the toLowerCase() method of strings, because that's the first method being called in the conditional, and if that's returning any strange results, everything else will naturally break down.
According to MDN ,
The toLowerCase() method returns the value of the string converted to lower case. toLowerCase() does not affect the value of the string str itself.
But what if you call toLowerCase() on a value that isn't a string? what if somehow str is being converted to a number somewhere? We can open up the console (ctrl+J in chrome or ctrl+K in firefox) and type out this:
x = 123
x.toLowerCase()
What we get is an uncaught type error. toLowerCase() is not a method of x. So if that were the issue, since we're not actually catching an error in our superman code, the output would not be "String does not contain superman." It must be something else.
So let's check out the next link in the chain: indexOf().
MDN says,
[indexOf returns] The first index of the element in the array; -1 if not found.
By "element," they mean the argument passed to indexOf(). For example, if you typed
array = [1, 2, 2]
console.log(array.indexOf(2))
You would get 1, because the first 2 is at the index 1. Likewise, because a string is an array, if you search for a string within that string, you will get the index of the first occurrence of that search string within the string we're searching through. "hello".indexOf('l') returns 2. "hello".indexOf("llo") also returns 2.
So "superman".indexOf("superman") will return? That's right, 0! Now let's look at the code again.
function validateString(str) {
if (!str.toLowerCase().indexOf('superman')) {
throw new Error('String does not contain superman');
}
}
We expect str.toLowerCase().indexOf('superman') to return 0 when str is any string that starts with the word superman. Our if statement will evaluate 0 as falsy, since JavaScript treats 0 as equivalent to boolean false. But we're using the ! operator, which means we will reverse that falsy value to truthy. So if the value is 0, then we expect to throw our error.
The QA team, in this example, has reported that "this code works great for strings like 'I love superman', but an exception is generated for strings like 'Superman is awesome!', which should not happen."
But according to our code, that actually should happen. Because null and undefined values are falsy, and the ! operator converts them to truthy, our code's intention was to catch instances in which .toLowerCase() or .indexOf() returned those falsy values, in which case we should throw our error. We also falsely assumed that if indexOf returns a falsy value, our string doesn't contain superman.
In truth, indexOf returns -1 when superman is not inside the string. So we need to check instead if indexOf('superman') is == -1. But also, we need to catch the potential errors we were worried about. In this case, a try/catch block will handle the issue for us.
function validateString(str) {
try{
if (str.toLowerCase().indexOf('superman') == -1) {
throw new Error('String does not contain superman');
}
} catch (error){
console.log(`Invalid String! Error: ${error}`);
}
}
Question 2
How can you avoid callback hells?
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
getMoreData(d, function(e) {
// ...
})
})
})
})
})
Do you immediately know the answer to this? Leave a comment! Tell me about it!
First, what is callback hell? We can look at the above example code and make some pretty clear guesses. For example, you can see that getData is a function that takes a function as an argument--most likely a callback--and the function passed to it calls a new function, getMoreData, handing over the argument passed into its own parameter down to getMoreData as the first parameter. This same cycle continues from function to function.
It looks very intimidating.
A quick google search confirms our intuition:
Callback Hell, also known as Pyramid of Doom, is an anti-pattern seen in code of asynchronous programming. It is a slang term used to describe and unwieldy number of nested โifโ statements or functions.
The most common solution to this problem is to use promises rather than callbacks. A promise is a built-in JavaScript object that conceptually represents a future outcome for a callback that has not been called yet, but in actual code terms is its own unique value with its own unique methods and properties.
Promises take a callback as their argument, with resolve and reject as their parameters. Then in the callback function body, you implement your callback and call resolve() in cases where the result is desired and reject() in cases where an error occurs.
new Promise( (resolve, reject)=>{
randomCode.getInfo(data, result=>{
if(!result) reject("No result found!");
else resolve(result);
})
});
With the above sample code, you can see that randomeCode.getInfo takes data as its first argument, and a callback as its second. When the callback is eventually triggered, ten milliseconds, ten seconds or even ten minutes from now, it will check if the result is falsy. If it is, the reject method will be called, and the promise object will handle that. If not, the resolve method will be called, and likewise the promise will handle that.
What the promise does is, when resolve is called, it passes the argument in its parameter to its .then() method. Its .then() method takes a function as an argument, inside of which we can decide what to do now that our callback has triggered. When reject is called, the promise passes its parameter to a .catch() method, which also takes a function that deals with the error passed through.
new Promise( (resolve, reject)=>{
randomCode.getInfo(data, result=>{
if(!result) reject("No result found!");
else resolve(result);
})
})
.then(result=>{
//Do complicated stuff.
console.log(`Success! Here is our result! ${result.toString()}`);
})
.catch(error=>{
console.log(`Oops! Too bad. We got an error message: ${error}`);
});
As you can see, the above code, using .then() and .catch(), is much cleaner, and avoids all the indentations and complex nested layers of }).
If you're using the NodeJS runtime for backend JavaScript, you can use the utility function promisify instead. All you have to do is pass the function that requires the callback as an argument into the promisify function. For example. the stat() method of Node's fs module returns data about a particular file. To do this, it requires a callback which will be triggered once the file is located.
Its generic form looks something like this:
// Getting information for a file
fs.stat("example_file.txt", (error, stats) => {
if (error) {
console.log(error);
}
else {
console.log("Stats object for: example_file.txt");
console.log(stats);
// Using methods of the Stats object
console.log("Path is file:", stats.isFile());
console.log("Path is directory:", stats.isDirectory());
}
});
But if you used promisify, you could do this:
const util = require('util');
const fs = require('fs');
const stat = util.promisify(fs.stat);
stat("example_file.txt").then((stats) => {
console.log("Stats object for: example_file.txt");
console.log(stats);
// Using methods of the Stats object
console.log("Path is file:", stats.isFile());
console.log("Path is directory:", stats.isDirectory());
}).catch((error) => {
console.log(error);
});
Beautiful.
Our final method for avoiding callback hell is to name our methods.
You can create modules in JS or, if you don't want to touch that feature, you can import other JS files into your webpage that contain methods and classes for you to use. In these files, you might define the method you'd like to use as a callback for when you fetch data from an API or some other action that has a delayed response.
This way, instead of having code like this:
getData(info, (error, result)=>{
if(error) //Do things
else{
getMoreData(result, (error, result)=>{
//Do even more stuff. Call another callback
getEvenMoreStuff(result, (error, result)=>{
//Again, more stuff, blah blah blah
})
})
}
});
You can have code like this:
getData(info, handleDataResponse);
In another file, we'd have something like:
function handleDataResponse(error, result){
if(error){
throw new Error(error);
} else {
getMoreData(result, handleMoreDataResponse);
}
}
This solution is of course not as good as promises, because simply disperses the code elsewhere and forces readers to have to scroll around to find it, but it is still better than callback hell.
Thanks for reading!
Get in touch with me on twitter: twitter.com/shaquilhansford
Don't be afraid to leave a comment! Hope this blog helped!