Few weeks ago I found on my twitter feed a very interesting blog post: "The Best Frontend JavaScript Interview Questions (written by a Frontend Engineer)" written by Boris Cherny.
As you may guess, the author show some interesting questions to ask during a job interview. Questions are splitted in 4 parts: Concepts, Coding, Debugging, and System Design. Here, I'll focus on the Debugging part.
I really like theses question, because they deal with the specificities of JavaScript: object comparison, event loop, scope, this, prototypal inheritance and the equal operator combined with Abstract Equality Comparison Algorithm.
Before reading the solution, I'd recommend you to find the answer by yourself.
Exercise 1
I want this code to log out "hey amy", but it logs out "hey arnold" - why?
function greet (person) {
if (person == { name: 'amy' }) {
return 'hey amy'
} else {
return 'hey arnold'
}
}
greet({ name: 'amy' })
Answer
Here the problem is the following: { name: 'amy' } != { name: 'amy' }
. When comparing two object with equality or strict equality, JavaScript gonna compare the related internal references. Here, these two objects have the same properties and the same value. But in memory, this is 2 different objects.
A solution here could be:
function greet (person) {
if (person.name === 'amy') {
return 'hey amy'
}
return 'hey arnold'
}
greet({ name: 'amy' }) // "hey amy"
Exercise 2
I want this code to log out the numbers 0, 1, 2, 3 in that order, but it doesn’t do what I expect (this is a bug you run into once in a while, and some people love to ask about it in interviews).
for (var i = 0; i < 4; i++) {
setTimeout(() => console.log(i), 0)
}
Problem
I like this one because it's a bit trickier and it deal with scope and the JavaScript Event Loop.
The classic pitfall here is the Zero delays. setTimeout(callback, 0)
doesn't mean that the callback will be fire after zero milliseconds.
Here's what happen on the event loop side:
- Current Call Stack is set to the first setTimeout().
- windows.setTimeout() is considered as a Web APIs (for better Non-Blocking I/O). So the call stack send this part of code to correct Web APIs. After 0 milliseconds, the callback (here an anonymous function) would be send to the Queue (not to the call stack).
- As the call stack is free, for-loop can continue to the second setTimeout ...(repeat after we meet this condition i < 4)...
- Now the loop is over and
i === 4
. JS can now execute the callback queue one by one. Each console.log(i) will print the 4.
Do you feel lost? I hope this animation will better helps you!
Animation made with Loupe (try it it's fun!)
The second problem is related to scope. The 4 instance of setTimeout function share the same instance of i
.
var foo = 'bim'
// â–¼ this is a reference to variable foo, not his associated value ('bim')
var getFoo = function () { return foo }
foo = 'boum'
getFoo() // 'boum'
Answer
So, there are several solutions available:
- use an Immediately-Invoked Function Expression a.k.a. IIFE. The 'wrapper function' will run as soon as she's defined.
for (let i = 0; i < 4; i++) {
(function (i) {
setTimeout(() => console.log(i), 0)
})(i)
}
- switch to
let
keyword (instead ofvar
). This (new?) keyword makes scope a bit more easy to understand.
for (let i = 0; i < 4; i++) {
setTimeout(() => console.log(i), 0)
}
Exercise 3
I want this code to log out "doggo", but it logs out undefined!
let dog = {
name: 'doggo',
sayName () {
console.log(this.name)
}
}
let sayName = dog.sayName
sayName()
Answer
The previous code return undefined
. Why? Looks, on the first let condition, we define an object with 2 attributes (name and the function sayName()). Then on the second let, we copy the attribute sayName, which is a function into another variable. And then, we call this variable out of her context (in the global one). The function sayName() will return window.name (global if the environment is Node). And typeof window.name === "undefined"
.
- 👎 (the dirty one). If we want to keep the sayName variable. Then we need to bind the dog the context on it:
sayName.bind(dog)()
// or:
dog.sayName.bind(dog)()
This is dirty, right? ðŸ¤
- 👠call the function directly on her original context
let dog = {
name: 'doggo',
sayName () {
console.log(this.name)
}
}
dog.sayName() // will log "doggo"
Exercise 4
I want my dog to bark(), but instead I get an error. Why?
function Dog (name) {
this.name = name
}
Dog.bark = function () {
console.log(this.name + ' says woof')
}
let fido = new Dog('fido')
fido.bark()
Answer
We got the following error TypeError: fido.bark is not a function. On the previous code, we set the bark function on an another function (Dog()
), which is also a constructor. Is it possible because in JavaScript, functions are objects.
2 solutions:
- 👎 (the dirty one). fido.bark isn't a function but
Dog.bark
is. So let's use this one and solve thethis
issue with a function.prototype.bind() as in the exercise above:
var boundedBark = Dog.bark.bind(fido)
boundedBark() // "fido says woof"
But from my point of view using function.prototype.bind() (almost always) lead to confusion.
- 👠set bark() on the Dog's prototype
function Dog (name) {
this.name = name
}
Dog.prototype.bark = function () {
console.log(this.name + ' says woof')
}
let fido = new Dog('fido')
fido.bark() // "fido says woof"
We can also use the class keyword (ES2015) which is just a syntactic sugar to the previous code.
class Dog {
constructor (name) {
this.name = name
}
bark () {
console.log(this.name + ' says woof')
}
}
let fido = new Dog('fido')
fido.bark() // "fido says woof"
Exercise 5
Why does this code return the results that it does?
function isBig (thing) {
if (thing == 0 || thing == 1 || thing == 2) {
return false
}
return true
}
isBig(1) // false
isBig([2]) // false
isBig([3]) // true
Answer
We are using here the simple equality operator (e.g. ==) by opposition to strict comparison (e.g. ===). With this operator, it's not mandatory to compare the same type.
-
isBig(1)
pass the conditionthing == 1
as expected. -
isBig([2])
will pass the conditionthing == 2
. When comparing an array to a number, the array will be converted to a number. This is a part of the Abstract Equality Comparison Algorithm. According to this algorithm, if we compare a number with an Object (reminder: arrays are object in JS), this array will be converted to an array. Here, there is only one item inside so[2] == 2
.
Because this algorithm is obscure for the most common developers, we should aboid this operator (ESLint eqeqeq rule is your friend ðŸ‘).
// weird results
[] == ![] // true
[] == false // true
// Non transitive relation
"1" == true // true
"01" == true // true
"01" == "1" // false
Exercise 6 (bonus)
How to preserve the immutability on my heroes list?
const heroes = [
{ name: 'Wolverine', family: 'Marvel', isEvil: false },
{ name: 'Deadpool', family: 'Marvel', isEvil: false },
{ name: 'Magneto', family: 'Marvel', isEvil: true },
{ name: 'Charles Xavier', family: 'Marvel', isEvil: false },
{ name: 'Batman', family: 'DC Comics', isEvil: false },
{ name: 'Harley Quinn', family: 'DC Comics', isEvil: true },
{ name: 'Legolas', family: 'Tolkien', isEvil: false },
{ name: 'Gandalf', family: 'Tolkien', isEvil: false },
{ name: 'Saruman', family: 'Tolkien', isEvil: true }
]
const newHeroes = heroes.map(h => {
h.name = h.name.toUpperCase()
return h
})
Do you have any idea? 🙂
Top comments (20)
cool but proofread your articles :P
I've been writing for about a decade now, but didn't know about that it is possible to bind functions to objects like this:
Is this a relatively new thing or it has always worked like this? Are there any reference material on this?
Maybe, but explain why. The second version has been used ad nauseam by React developers who had to bind the component's prototype methods to the component's instance in order to use them nicely in their JSX templates.
The real ugliness here is that object methods lose their context so easily - but it's also one of the best part of JavaScript, since this makes functions as first class objects.
Which means I can do something like this:
You already got good answers for your final question 🙂
A solution to the last question:
To be very thorough:
Why would you need to freeze it? This stuff is synchronous, how would it change in the middle of your operation?
It's to guard against future changes. But the requirement was to "preserve immutability" and so this makes the original data set immutable going forward.
This approach is the only one that seems to work for me.
const newHeroes = heroes.map(h => {
const newHero = {...h}
newHero.name = newHero.name.toUpperCase()
return newHero
})
Very nice explanation for each scenario. Thanks for the write-up!