- Coercion
- Var vs. let
- Function Declaration vs. Function Expression
- Prototype.method vs. this.method
- Immediately-Invoked Function Expression, or IIFE
- Closures and Lexical Scope
- Recursion
These are both comparison operators. ==
is a simple comparison, while ===
is a strict comparison. In contrast to the former, the latter returns true if the operands are equal and of the same type. When in doubt, it is convention to always use ===
. Learn more about comparison operators here.
The following returns false
because they are different objects. 1 is a Number
and '1'
is a String
.
console.log(1 === '1') // false
The following returns true
because both operands are equivalent.
console.log(1 == '1') // true
Var is globally scoped as opposed to let.
for(var i = 1; i <= 5; i++) {
console.log(i); // 1-5
}
console.log(i); // 6
In ES6, for loops are much cleaner. They don't leave behind and variables such as i
because it is only scoped to the code block (for loop). Read more about it here.
for(let i = 1; i <= 5; i++) {
console.log(i); // 1-5
}
console.log(i); // i is undefined
These are two ways used to create functions.
A concise example:
function foo() { ... }
With function declarations, the function is hoisted to the top of the code. This means you can call it before or after it is created:
bar(); // 42
function bar() { return 42; }
bar(); // 42
A concise example:
var foo = function() { ... }
With function expressions, the functions are not hoisted. As a result, you cannot call the function before you create it:
baz(); // baz is not a function
var baz = function() { return 42; }
baz(); // 42
Sometimes when you want to create objects in JavaScript, you create a constructor function. These are useful for instantiating multiple instances of things such as sliders and modals that possess their own properties and methods, or functions associated with objects. Additionally, with objects, these things have the ability to inherit from parent objects; however, we will talk about that at a different time.
An example of a constructor with both types of methods:
function Rectangle(x, y) {
this.x = x;
this.y = y;
this.getArea = function() {
return this.x * this.y;
}
}
// prototype method
Rectangle.prototype.getAreaPrototype = function() {
return this.x * this.y;
}
With prototypes, you define the methods one time, and all of the instantiated objects will inherit them. You can also modify the prototype method at any time:
// define constructor
function Rectangle(x, y) {
this.x = x;
this.y = y;
this.getArea = function() {
return this.x * this.y;
}
}
// apply prototype methods for inheritance
Rectangle.prototype.getAreaPrototype = function() {
return this.x * this.y;
}
// instantiate objects
var rect1 = new Rectangle(1, 3);
var rect2 = new Rectangle(2, 5);
console.log(rect1.getArea()); // 3
console.log(rect1.getAreaPrototype()); // 3
console.log(rect2.getArea()); // 10
console.log(rect2.getAreaPrototype()); // 10
// redefine the prototype method
Rectangle.prototype.getAreaPrototype = function() {
return this.x * this.y + ' units'
}
console.log(rect1.getAreaPrototype()); // 3 units
console.log(rect2.getAreaPrototype()); // 10 units
As you can see, redefining the prototype yields different results.
In constrast, methods attached via this
are created with every instantiated object. This is bad for performance, for if you were to instantiate 1000 Rectangles
, the this.getArea
will be defined 1000 times, whereas Rectangle.prototype.getAreaPrototype
will only be defined once. Despite this obvious disadvantage, methods applied to this
have one benefit; they have access to private functions and values. Consider the following:
function Citizen(name, age) {
var countryOfOrigin = 'United States';
function _isLegalAge(age) {
if(countryOfOrigin === 'United States') {
return age === 21;
} else {
return age === 19;
}
}
this.name = name;
this.age = age;
this.greet = function(anotherPerson) {
if(countryOfOrigin === 'United States') {
return `Hello, ${anotherPerson}!`;
} else {
return `Hallo, ${anotherPerson}.`;
}
}
this.drink = function(){
var canDrink = _isLegalAge(this.age);
if(canDrink) {
return `You can drink.`;
} else {
return `You cannot drink!`;
}
}
}
var Matthew = new Citizen('Matthew', 19);
console.log(Matthew.greet('Tom')); // Hello, Tom!
console.log(Matthew.drink()); // You cannot drink!
// private properties
console.log(Matthew.countryOfOrigin); // undefined
console.log(Matthew._isLegalAge(19)); // is not a function
As you can see, private values such as countryOfOrigin
and methods like _isLegalAge
are only accessible from within the constructor.
IIFEs are function expressions that are immediately called. For example, the following is a function declaration; however, it is not called, so nothing will be logged.
function foo(){
var i = 0;
console.log(i);
};
In order to immediately call this code, one may assert that adding ()
at the end would work, right? Kinda like foo()
?
function foo(){
var i = 0;
console.log(i);
}();
Nope! The above will yield the following error:
Uncaught SyntaxError: Unexpected token )
There are a few ways to get around this, but first, one should understand why an exception is thrown. According to Ben Alman:
"When the parser encounters the
function
keyword in the global scope or inside a function, it treats it as a function declaration (statement), and not as a function expression, by default. If you don’t explicitly tell the parser to expect an expression, it sees what it thinks to be a function declaration without a name and throws a SyntaxError exception because function declarations require a name."
Essentially, the problem is that it is being evaluated as a statement, not an expression. To fix this, one can wrap the entire function in ()
to force the parser to interpret it as an expression.
(function foo(){
var i = 0;
console.log(i);
}());
You can also prefix the function with a unary
operator; however, this is less common.
+function foo(){
var i = 0;
console.log(i);
}();
According to MDN, "Closures are functions that refer to independent (free) variables (variables that are used locally, but defined in an enclosing scope). In other words, these functions 'remember' the environment in which they were created."
const greet = (name) => `Hello, ${name}!`;
function init() {
let name = 'foobarbaz';
const closure = () => greet(name);
return closure();
}
console.log(init()); // 'Hello, foobarbaz!'
In the above code, closure()
is a closure because it refers to the variable name
, which is defined locally, or in the enclosing function.
Here is another example of closures using an object. Increment, decrement, and value are all closures that have access to the _change()
private method.
function Counter() {
let i = 0;
function _change(n) {
i+=n;
}
return {
increment() {
_change(1);
},
decrement() {
_change(-1);
},
value() {
return i;
}
};
}
let counter1 = new Counter();
let counter2 = new Counter();
counter1.increment();
counter2.decrement();
console.log(counter1.value()); // 1
console.log(counter2.value()); // -1
Recursion occurs when a function calls itself repeatedly until otherwise stated.
let i = 0;
function repeat(func) {
if(func() !== undefined) {
return repeat(func);
}
}
repeat(function(){
if(i < 5) {
console.log(i);
i++;
return true;
}
});
This code is identical to the following:
for(let i = 0; i < 5; i++) {
console.log(i);
}