5 Topics you should handle to start being a serious JavaScript Developer
I've been working with JavaScript for the last 15 years or so, and doing trainings to teach it for the last eight years.
Over the last two years, I've been focused on teaching JavaScript for several full-stack development bootcamps.
The main goal of these bootcamps is to prepare people to enter into the web development job market. My mission as a teacher has been, among other things, to point, focus, and explain those topics and resources I consider (from my experience) will help my students understand code from others and solve tasks/problems efficiently in their future jobs as web developers.
From my experience, there is a set of topics that are hard to understand in the beginning, but once you get them, you really start to understand complex code and you are able to write elegant, clean, and efficient code.
If I had to do a Top 5 List of Topics that cause the bigger impact on the improvement of the quality of your code, they would be:
- Closures
prototype
of a function- The value of
this
- Asynchronicity
- Unit Testings and TDD
These are not the only ones, but by understanding these, you'll be able to understand most of the code you'll find and be able to extend this code or fix it in a tested, maintainable, and understandable way.
Let's say this is the knowledge you need to remove the junior label from your JavaScript job title.
Let's start with closures.
1. Closures
A closure is created when a function returned by another function maintains access to variables located on its parent function.
To understand closures, we first need to understand two other concepts: scope chain and lexical scope.
Functions have scope chain that means that a function n()
inside of another function f()
will have access to all of its variables (the ones in its scope) and the ones in its father's scope.
const a = 1
function f() {
const b = 1
function n() {
const c = 3
}
}
In the example above, from n()
we can access a
and b
, but from f()
we cannot access c
.
Functions have lexical scope, that means they create their scope (which variables they can access) when they are defined, not when they are executed.
If we define this...
function f1(){ const a = 1; return f2(); }
function f2(){ return a; }
What will we get if we execute f1()
?
What will we get if we define var a = 55
and then we try again f1()
?
>>> f1();
a is not defined
>>> var a = 5;
>>> f1();
5
>>> a = 55;
>>> f1();
55
>>> delete a;
true
>>> f1();
a is not defined
When f2()
is created, JavaScript checks that it is using a variable named a
that is not declared locally, so any use of a
will be looked for in the parent scope (global scope in this case).
That's why, only when we assign a value to a global a
, we get returned that value when we execute f2()
.
Notice how it doesn't matter where we execute that function. When the function is created, the variables the function can access and from where become part of the function's definition.
So, mixing these two concepts with the fact that we can return a function from another function we can achieve a curious effect.
function f(){
var b = "b";
return function(){
return b;
}
}
>>> b
b is not defined
>>> var n = f();
>>> n();
"b"
Because of the lexical scope and the scope chain, the function returned maintains access to b
even when is not part the function returned and even when f()
has finished its execution. This effect of a returned function maintaining access to its enclosing function's variables is called a closure.
Closures are everywhere.
With them, we can do functional programming...
const mathy = x => y => z => (x / y) - z
mathy(10)(2)(3) // 2 => (10/2)-3
We can create private and hidden variables...
var howManyExecutions = (() => {
let executions = 0
return () => ++executions
})()
howManyExecutions() // 1
howManyExecutions() // 2
howManyExecutions() // 3
And a lot of other cool stuff.
More to read about closures:
- Closures | MDN
- JavaScript closures | jibbering.com
- Understand JavaScript Closures With Ease | JavaScript is Sexy
- Explaining JavaScript Closure & Scope Chain with Examples | risingstack.com
prototype
of a function
2.
prototype
is a property of constructor functions that contains an object that will be accessible by other objects created by this function.
In JavaScript, not only an object
is an object
. An array
is also an object
. And a function
is also an object
, but a special one because we can execute them.
({}) instanceof Object // true
([]) instanceof Object // true
(function (){}) instanceof Object // true
As objects, functions can have properties. So we can say...
function Hi() {
return Hi.msg || 'Hello!'
}
Hi.msg = "Hola!"
By default, all functions when created have a property named prototype
.
The prototype
property of functions only takes action for a special type of function, the constructor functions, this is, functions we use to create objects by using the new
keyword.
With constructor functions, we can prepare like a template for creating objects like this...
function Person(name, city) {
this.name = name
this.city = city
}
If we call this function with the new
keyword, we get an object with all of the properties assigned to this
in the constructor function. This is...
var me = new Person('Johnny','Barcelona')
me // { name: 'Johnny', city: 'Barcelona' }
var him = new Person('Marty','Hill Valley')
him // { name: 'Marty', city: 'Hill Valley' }
These objects, created by a constructor function get access to the prototype
of that constructor function, even after they have been created.
So, if we do...
Person.prototype.sayHi = function() {
return "Hello I'm" + this.name
}
Then we can do...
me.sayHi() // Hello I'm Johnny
him.sayHi() // Hello I'm Marty
Placing methods in the prototype
of a constructor function is a very efficient way of sharing functions among its objects. The general rule when using constructor functions for creating objects is:
- Properties are assigned to the object in the constructor function
- Methods are assigned to the
prototype
so these objects can access them either way
With the class
keyword, we can create a constructor function and assign methods to its prototype directly under the same structure.
class Person {
constructor(name, city) {
this.name = name
this.city = city
}
sayHi() {
return `Hello I'm ${this.name}`
}
}
You'll sure see lots of code that uses this prototype
of functions, especially with this class
structure.
prototype
:
More to read about
this
3. The value of
this
is a special keyword available in all functions that takes different values depending on how the function is called.
The scope of a function is established when the function is defined. But there is an element which value is set when the function is executed. This is the case of this
.
const me = {
name: 'Johnny',
sayHi: function() {
return `Hi, I'm ${this.name}`
}
}
me.name // Johnny
me.sayHi() // Hi, I'm Johnny
When we use this
inside of an objects' method, it points (by default) to the object that contains the method.
But we can call this method and assign another this
to it by executing the method through call
.
me.sayHi.call({ name: 'Jesie'}) // Hi, I'm Jesie
We can also attach a permanent this
to a method with bind
.
const HiStuart = me.sayHi.bind({ name: 'Stuart'})
HiStuart() // Hi, I'm Stuart
And, as we saw when used in a constructor function and its prototype, this
represents the object returned.
class Person {
constructor(name, city) {
this.name = name
this.city = city
}
sayHi() {
return `Hello I'm ${this.name}`
}
}
const me = new Person('JuanMa','Barcelona')
me.name // JuanMa
me.city // Barcelona
me.sayHi() // Hello I'm JuanMa
When used in event handlers, this
represents the DOM element that listens to the event.
<button>Click Me</button>
const myButton = document.querySelector('button)
myButton.addEventListener('click', function() {
console.log(this.innerText)
});
When clicking the button, it will display in the console Click Me
because this
in the handler represents the DOM selection.
I think these examples about this
cover most of the use cases you'll find around there.
this
:
More to read about
4. Asynchronicity
Asynchronous operations are those that are launched to be resolved at some time in the future while our code continues its execution.
Asynchronicity is one of the big deals in JavaScript. By performing operations asynchronously, we can optimize the performance of our application because we can continue doing other things while the operation (or operations) is being executed.
For example, in NodeJS, we can read and write files synchronously...
const contentFile = fs.readFileSync('notes.txt')
const numWords = contentFile.split(' ')
const numLines = contentFile.split('\n')
fs.writeFileSync('notes.txt', JSON.stringify({ numWords, numLines }))
console.log(`Results written succesfully`)
in which case we have to wait in every line until the process is completed to continue processing the following line.
Note: I will omit the error handling in the examples for the purpose of simplification.
...or we can launch these operations asynchronously by using callback functions...
fs.readFile('notes.txt', (_, contentFile) => {
const numWords = contentFile.split(' ')
const numLines = contentFile.split('\n')
fs.writeFile('results.txt', JSON.stringify({ numWords, numLines }), () => {
console.log(`Results written succesfully`)
})
});
The second argument of the fs.readFile
(and the third of fs.writeFile
) is what is called a callback function because it will be executed with the result obtained in the operation when this result is ready.
As we can see in the example, if we concatenate several asynchronous operations with this callback way, the code starts to be harder to understand and maintain (and we can end with something called callback hell).
Another way of managing asynchronous operations in a more elegant way is by using promises.
A promise is an object that encapsulates an asynchronous operation that will return either some data or some error in the future.
We have a lot of libraries that already perform operations and return promises, but we can promisify these natives I/O (callback-way) methods...
const psReadFile = fileToRead =>
new Promise( resolve => {
fs.readFile('notes.txt', (_, contentFile) => {
resolve(contentFile)
}
})
const psWriteFile = contentToWrite =>
new Promise(resolve => {
fs.writeFile('results.txt', contentToWrite, err => {
resolve(`Results written succesfully`)
}
})
Once we have the promisified version of these two methods, we can express our operations in a cleaner way...
psReadFile('notes.txt')
.then(contentFile => {
const numWords = contentFile.split(' ')
const numLines = contentFile.split('\n')
return JSON.stringify({ numWords, numLines })
})
.then(psWriteFile)
.then(console.log)
But, we can go a step further and manage these promises in a more synchronous-looking way with async-await
...
(async () => {
const contentFile = await psReadFile('notes.txt')
const numWords = contentFile.split(' ')
const numLines = contentFile.split('\n')
const writeFileResult = await psWriteFile(JSON.stringify({ numWords, numLines }))
console.log(writeFileResult)
})()
This last code is very similar to the first one that used synchronous methods, right?
As you can see, with async-await
, we can get a synchronous-syntax for
asynchronous-operations (so we can have best of both worlds).
More to read about Asynchronicity in JS:
- The Evolution of Asynchronous JavaScript | risingstack.com
- Getting to know asynchronous JavaScript: Callbacks, Promises and Async/Await | medium.com
5. Testing and TDD
If you want to be serious about JavaScript, you have to deliver not only your primary code, but also the code that tests that your primary code does what it should.
These tests are usually delivered in the form of unit testings.
A Unit testing is a piece of code that serves to check if another piece of code (usually a function) works properly. It is code that serves to test another code.
These unit testings should:
- be able to be launched automatically (this is especially important for a continuous integration)
- test the maximum code possible (code coverage as high as possible)
- be able to be executed as many times as needed
- be independent (the execution of one test shouldn't affect the execution of the others).
- maintain the quality of the code (code convention, good practices, documentation...)
TDD (Test Driven Development) is a methodology, a way of programming, a workflow, that basically consists of doing the tests first (specifying what our code should do) and after that, doing the code that passes these tests.
The recommended workflow in TDD is:
- We write the tests for a new feature (assuming method names, input parameters, returned values...)
- We execute the tests and check they fail (we still haven't written the code to be tested
- We write the simplest solution that passes the tests
- We refactor the code (cleaner and more efficient code that still passes the tests)
- We repeat these steps for another feature
By applying TDD, we can focus on the interface (API, methods, inputs, and outputs) more than in the details of the implementation.
To write unit testings in a TDD workflow, we can use the help of testing frameworks that provide us with:
- structures to write the tests in an organized and easy-to-read way
- assertions (or matchers) that are semantic functions to compare an expected value with the current one our code is returning.
So, for this piece of code ...
function addValues( a, b ) {
return a + b;
};
A Unit Testing in Jasmine could be
describe("addValues(a, b) function", function() {
it("should return the sum of the numbers passed as parameters", function(){
expect( addValues(1, 2) ).toBe( 3 );
});
it("should work also for decimal numbers", function(){
expect( addValues(1.75, 2) ).toBe( 3.75 );
});
it("should NOT return a String", function(){
expect( addValues(1, 2) ).not.toBe( "3" );
});
it("should accept numbers as strings", function(){
expect( addValues(1, '2') ).toBe( 3 );
});
});
In this example, the last test would fail, and that's ok when using TDD. Remember! do the test → execute it → check it fails → do the code so it passes the test.
So, after, we modify the code like this:
function addValues( a, b ) {
return parseInt(a) + parseInt(b);
};
All of our tests will pass. So we can continue writing tests using the TDD workflow until we decide the code is tested enough
More to read about Unit Testings and TDD in JS:
- Learning JavaScript Test-Driven Development by Example | sitepoint.com
- TDD the RITE Way | medium.com
- Learn Test Driven Development (TDD) | GitHub.com
And that's all. I've tried to give a brief explanation of these concepts, but there is, of course, a lot more to be explained and discussed on each topic.
Anyway, this is my top five of topics to give a boost to the quality of the code. What is yours?