There are several ways to create or initialize an object.
const myObj = {
name: 'Peter'
};
console.log(myObj);
// { name: 'Peter' }We can use literal notation to initialize an object and the dot notation to add properties.
// literal notation
const myObj = {};
// dot notation
myObj.name = 'Peter';
console.log(myObj);
// { name: 'Peter' }const myObj = new Object();
myObj.name = 'Peter';
console.log(myObj);
// { name: 'Peter' }We pass an existing object as the prototype of the new object or null
const myObj = {};
myObj.name = 'Peter';
const mySubObj = Object.create(myObj);
console.log(mySubObj);
// {}
console.log(mySubObj.name);
// Peter... and we can check if myObj is the prototype of mySubObj
console.log(Object.getPrototypeOf(mySubObj) === myObj)
// trueNote: Later we will see what prototype means.
Literal notation is faster and less verbose and it should be your first option.
Now, let's say that you need to create several objects with "the same shape" or properties.
You could do...
const peter = {
name: 'Peter',
greeting() {
console.log(`Hello, I'm ${this.name}`)
}
}
const wendy = {
name: 'Wendy',
greeting() {
console.log(`Hello, I'm ${this.name}`)
}
}
const tinkerbell = {
name: 'Tinkerbell',
greeting() {
console.log(`Hello, I'm ${this.name}`)
}
}
console.log(peter);
// { name: 'Peter', greeting: [Function: greeting] }
peter.greeting();
// Hello, I'm Peter... or, use a regular function to construct your objects.
function createCharacter(name) {
const newCharacter = {};
newCharacter.name = name;
newCharacter.greeting = function greeting() {
console.log(`Hello, I'm ${this.name}`);
}
return newCharacter;
}
const peter = createCharacter('Peter');
const wendy = createCharacter('Wendy');
const tinkerbell = createCharacter('Tinkerbell');
console.log(peter);
// { name: 'Peter', greeting: [Function: greeting] }
peter.greeting();
// Hello, I'm PeterNote: In pre es2015 implementations you will see greeting: function greeting() {} instead of greeting() {}. Check the transpiled code into pre es2015 in babel
This is better (aka, cleaner/DRYer) than the previous snippet, but... We are still creating the same function and adding it as a method on the object.
And here is where the "OOP" or JS Prototypal Inheritance shines.
We are going to abstract the common logic into a new object and keep our function to initialize the characters objects. It sounds fairly simple, no?
But, how are we going to let each character object know where to find the abstracted functionality and link it at runtime to the "caller object"? Voila, we will create the characters objects with the functionality object as their prototypes using Object.create(objectPrototype) instead of an object literal.
const characterFunctionality = {
greeting: function greeting() {
console.log(`Hello, I'm ${this.name}`);
}
};
function createCharacter(name) {
const newCharacter = Object.create(characterFunctionality)
newCharacter.name = name;
return newCharacter;
}
const peter = createCharacter('Peter');
const wendy = createCharacter('Wendy');
const tinkerbell = createCharacter('Tinkerbell');
console.log(peter);
// { name: 'Peter', greeting: [Function: greeting] }
peter.greeting();
// Hello, I'm PeterWhat is going on...?
At a high level (we will see this in more detail) when we call the method greeting() on the object peter, the JS interpreter tries to find it on the own object (peter), but, since that method doesn't exist, it will go UP on the prototype chain trying to find that method in the next prototype.
When we created our objects, we linked them to characterFunctionality, and, that link resides in a hidden property: __proto__.
Until now, we have being using regular functions to create or initialize our objects. However, we can add the usage of the new keyword to simplify some of the initialization process deferring to it (the new keyword, allow me the alliteration) the workload.
Before seeing an example, let's state what new is going to do for us:
- Creates a blank object
- Sets the prototype bind
- Sets this as the newly object: {}
- Returns
function CreateCharacter(name) {
this.name = name;
}
const peter = new CreateCharacter('Peter');
console.log(peter);
// CreateCharacter { name: 'Peter' }Sample object
const character = {
name: 'Peter',
lastName: 'Pan',
greeting: (yourName) => `Hello, ${yourName}`
}
console.log(
character.greeting('Wendy')
)Quick note about THIS:
We are going to address this keyword in other section. But for the moment, and to keep the sight in OOP, just remember... Arrow functions close over this of the lexically enclosing context.
So, if you need to access to a property of your object, use regular functions.
Examples:
The context of our arrow function is the Global/Window object; the context of our "regular" function is the base object.
const name = 'Global-Peter'
const character = {
name: 'Scoped-Peter',
arrowGreeting: () => {
return `My name is ${name}`
},
funcGreeting() {
return `My name is ${this.name}`
}
}
console.log(
character.arrowGreeting(),
character.funcGreeting()
)const myObj = {}
myObj.someProperty = 123
myObj['property with special chars'] = 'abc'
myObj.someMethod = function() {
return 'Hi!'
}
console.log(myObj.someProperty)
// 123
console.log(myObj['property with special chars'])
// abc
console.log(myObj.someMethod())
// Hi!A few notes before jumping to the next topic:
- You can take advantage of named function to improve your debugging work (instead of anonymous functions). Here will not have much sense, but when you are going through stack traces it could make thing clearer and easier to understand or debug.
myObj.someMethod = function theFunctionName() {
return 'Hi!'
}- Feel free to use ES2015
myObj.someMethod = () => {
return 'Hi!'
}- Since we are just returning, if you want to have a "cleaner" code you can do:
myObj.someMethod = () => 'Hi!'const myObj = {}
myObj.someProperty = 123
delete myObj.someProperty
console.log(myObj)Both previous methods to ADD and DELETE properties mutate the object.
In JavaScript we have primitive (string, number, boolean, etc) and reference types (objects and its special object type, array)
Primitives are immutable while reference types are mutable.
Let's see some examples to illustrate the previous statements. They will also help us to start taking scopes into consideration.
varholding aprimitivevalue and afunction updating(or mutating) the value of that variable
var primitive = 123
function updt(n) {
primitive = n
}
updt(1)
console.log(primitive)
// 1
console.log(window.primitive)
// 1Some considerations...
When we use var, the scope is going to be global/window or the function within its declared.
If we add the var statement inside the function we will have a totally different result, since now, we have 2 primitive variables, one scoped to the global object (in the browser, window) and the other to the function updt
var primitive = 123
function updt(n) {
var primitive = n
return n
}
updt(1)
console.log(window.primitive)
// 123
console.log(updt(1))
// 1Remember, since our variable is scoped to the updt function, every other function within this one will have access to it.
var primitive = 123
function updt(n) {
var primitive = n
return function insideUpdt() {
return primitive
}()
}
updt(1)
console.log(console.log(updt(1)))Note: Here we are returning what the invocation of insideUpdt() returns, that's why we have the extra (). And yes, we could totally avoid naming our function (aka, use an anonymous function) but, for debugging purposes, named function always help.
varholding aprimitivevalue and anif statementre-declaring that variable and assigning a new value
var primitive = 123
// same as if(true)
if(1) {
var primitive = 456
}
console.log(primitive)
// 456Remember var is NOT scoped at the block-level.
We are re-declaring our primitive variable (doable with var) and assigning a new value.
BTW, remember that JS is going to process variable declarations before execution (aka, hoisting). So, the engine will do the following:
var primitive = 123
primitive = 456
Now... What happens if we replace the var key with let...?
let primitive = 123
// same as if(true)
if(1) {
let primitive = 456
}
console.log(primitive)
// 123As you probably guessed, for everything inside the "if scope" the value of primitive is going to be 456. For the rest, it will be 123. We are going to obtain "similar" results with const, with the difference that once declared and assigned the value, we won't be able to mutate it.
if(1) {
const primitive = 456
primitive = 789
console.log(primitive)
}Result: TypeError: Assignment to constant variable.
One more thing to remember... Global constants (let and const) do not become properties of the window object, unlike var variables.
var global = 1
let notGlobal0 = 2
const notGlobal1 = 3
console.log(
`
${window.global}
${window.notGlobal0}
${window.notGlobal1}
`
)Result:
1
undefined
undefinedconstholding areference typevalue and afunction updating(or mutating) the data source (original variable)
const referenceValue = [1,2,3]
function addNumberToArray(arr, num) {
arr.push(num)
return arr
}
console.log(addNumberToArray(referenceValue, 4))
// [ 1, 2, 3, 4 ]
console.log(referenceValue)
// [ 1, 2, 3, 4 ]We have seen mutations (and immutability patterns) in several sections. But, it never hurts to refresh the memory.
Look at the snippet above...? Is the return of addNumberToArray() what you were expecting...?
If not, let's make a quick go-through.
We declared a constant holding an array (of numbers) as value.
We declared a function which receives as arguments an array and a number. This function pushes the number as an item into the passed array (aka, it adds the item to the end of the array); then, it returns its value.
However... When we are logging both, the return of our function and our original constant, we can see that both are returning the "same value".
First feeling: Didn't we say we cannot reassign the value of a constant?
Yes... Technically, we are not changing its value if not altering one of its properties (same for item's arrays)
This reassigns the values and produces the expected ERROR
const numbers = [1,2,3]
numbers = [1,2,3,4]
const names = {
dad: 'Peter',
son: 'Pan'
}
names = {
dad: 'Peter',
son: 'Pan',
sister: 'Wendy'
}Result: TypeError: Assignment to constant variable.
Now, this doesn't reassign. It just update the property/list.
const numbers = [1,2,3]
numbers.push(4)
const names = {
dad: 'Peter',
son: 'Pan'
}
names.sister = 'Wendy'
console.log(numbers)
// [ 1, 2, 3, 4 ]
console.log(names)
// { dad: 'Peter', son: 'Pan', sister: 'Wendy' }From Mozilla's docs...
The const declaration creates a read-only reference to a value. It does not mean the value it holds is immutable—just that the variable identifier cannot be reassigned. For instance, in the case where the content is an object, this means the object's contents (e.g., its properties) can be altered.
Second feeling: Why did the original constant change...?
Maybe you tried to declare a variable within the function, assigned as its value the array we are passing and then, performed the operation...
function addNumberToArray(arr, num) {
const newReferenceValue = arr
newReferenceValue.push(num)
return newReferenceValue
}Yet, the output is still the same.
And this is because we are dealing with reference types.
Objects (and arrays) are reference types... This means that every time we perform an operation over the reference, we are actually affecting the original object itself.
Time to see some basic examples:
Here we are declaring a constant with an object as value. That object is created using literal notation
Then, we declare a new constant which points to the previous one.
That why when we compare both, the result is true: they refer to the same object.
const character = {
name: 'Peter',
latName: 'Pan'
}
const refToObj = character
console.log(refToObj)
console.log(character)
// { name: 'Peter', latName: 'Pan' }
// { name: 'Peter', latName: 'Pan' }
console.log(character === refToObj)
// true Here we have 2 objects with the same keys and values. When we compare both, the result is false
const peter1 = {
name: 'Peter',
latName: 'Pan'
}
const peter2 = {
name: 'Peter',
latName: 'Pan'
}
console.log(peter1)
console.log(peter2)
// { name: 'Peter', latName: 'Pan' }
// { name: 'Peter', latName: 'Pan' }
console.log(peter1 === peter2)
// false This is because comparisons between objects is by reference (not value), and, as you can see, each constant (object) refers to itself.
DO NOT do the following. Take it just as an illustration
What happens if we convert into strings both object before comparing them...
console.log(JSON.stringify(peter1) === JSON.stringify(peter2))
// true ... since string comparisons are by value, both strings are equal.