Improve Your JavaScript Level With These 4 Questions

Things you should know to prepare JavaScript Interviews.

bitfish
bitfish
Sep 11 · 11 min read
Image for post
Image for post
Photo by Headway on Unsplash

JavaScript is now a very popular programming language, based on which a large number of libraries and frameworks have been derived. But no matter how the upper ecosystem evolves, it can’t do without vanilla JavaScript. Here I’ve selected 4 JavaScript interview questions to test a programmer’s skill of vanilla JavaScript.

1. Implement Array.prototype.map

How to implement an Array.prototype.map method by hand?


It is not difficult to become adept at using the built-in methods of arrays. But if you’re just familiar with the syntax and don’t know the principles, it’s hard to really understand JavaScript.

For Array.prototype.map , it creates a new array populated with the results of calling a provided function on every element in the calling array.

Image for post
Image for post

If we refer to lodash, we can write a map function like this:

Image for post
Image for post
function map(array, iteratee) {
let index = -1
const length = array == null ? 0 : array.length
const result = new Array(length)
while (++index < length) {
result[index] = iteratee(array[index], index, array)
}
return result
}

Use examples:

Image for post
Image for post

2. Object.defineProperty and Proxy

How to achieve this code effect?

Image for post
Image for post

We can see that when we try to print obj.a three times in a row, we get three different results. How incredible it seems!

Can you create a mysterious object obj to achieve this effect?


In fact, there are three solutions to this question:

  • accessor property
  • Object.defineProperty
  • Proxy

According to ECMAScript, an object’s properties can take two forms:

Image for post
Image for post

An Object is logically a collection of properties. Each property is either a data property or an accessor property:

  • A data property associates a key value with an ECMAScript language value and a set of Boolean attributes.
  • An accessor property associates a key value with one or two accessor functions, and a set of Boolean attributes. The accessor functions are used to store or retrieve an ECMAScript language value that is associated with the property.

The so-called data property is what we usually write:

let obj = {
a: 1,
b: 2
}

We have no more than two operations on the property of an object: reading the property and setting the property. For accessor property, we use get and set methods to define properties, which are written as follows:

Image for post
Image for post
let obj = {
get a(){
console.log('triggle get a() method')
console.log('you can do anything as you want')
return 1
},
set a(value){
console.log('triggle set a() method')
console.log('you can do anything as you want')
console.log(`you are trying to assign ${value} to obj.a`)
}
}
Image for post
Image for post

Access properties give us powerful metaprogramming abilities, so we can accomplish our requirements in this way:

let obj = {
_initValue: 0,
get a() {
this._initValue++;
return this._initValue
}
}
console.log(obj.a, obj.a, obj.a)
Image for post
Image for post

The second way is to use Object.defineProperty, which works the same way we used to access properties, except instead of declaring access properties directly, we configure access properties via Object.defineProperty.

This is a little bit more flexible to use, so we can write it like this:

Image for post
Image for post
let obj = {}
Object.defineProperty(obj, 'a', {
get: (function(){
let initValue = 0;
return function(){
initValue++;
return initValue
}
})()
})
console.log(obj.a, obj.a, obj.a)

In the get method here, we use a closure so that the variable initValue that we need to use is hidden in the closure and does not contaminate the other scope.

The third way is to use Proxy. If you don’t already know about Proxy, you can refer to an article I wrote earlier:

With Proxy, we can intercept access to object properties. As long as we use Proxy to intercept the access to obj.a, and then return 1, 2, and 3 in turn, we can complete the requirements before:

let initValue = 0;
let obj = new Proxy({}, {
get: function(item, property, itemProxy){
if(property === 'a'){
initValue++;
return initValue
}
return item[property]
}
})
console.log(obj.a, obj.a, obj.a)
Image for post
Image for post

Why is it important to understand this question? Because Object.defineProperty and Proxy gave us powerful metaprogramming abilities, we could modify our object appropriately to do something special.

In the Vue, a well-known front-end framework, one of its core mechanisms is the two-way binding of data. In Vue2.0, Vue implemented this mechanism by using Object.defineProperty; in Vue3.0, Proxy is used to accomplish this mechanism.

You can’t really understand how a framework like Vue works if you don’t master this. If you master these principles, you will get twice the result with half the effort in learning Vue.

3. Scope and closure

What is the result of running this code?

Image for post
Image for post
function foo(a,b) {
console.log(b)
return {
foo:function(c){
return foo(c,a);
}
};
}
let res = foo(0);
res.foo(1);
res.foo(2);
res.foo(3);

The above code has multiple nested functions and three foo in nested functions at the same time, which at first glance looks very tedious. So how do we make sense of this?

