Objects are used to store keyed collections of various data and more complex entities.
There are two ways we can create an object -
let user = new Object(); // "object constructor" syntax let user = {}; // "object literal" syntax
Usually, the figure brackets {...} are used. That declaration is called an object literal.
We can immediately put some properties into {...}
as “key: value” pairs:
let user = { // an object name: "John", // by key "name" store value "John" age: 30 // by key "age" store value 30 };
Here, name
, age
are called Properties, and "John"
and 30
is called value. They are seperated by a colon :
. Internally properties are always treated as a string, even though the properties are not quoted, they still are strings. You can optionally enclose the property within the single or double quote.
A property can contain any type of value. In the first property name
we have string data type value, in the second property age
, we have Number data type value. We can also add boolean, array, etc. Any type of data is valid for property.
In the above example, we have assigning literal directly to the property. But we can also add a variable to that property -
let myName = "Santanu Bera"; let obj = { name : myName, age : "29" };
In the above example, the property name
will contain the value of the variable myName
that is "Santanu Bera". In the above case, the value is copied by value. It means the value of the variable myName
gets copied to the property name
. If the value were array, or another object, then a reference to the variable would get copied to the property.
After you have declared an object, you may want to add more properties to it, you can do it by dot notation -
object.newProperty = value;
In the above syntax, if the newProperty
already exists, then it updates the value, if it doesn't exists, then it adds a new property called newProperty
and assign the value value
. For example -
let myName = "Santanu Bera"; let obj = { name : myName, age : "29" }; obj.height = "6 Feet"; console.log(obj); // name : "Santanu Bera" // age : 29 // height : "6 Feet"
Property values are accessible using the dot notation:
// get fields of the object: alert( user.name ); // John alert( user.age ); // 30
To remove a property, we can use delete
operator:
delete user.age;
We can also use multiword property names, but then they must be quoted:
let user = { name: "John", age: 30, "likes birds": true // multiword property name must be quoted };
A variable cannot have a name equal to one of language-reserved words like “for”, “let”, “return” etc. But for an object property, there’s no such restriction. Any name is fine:
let obj = { for: 1, let: 2, return: 3 }; alert( obj.for + obj.let + obj.return ); // 6
You cannot have a property named __proto__
, In JS, it convey different meaning and bound to object. So do not ever create a property with the name __proto__.
The last property in the list may end with a comma:
let user = { name: "John", age: 30, }
That is called a “trailing” or “hanging” comma. Makes it easier to add/remove/move around properties, because all lines become alike.
For multiword properties, the dot access doesn’t work -
// this would give a syntax error user.likes birds = true
That’s because the dot requires the key to be a valid variable identifier. That is: no spaces and other limitations.
There’s an alternative “square bracket notation” that works with any string:
let user = {}; // set user["likes birds"] = true; // get alert(user["likes birds"]); // true // delete delete user["likes birds"];
Now everything is fine. Please note that the string inside the brackets is properly quoted (any type of quotes will do).
You can use square baracket notation along with variable to create property name. These kind of property is called computed property. For example -
let name = "Santanu"; let obj = {}; obj[name] = "Access Granted"; console.log(obj); // Santanu : "Access Granted"
But this is not possible with dot notation -
obj.name = "Access Granted"; console.log(obj); // name : "Access Granted"
You can use computed property directly within object literal -
let name = "Santanu"; let obj = { [name] : "Access Granted", }; console.log(obj); // Santanu : "Access Granted";
In real code we often use existing variables as values for property names.
function makeUser(name, age) { return { name: name, age: age // ...other properties }; } let user = makeUser("John", 30); alert(user.name); // John
In the example above, properties have the same names as variables. The use-case of making a property from a variable is so common, that there’s a special property value shorthand to make it shorter.
Instead of name:name
we can just write name
, like this:
function makeUser(name, age) { return { name, // same as name: name age // same as age: age // ... }; }
We can use both normal properties and shorthands in the same object:
let user = { name, // same as name:name age: 30 };
To know if a property exists within a object, we can compare with undefined
value -
let obj = { name : "Santanu", age : 30, }; console.log(obj.address == undefined); // true console.log(obj.name == undefined); // false
So, if you try to access any property that doesn't exists, it returns undefined, otherwise it returns it's value. But there is a problem here, what if a property exists but it contains undefined
value.
let obj = { name : undefined, }; console.log(obj.name == undefined); // true
In the above example, the property name
exists, but it still returns true. In these kind of scinario, you can use in
operator to check if the operator exists.
// Syntax -- "propertyName" in ObjectName // Example -- let obj = { name : undefined, "my name" : "Santanu Bera"; }; // Exits console.log("name" in obj); // true // Doesn't Exists console.log("address" in obj); // false
Remember that the property name must be quoted. This example works multiword property too -
console.log("my name" in obj); // true
To walk over all keys of an object, there exists a special form of the loop: for..in
for(key in object) { // executes the body for each key among object properties }
let user = { name: "John", age: 30, isAdmin: true }; for(let key in user) { // keys alert( key ); // name, age, isAdmin // values for the keys alert( user[key] ); // John, 30, true }
A variable stores not the object itself, but its “address in memory”, in other words “a reference” to it.
let user = { name: "John" };
Here, the object is stored somewhere in memory. And the variable user has a “reference” to it. When an object variable is copied – the reference is copied, the object is not duplicated. For instance:
let user = { name: "John" }; let admin = user; // copy the reference
Now we have two variables, each one with the reference to the same object:
let user = { name: 'John' }; let admin = user; admin.name = 'Pete'; // changed by the "admin" reference alert(user.name); // 'Pete', changes are seen from the "user" reference
The equality ==
and strict equality ===
operators for objects work exactly the same. Two objects are equal only if they are the same object.
let a = {}; let b = a; // copy the reference alert( a == b ); // true, both variables reference the same object alert( a === b ); // true
And here two independent objects are not equal, even though both are empty:
let a = {}; let b = {}; // two independent objects alert( a == b ); // false
An object declared as const
can be changed.
const user = { name: "John" }; user.age = 25; // (*) alert(user.age); // 25
It might seem that the line (*) would cause an error, but no, there’s totally no problem. That’s because const fixes the value of user itself. And here user stores the reference to the same object all the time. The line (*) goes inside the object, it doesn’t reassign user.
The const
would give an error if we try to set user to something else, for instance:
const user = { name: "John" }; // Error (can't reassign user) user = { name: "Pete" };
So, copying an object variable creates one more reference to the same object. But what if we need to duplicate an object? Create an independent copy, a clone? That’s also doable, but a little bit more difficult, because there’s no built-in method for that in JavaScript. Actually, that’s rarely needed. Copying by reference is good most of the time. But if we really want that, then we need to create a new object and replicate the structure of the existing one by iterating over its properties and copying them on the primitive level.
let user = { name: "John", age: 30 }; let clone = {}; // the new empty object // let's copy all user properties into it for (let key in user) { clone[key] = user[key]; } // now clone is a fully independent clone clone.name = "Pete"; // changed the data in it alert( user.name ); // still John in the original object
Also we can use the method Object.assign
for that.
Object.assign(dest[, src1, src2, src3...])
Arguments dest
, and src1, ..., srcN
(can be as many as needed) are objects. It copies the properties of all objects src1, ..., srcN
into dest
. In other words, properties of all arguments starting from the 2nd are copied into the 1st. Then it returns dest
. For example -
let user = { name: "John" }; let permissions1 = { canView: true }; let permissions2 = { canEdit: true }; // copies all properties from permissions1 and permissions2 into user Object.assign(user, permissions1, permissions2); // now user = { name: "John", canView: true, canEdit: true }
If the receiving object (user) already has the same named property, it will be overwritten:
let user = { name: "John" }; // overwrite name, add isAdmin Object.assign(user, { name: "Pete", isAdmin: true }); // now user = { name: "Pete", isAdmin: true }
We also can use Object.assign
to replace the loop for simple cloning:
let user = { name: "John", age: 30 }; let clone = Object.assign({}, user);
It copies all properties of user into the empty object and returns it. Actually, the same as the loop, but shorter.
Until now we assumed that all properties of user are primitive. But properties can be references to other objects. What to do with them? For example -
let user = { name: "John", sizes: { height: 182, width: 50 } }; alert( user.sizes.height ); // 182
Now if you use loop or Object.assign to clone the object, in the new object, the properties sizes
will be copied by reference, as it is an object. So the sizes
properties of source object and destination object will share the same reference. If you change in the source, it will be affected in destination object.
let targetObject = Object.assign({}, user); user.sizes.width = 100; alert(targetObject.sizes.width); // 100
So Object.assign
actually clone upto the topmost level. It cannot copy in the deep level.
To fix that, we should use the cloning loop that examines each value of user[key]
and, if it’s an object, then replicate its structure as well. That is called a Deep Cloning
.
There’s a standard algorithm for deep cloning that handles the case above and more complex cases, called the Structured cloning algorithm. In order not to reinvent the wheel, we can use a working implementation of it from the JavaScript library lodash, the method is called _.cloneDeep(obj)
.
An object can also contain methods -
let user = { name: "John", age: 30 }; user.sayHi = function() { alert("Hello!"); }; user.sayHi(); // Hello!
Of course, we could use a pre-declared function as a method, like this:
let user = { // ... }; // first, declare function sayHi() { alert("Hello!"); }; // then add as a method user.sayHi = sayHi; user.sayHi(); // Hello!
// these objects do the same let user = { sayHi: function() { alert("Hello"); } }; // method shorthand looks better, right? let user = { sayHi() { // same as "sayHi: function()" alert("Hello"); } };
As demonstrated, we can omit "function" and just write sayHi().
It’s common that an object method needs to access the information stored in the object to do its job. For instance, the code inside user.sayHi()
may need the name of the user
.
To access the object, a method can use the this
keyword.
let user = { name: "John", age: 30, sayHi() { alert(this.name); } }; user.sayHi(); // John
In the above example, the keyword this
refers to the same object by which it is called. As we are using user.sayHi()
, in this case this
refers to the object user
.
Technically, it’s also possible to access the object without this
, by referencing it via the outer variable:
let user = { name: "John", age: 30, sayHi() { alert(user.name); // "user" instead of "this" } };
But this way of accessing object from inside the method is not recommended. In few cases, it will result unpredictable behaviour. So always use this
.
An intricate method call can lose this
, for instance:
let user = { name: "John", hi() { alert(this.name); }, bye() { alert("Bye"); } }; user.hi(); // John (the simple call works) // now let's call user.hi or user.bye depending on the name (user.name == "John" ? user.hi : user.bye)(); // Error!
You can see that the last call results in an error, because the value of "this" inside the call becomes undefined
.
If we want to understand why it happens, let’s get under the hood of how obj.method()
call works.
Looking closely, we may notice two operations in obj.method()
statement:
obj.method
.()
execute it.So, how does the information about this
get passed from the first part to the second one? If we put these operations on separate lines, then this
will be lost for sure:
let user = { name: "John", hi() { alert(this.name); } } // split getting and calling the method in two lines let hi = user.hi; hi(); // Error, because this is undefined
Here hi = user.hi
puts the function into the variable, and then on the last line it is completely standalone, and so there’s no this
.
To make user.hi()
calls work, JavaScript uses a trick – the dot '.' returns not a function, but a value of the special Reference Type.
The Reference Type is a “specification type”. We can’t explicitly use it, but it is used internally by the language. The value of Reference Type is a three-value combination (base, name, strict), where:
base
is the object.name
is the property.strict
is true if use strict is in effect.The result of a property access user.hi
is not a function, but a value of Reference Type. For user.hi
in strict mode it is:
// Reference Type value (user, "hi", true)
When parentheses ()
are called on the Reference Type, they receive the full information about the object and its method, and can set the right this
(=user in this case).
Any other operation like assignment hi = user.hi
discards the reference type as a whole, takes the value of user.hi
(a function) and passes it on. So any further operation “loses” this.
So, as the result, the value of this is only passed right way if the function is called directly using a dot obj.method()
or square brackets obj['method']()
syntax (they do the same here). Later in this tutorial, we will learn various ways to solve this problem such as func.bind()
.
Arrow functions are special: they don’t have their “own” this. If we reference this from such a function, it’s taken from the outer “normal” function. For instance, here arrow()
uses this
from the outer user.sayHi()
method:
// The following example works // as "this" inside the arrow is taken from outer function sayHi() let user = { firstName: "Ilya", sayHi() { let arrow = () => alert(this.firstName); arrow(); } }; user.sayHi(); // Ilya
That’s a special feature of arrow functions, it’s useful when we actually do not want to have a separate this, but rather to take it from the outer context.
When an object is used in the context where a primitive is required, for instance, in an alert or mathematical operations, it’s converted to a primitive value using the ToPrimitive
algorithm. That algorithm allows us to customize the conversion using a special object method. Depending on the context, the conversion has a so-called “hint”. There are three hints -
When an operation expects a string, for object-to-string conversions, like alert:
// output alert(obj); // using object as a property key anotherObj[obj] = 123;
When an operation expects a number, for object-to-number conversions, like maths:
// explicit conversion let num = Number(obj); // maths (except binary plus) let n = +obj; // unary plus let delta = date1 - date2; // less/greater comparison let greater = user1 > user2;
Occurs in rare cases when the operator is “not sure” what type to expect.
For instance, binary plus +
can work both with strings (concatenates them) and numbers (adds them), so both strings and numbers would do. Or when an object is compared using ==
with a string, number or a symbol.
// binary plus let total = car1 + car2; // obj == string/number/symbol if (user == 1) { ... };
The greater/less operator <>
can work with both strings and numbers too. Still, it uses “number” hint, not “default”. That’s for historical reasons.
In practice, all built-in objects except for one case (Date object, we’ll learn it later) implement "default" conversion the same way as "number". And probably we should do the same.
Please note – there are only three hints. It’s that simple. There is no “boolean” hint (all objects are true
in boolean context) or anything else. And if we treat "default" and "number" the same, like most built-ins do, then there are only two conversions.
obj[Symbol.toPrimitive](hint)
if the method exists,obj.toString()
and obj.valueOf()
, whatever exists.obj.valueOf()
and obj.toString()
, whatever exists.Let’s start from the first method. There’s a built-in symbol named Symbol.toPrimitive
that should be used to name the conversion method, like this:
obj[Symbol.toPrimitive] = function(hint) { // return a primitive value // hint = one of "string", "number", "default" }
For instance, here user object implements it:
let user = { name: "John", money: 1000, [Symbol.toPrimitive](hint) { alert(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; // conversions demo: alert(user); // hint: string -> {name: "John"} alert(+user); // hint: number -> 1000 alert(user + 500); // hint: default -> 1500
As we can see from the code, user becomes a self-descriptive string or a money amount depending on the conversion. The single method user[Symbol.toPrimitive]
handles all conversion cases.
Methods toString
and valueOf
come from ancient times. They are not symbols (symbols did not exist that long ago), but rather “regular” string-named methods. They provide an alternative “old-style” way to implement the conversion.
If there’s no Symbol.toPrimitive
then JavaScript tries to find them and try in the order:
toString -> valueOf
for “string” hint.valueOf -> toString
otherwise.For instance, here user does the same as above using a combination of toString and valueOf:
let user = { name: "John", money: 1000, // for hint="string" toString() { return `{name: "${this.name}"}`; }, // for hint="number" or "default" valueOf() { return this.money; } }; alert(user); // toString -> {name: "John"} alert(+user); // valueOf -> 1000 alert(user + 500); // valueOf -> 1500
In the absence of Symbol.toPrimitive and valueOf, toString will handle all primitive conversions.
let user = { name: "John", toString() { return this.name; } }; alert(user); // toString -> John alert(user + 500); // toString -> John500
In the above example, we didn't implement valueOf
method to convert Object to Number type. In these scinario, toString()
is called.
The regular {...}
syntax allows to create one object. But often we need to create many similar objects, like multiple users or menu items and so on. That can be done using constructor functions and the "new"
operator.
Constructor functions technically are regular functions. There are two conventions though:
"new"
operator.function User(name) { this.name = name; this.isAdmin = false; } let user = new User("Jack"); alert(user.name); // Jack alert(user.isAdmin); // false
When a function is executed as new User(...)
, it does the following steps:
this
.this
, adds new properties to it.this
is returned.In other words, new User(...)
does something like:
function User(name) { // this = {}; (implicitly) // add properties to this this.name = name; this.isAdmin = false; // return this; (implicitly) }
So the result of new User("Jack")
is the same object as:
let user = { name: "Jack", isAdmin: false };
Now if we want to create other users, we can call new User("Ann")
, new User("Alice")
and so on. Much shorter than using literals every time, and also easy to read.
That’s the main purpose of constructors – to implement reusable object creation code.
If we have many lines of code all about creation of a single complex object, we can wrap them in constructor function, like this:
let user = new function() { this.name = "John"; this.isAdmin = false; // ...other code for user creation // maybe complex logic and statements // local variables etc };
The constructor can’t be called again, because it is not saved anywhere, just created and called. So this trick aims to encapsulate the code that constructs the single object, without future reuse.
By the way, we can omit parentheses after new, if it has no arguments:
let user = new User; // <-- no parentheses // same as let user = new User();
Of course, we can add to this
not only properties, but methods as well. For instance, new User(name)
below creates an object with the given name and the method sayHi
:
function User(name) { this.name = name; this.sayHi = function() { alert( "My name is: " + this.name ); }; } let john = new User("John"); john.sayHi(); // My name is: John /* john = { name: "John", sayHi: function() { ... } } */
Inside a function, we can check whether it was called with new
or without it, using a special new.target
property.
It is empty for regular calls and equals the function if called with new
:
function User() { alert(new.target); } // without "new": User(); // undefined // with "new": new User(); // function User { ... }
That can be used to allow both new
and regular calls to work the same. That is, create the same object:
function User(name) { if (!new.target) { // if you run me without new return new User(name); // ...I will add new for you } this.name = name; } let john = User("John"); // redirects call to new User alert(john.name); // John
This approach is sometimes used in libraries to make the syntax more flexible. So that people may call the function with or without new
, and it still works.
When JavaScript was created, there was an idea of a “global object” that provides all global variables and functions. It was planned that multiple in-browser scripts would use that single global object and share variables through it. Since then, JavaScript greatly evolved, and that idea of linking code through global variables became much less appealing. In modern JavaScript, the concept of modules took its place. But the global object still remains in the specification.
In a browser it is named “window”, for Node.JS it is “global”, for other environments it may have another name.
It does two things:
alert
directly or as a method of window
:
alert("Hello"); // the same as window.alert("Hello");
The same applies to other built-ins. E.g. we can use window.Array
instead of Array
.
var
variables. We can read and write them using its properties, for instance:
var phrase = "Hello"; function sayHi() { alert(phrase); } // can read from window alert( window.phrase ); // Hello (global var) alert( window.sayHi ); // function (global function declaration) // can write to window (creates a new global variable) window.test = 5; alert(test); // 5
…But the global object does not have variables declared with let
/const
!
let user = "John"; alert(user); // John alert(window.user); // undefined, don't have let alert("user" in window); // false
The usage of window is not very often though but still there are some situation where you can use Window object -
To access exactly the global variable if the function has the local one with the same name.
var user = "Global"; function sayHi() { var user = "Local"; alert(window.user); // Global } sayHi();
If you used user
instead of window.user
, then you would have gotten "Local", local value. Using Window
, you now have access to the Global object. The trick doesn’t work with let
variables.
To check if a certain global variable or a builtin exists. For instance, we want to check whether a global function XMLHttpRequest
exists. We can’t write if (XMLHttpRequest)
, because if there’s no XMLHttpRequest
, there will be an error (variable not defined). But we can read it from window.XMLHttpRequest
:
if (window.XMLHttpRequest) { alert('XMLHttpRequest exists!') }
If there is no such global function then window.XMLHttpRequest
is just a non-existing object property. That’s undefined
, no error, so it works.
Sometimes, the value of this
is exactly the global object. That’s rarely used, but some scripts rely on that.
In the browser, the value of this
in the global area is window
:
// outside of functions alert( this === window ); // true
When a function with this
is called in non-strict mode, it gets the global object as this:
// not in strict mode (!) function f() { alert(this); // [object Window] } f(); // called without an object
By specification, this in this case must be the global object, even in non-browser environments like Node.JS
. That’s for compatibility with old scripts, in strict mode this would be undefined
.