First, let’s make sure how many functions are there in the above code? We can see that the keyword function is used in two places in the code above, so there are two functions in the code above, namely the first line function foo(a,b) { and the fourth line foo:function(c){. And these two functions have the same name.

Second question: Which function is called by foo (c, a) on line 5? If you’re not sure, let’s take a look at a simpler example:

var obj={
fn:function (){
console.log(fn);
}
};
obj.fn()

Does this code throw an exception if we run it? The answer is yes.

Image for post
Image for post

This is because the upper scope of the obj.fn() method is global and the fn method inside obj cannot be accessed.

Going back to our previous example, by the same logic, when we call foo(c, a), we’re actually calling the foo function on the first line.

And when we call res.foo(1), which foo is called? Obviously, the foo function on the 4th line is called.

Because the two foo functions do not work the same way, we can change the name of one of them to bar to make it easier for us to understand the code.

Image for post
Image for post
function foo(a,b) {
console.log(b)
return {
bar:function(c){
return foo(c,a);
}
};
}
let res = foo(0);
res.bar(1);
res.bar(2);
res.bar(3);

This change will not affect the final result but will make it easier for us to understand the code. Try this tip if you meet a similar question in the future.

Each time a function is called, a new scope is created, so we can draw the diagram to help us understand the logic of how the code works.

When we execute let res = foo(0);, we’re actually executing foo(0, undefiend). At this point, a new scope is created in the program, in the current scope, a=0, b=undefined. So the diagram that I’ve drawn looks something like this.

Image for post
Image for post

Then console.log(b) will be executed, so the first time it prints out ‘undefined’ in the console.

Then res.bar(1) is executed, creating a new scope where c=1:

Image for post
Image for post

And then foo(c, a) is called again from the above function, which is actually foo(1, 0), and the scope looks like this:

Image for post
Image for post

In the new scope, the value of a is 1 and the value of b is 0, so the console will print out the 0.

Again, res.bar(2) is executed next. Notice that res.bar(2) and res.bar(1) are parallel relations, so we should draw the scope diagram like this:

Image for post
Image for post

So in this code, the console prints out the value 0 too.

The same goes for the process that executes res.bar(3), and the console still prints a 0.

So the end result of the code above is:

Image for post
Image for post

In fact, the above question can be changed in other ways. For example, it can be changed into the following:

Image for post
Image for post
function foo(a,b) {
console.log(b)
return {
foo:function(c){
return foo(c,a);
}
};
}
foo(0).foo(1).foo(2).foo(3);

Before we solve this question, the first thing we need to do is distinguish between the two different foo functions, so the above code can be changed to look like this:

Image for post
Image for post
function foo(a,b) {
console.log(b)
return {
bar:function(c){
return foo(c,a);
}
};
}
foo(0).bar(1).bar(2).bar(3);

When it executes foo(0), the scope is the same as before, and then the console will print out ‘undefined’.

Image for post
Image for post

Then execute .bar(1) to create a new scope. This parameter 1 is actually the value of c.

Image for post
Image for post

Then the .bar(1) method calls foo(c, a) again, which is actually foo(1, 0). Parameter 1 here is actually going to be the value of a in the new scope, and 0 is going to be the value of b in the new scope.

Image for post
Image for post

So the console then prints out the value of b, which is 0.

.bar(2) is called again, with 2 as the value of c in the new scope:

Image for post
Image for post

And then .bar(2) calls foo(c, a), which is actually foo(2, 1), where 2 is the value of a in the new scope, and 1 is the value of b in the new scope.

Image for post
Image for post

So the console then prints out the value of b, which is 0.

And then it will execute .bar(3), the process is the same as before, so I’m not going to expand the description, this step the console prints out the 2.

As mentioned above, the final result of the code running is:

Image for post
Image for post

Well, after a long journey, we finally got the answer. This question is a good test of the interviewee’s understanding of closures and scopes.

4. Compose

Let’s say we have a function that looks like this:

Image for post
Image for post
function compose (middleware) {
// some code
}

The compose function accepts a function array middleware :

Image for post
Image for post
let middleware = []
middleware.push((next) => {
console.log(1)
next()
console.log(1.1)
})
middleware.push((next) => {
console.log(2)
next()
console.log(2.1)
})
middleware.push(() => {
console.log(3)
})
let fn = compose(middleware)
fn()

When we try to execute fn, it calls the functions in the middleware and passes the next function as a parameter to each small function.

If we execute next in a small function, the next function of this function in middleware is called. And if you don’t execute next, the program doesn’t go down.

After executing the above code, we get the following result:

1
2
3
2.1
1.1

So how can we write a compose function to do that?


First of all, the compose function must return a composed function, so we can write code like this:

Image for post
Image for post
function compose (middleware) {
return function () {
}
}

Then, in the returned function, the first function from the middleware starts executing. We will also pass the next function as its argument. So let’s write it this way:

Image for post
Image for post
function compose (middleware) {
return function () {
let f1 = middleware[0]
f1(function next(){
})
}
}

The next function acts as a switch that continues through the middleware, which looks like this:

Image for post
Image for post
function compose (middleware) {
return function () {
let f1 = middleware[0]
f1(function next(){
let f2 = middleware[1]
f2(function next(){
...
})
})
}
}

Then proceed to call the third function in the next function… Wait, this looks like recursion! So we can write a recursive function to complete this nested call:

Image for post
Image for post
function compose (middleware) {
return function () {
dispatch(0)
function dispatch (i) {
const fn = middleware[i]
if (!fn) return null
fn(function next () {
dispatch(i + 1)
})
}
}
}

Okay, so that’s our compose function, so let’s test it out:

Image for post
Image for post

Well, this function does exactly what it needs to do. But we can also optimize that our compose function can support asynchronous functions. We can improve the following code:

Image for post
Image for post
function compose (middleware) {
return async function () {
await dispatch(0)
function async dispatch (i) {
const fn = middleware[i]
if (!fn) return null
await fn(function next () {
dispatch(i + 1)
})
}
}
}

Actually, the compose function above is the core mechanism of the well-known node framework koa.

When I choose a candidate, I accept that he/she is not familiar with some of the frameworks. After all, there are so many libraries and frameworks in the JavaScript ecosystem that no one can master them all. But I do want the candidate to be aware of these important vanilla JavaScript tricks because they are the foundation of all libraries and frameworks.

Conclusion

Actually, there are some other interview questions in my draft, but I will not continue to explain them here due to the limited length of the article. I’ll share it with you later.

This article mainly deals with vanilla JavaScript, not browsers, node, frameworks, algorithms, design patterns, etc. If you are also interested in these topics, feel free to leave a comment.

Thank you for reading!

JavaScript In Plain English

Did you know that we have three publications and a YouTube channel? Find links to everything at plainenglish.io!

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store