Objects: Objects: The Basics Page 1 of 47
Objects: Objects: The Basics Page 1 of 47
Objects: Objects: The Basics Page 1 of 47
Objects
As we know, there are seven data types in JavaScript. Six of them are called “primitive”, because
their values contain only a single thing (be it a string or a number or whatever).
In contrast, objects are used to store keyed collections of various data and more complex entities. In
JavaScript, objects penetrate almost every aspect of the language. So we must understand them first
before going in-depth anywhere else.
An object can be created with figure brackets {…} with an optional list of properties. A property is a
“key: value” pair, where key is a string (also called a “property name”), and value can be anything.
We can imagine an object as a cabinet with signed files. Every piece of data is stored in its file by the
key. It’s easy to find a file by its name or add/remove a file.
An empty object (“empty cabinet”) can be created using one of two syntaxes:
Usually, the figure brackets {...} are used. That declaration is called an object literal.
We can immediately put some properties into {...} as “key: value” pairs:
A property has a key (also known as “name” or “identifier”) before the colon ":" and a value to the
right of it.
1. The first property has the name "name" and the value "John".
2. The second one has the name "age" and the value 30.
Lesson 3 jyercia
Objects : The Basics Page 2 of 47
The resulting user object can be imagined as a cabinet with two signed files labeled “name” and
“age”.
user.isAdmin = true;
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
};
Lesson 3 jyercia
Objects : The Basics Page 3 of 47
let user = {
name: "John",
age: 30,
}
Square brackets
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:
// 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).
Square brackets also provide a way to obtain the property name as the result of any expression – as
opposed to a literal string – like from a variable as follows:
let key = prompt("What do you want to know about the user?", "name");
Lesson 3 jyercia
Objects : The Basics Page 4 of 47
// access by variable
alert( user[key] ); // John (if enter "name")
Computed properties
We can use square brackets in an object literal. That’s called computed properties.
For instance:
let bag = {
[fruit]: 5, // the name of the property is taken from the variable fruit
};
The meaning of a computed property is simple: [fruit] means that the property name should be taken
from fruit.
Square brackets are much more powerful than the dot notation. They allow any property names and
variables. But they are also more cumbersome to write.
So most of the time, when property names are known and simple, the dot is used. And if we need
something more complex, then we switch to square brackets.
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:
Lesson 3 jyercia
Objects : The Basics Page 5 of 47
let obj = {
for: 1,
let: 2,
return: 3
};
Basically, any name is allowed, but there’s a special one: "__proto__" that gets special treatment for
historical reasons. For instance, we can’t set it to a non-object value:
That can become a source of bugs and even vulnerabilities if we intend to store arbitrary key-value
pairs in an object, and allow a visitor to specify the keys.
In that case the visitor may choose “proto” as the key, and the assignment logic will be ruined (as
shown above).
There is a way to make objects treat __proto__ as a regular property, which we’ll cover later, but
first we need to know more about objects. There’s also another data structure Map, that we’ll learn in
the lesson Map, Set, WeakMap and WeakSet, which supports arbitrary keys.
In real code we often use existing variables as values for property names.
For instance:
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.
We can use both normal properties and shorthands in the same object:
let user = {
name, // same as name:name
age: 30
};
Existence check
A notable objects feature is that it’s possible to access any property. There will be no error if the
property doesn’t exist! Accessing a non-existing property just returns undefined. It provides a very
common way to test whether the property exists – to get it and compare vs undefined:
There also exists a special operator "in" to check for the existence of a property.
"key" in object
For instance:
Please note that on the left side of in there must be a property name. That’s usually a quoted string.
If we omit quotes, that would mean a variable containing the actual name will be tested. For
instance:
Usually, the strict comparison "=== undefined" check works fine. But there’s a special case when it
fails, but "in" works correctly.
let obj = {
Lesson 3 jyercia
Objects : The Basics Page 7 of 47
test: undefined
};
In the code above, the property obj.test technically exists. So the in operator works right.
Situations like this happen very rarely, because undefined is usually not assigned. We mostly use
null for “unknown” or “empty” values. So the in operator is an exotic guest in the code.
To walk over all keys of an object, there exists a special form of the loop: for..in. This is a
completely different thing from the for(;;) construct that we studied before.
The syntax:
Note that all “for” constructs allow us to declare the looping variable inside the loop, like let key
here.
Also, we could use another variable name here instead of key. For instance, "for (let prop in obj)" is
also widely used.
Are objects ordered? In other words, if we loop over an object, do we get all properties in the same
order they were added? Can we rely on this?
The short answer is: “ordered in a special fashion”: integer properties are sorted, others appear in
creation order. The details follow.
let codes = {
Lesson 3 jyercia
Objects : The Basics Page 8 of 47
"49": "Germany",
"41": "Switzerland",
"44": "Great Britain",
// ..,
"1": "USA"
};
The object may be used to suggest a list of options to the user. If we’re making a site mainly for
German audience then we probably want 49 to be the first.
The phone codes go in the ascending sorted order, because they are integers. So we see 1, 41, 44, 49.
The “integer property” term here means a string that can be converted to-and-from an integer without
a change.
So, “49” is an integer property name, because when it’s transformed to an integer number and back,
it’s still the same. But “+49” and “1.2” are not:
…On the other hand, if the keys are non-integer, then they are listed in the creation order, for
instance:
let user = {
name: "John",
surname: "Smith"
};
user.age = 25; // add one more
So, to fix the issue with the phone codes, we can “cheat” by making the codes non-integer. Adding a
plus "+" sign before each code is enough.
Like this:
Lesson 3 jyercia
Objects : The Basics Page 9 of 47
let codes = {
"+49": "Germany",
"+41": "Switzerland",
"+44": "Great Britain",
// ..,
"+1": "USA"
};
Copying by reference
One of the fundamental differences of objects vs primitives is that they are stored and copied “by
reference”.
Primitive values: strings, numbers, booleans – are assigned/copied “as a whole value”.
For instance:
As a result we have two independent variables, each one is storing the string "Hello!".
A variable stores not the object itself, but its “address in memory”, in other words “a
reference” to it.
let user = {
name: "John"
};
Lesson 3 jyercia
Objects : The Basics Page 10 of 47
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.
If we imagine an object as a cabinet, then a variable is a key to it. Copying a variable duplicates the
key, but not the cabinet itself.
For instance:
Now we have two variables, each one with the reference to the same object:
We can use any variable to access the cabinet and modify its contents:
The example above demonstrates that there is only one object. As if we had a cabinet with two keys
and used one of them (admin) to get into it. Then, if we later use the other key (user) we would see
changes.
Comparison by reference
The equality == and strict equality === operators for objects work exactly the same.
Two objects are equal only if they are the same object.
For instance, two variables reference the same object, they are equal:
let a = {};
let b = a; // copy the reference
And here two independent objects are not equal, even though both are empty:
let a = {};
let b = {}; // two independent objects
alert( a == b ); // false
Lesson 3 jyercia
Objects : The Basics Page 11 of 47
For comparisons like obj1 > obj2 or for a comparison against a primitive obj == 5, objects are
converted to primitives. We’ll study how object conversions work very soon, but to tell the truth,
such comparisons are necessary very rarely and usually are a result of a coding mistake.
Const object
For instance:
const user = {
name: "John"
};
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"
};
…But what if we want to make constant object properties? So that user.age = 25 would give an error.
That’s possible too. We’ll cover it in the lesson Property flags and descriptors.
So, copying an object variable creates one more reference to the same object.
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.
Like this:
let user = {
name: "John",
age: 30
};
Lesson 3 jyercia
Objects : The Basics Page 12 of 47
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.
If the receiving object (user) already has the same named property, it will be overwritten:
We also can use Object.assign to replace the loop for simple cloning:
let user = {
name: "John",
age: 30
};
let clone = Object.assign({}, user);
Lesson 3 jyercia
Objects : The Basics Page 13 of 47
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?
Like this:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
Now it’s not enough to copy clone.sizes = user.sizes, because the user.sizes is an object, it will be
copied by reference. So clone and user will share the same sizes:
Like this:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
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).
Summary
Additional operators:
Objects are assigned and copied by reference. In other words, a variable stores not the “object
value”, but a “reference” (address in memory) for the value. So copying such a variable or passing it
as a function argument copies that reference, not the object. All operations via copied references
(like adding/removing properties) are performed on the same single object.
What we’ve studied in this chapter is called a “plain object”, or just Object.
They have their special features that we’ll study later. Sometimes people say something like “Array
type” or “Date type”, but formally they are not types of their own, but belong to a single “object”
data type. And they extend it in various ways.
Objects in JavaScript are very powerful. Here we’ve just scratched the surface of a topic that is really
huge. We’ll be closely working with objects and learning more about them in further parts of the
lesson.
Tasks
1. Write the code, one line for each action:
2. Write the function isEmpty(obj) which returns true if the object has no properties, false otherwise.
Lesson 3 jyercia
Objects : The Basics Page 15 of 47
const user = {
name: "John"
};
// does it work?
user.name = "Pete";
let salaries = {
John: 100,
Ann: 160,
Pete: 130
}
Write the code to sum all salaries and store in the variable sum. Should be 390 in the example above.
For instance:
multiplyNumeric(menu);
Lesson 3 jyercia
Objects : The Basics Page 16 of 47
Please note that multiplyNumeric does not need to return anything. It should modify the object in-
place.
Garbage collection
Memory management in JavaScript is performed automatically and invisibly to us. We create
primitives, objects, functions… All that takes memory.
What happens when something is not needed any more? How does the JavaScript engine discover it
and clean it up?
Reachability
Simply put, “reachable” values are those that are accessible or usable somehow. They are guaranteed
to be stored in memory.
1. There’s a base set of inherently reachable values, that cannot be deleted for obvious reasons.
For instance:
2. Any other value is considered reachable if it’s reachable from a root by a reference or by a
chain of references.
For instance, if there’s an object in a local variable, and that object has a property referencing
another object, that object is considered reachable. And those that it references are also
reachable. Detailed examples to follow.
There’s a background process in the JavaScript engine that is called garbage collector. It monitors all
objects and removes those that have become unreachable.
A simple example
};
Here the arrow depicts an object reference. The global variable "user" references the object {name:
"John"} (we’ll call it John for brevity). The "name" property of John stores a primitive, so it’s
painted inside the object.
user = null;
Now John becomes unreachable. There’s no way to access it, no references to it. Garbage collector
will junk the data and free the memory.
Two references
user = null;
…Then the object is still reachable via admin global variable, so it’s in memory. If we overwrite
admin too, then it can be removed.
Interlinked objects
Lesson 3 jyercia
Objects : The Basics Page 18 of 47
return {
father: man,
mother: woman
}
}
Function marry “marries” two objects by giving them references to each other and returns a new
object that contains them both.
delete family.father;
delete family.mother.husband;
Lesson 3 jyercia
Objects : The Basics Page 19 of 47
It’s not enough to delete only one of these two references, because all objects would still be
reachable.
But if we delete both, then we can see that John has no incoming reference any more:
Outgoing references do not matter. Only incoming ones can make an object reachable. So, John is
now unreachable and will be removed from the memory with all its data that also became
unaccessible.
Unreachable island
It is possible that the whole island of interlinked objects becomes unreachable and is removed from
the memory.
family = null;
Lesson 3 jyercia
Objects : The Basics Page 20 of 47
It’s obvious that John and Ann are still linked, both have incoming references. But that’s not enough.
The former "family" object has been unlinked from the root, there’s no reference to it any more, so
the whole island becomes unreachable and will be removed.
Internal algorithms
We can clearly see an “unreachable island” to the right side. Now let’s see how “mark-and-sweep”
garbage collector deals with it.
Lesson 3 jyercia
Objects : The Basics Page 21 of 47
Now the objects that could not be visited in the process are considered unreachable and will be
removed:
Lesson 3 jyercia
Objects : The Basics Page 22 of 47
JavaScript engines apply many optimizations to make it run faster and not affect the execution.
Generational collection – objects are split into two sets: “new ones” and “old ones”. Many
objects appear, do their job and die fast, they can be cleaned up aggressively. Those that
survive for long enough, become “old” and are examined less often.
Incremental collection – if there are many objects, and we try to walk and mark the whole
object set at once, it may take some time and introduce visible delays in the execution. So the
engine tries to split the garbage collection into pieces. Then the pieces are executed one by
one, separately. That requires some extra bookkeeping between them to track changes, but we
have many tiny delays instead of a big one.
Idle-time collection – the garbage collector tries to run only while the CPU is idle, to reduce
the possible effect on the execution.
There are other optimizations and flavours of garbage collection algorithms. As much as I’d like to
describe them here, I have to hold off, because different engines implement different tweaks and
techniques. And, what’s even more important, things change as engines develop, so going deeper “in
advance”, without a real need is probably not worth that. Unless, of course, it is a matter of pure
interest, then there will be some links for you below.
Summary
Lesson 3 jyercia
Objects : The Basics Page 23 of 47
Symbol type
By specification, object property keys may be either of string type, or of symbol type. Not numbers,
not booleans, only strings or symbols, these two types.
Till now we’ve only seen strings. Now let’s see the advantages that symbols can give us.
Symbols
// id is a new symbol
let id = Symbol();
Upon creation, we can give symbol a description (also called a symbol name), mostly useful for
debugging purposes:
Symbols are guaranteed to be unique. Even if we create many symbols with the same description,
they are different values. The description is just a label that doesn’t affect anything.
For instance, here are two symbols with the same description – they are not equal:
If you are familiar with Ruby or another language that also has some sort of “symbols” – please
don’t be misguided. JavaScript symbols are different.
Most values in JavaScript support implicit conversion to a string. For instance, we can alert almost
any value, and it will work. Symbols are special. They don’t auto-convert.
let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string
That’s a “language guard” against messing up, because strings and symbols are fundamentally
different and should not occasionally convert one into another.
If we really want to show a symbol, we need to call .toString() on it, like here:
let id = Symbol("id");
Lesson 3 jyercia
Objects : The Basics Page 24 of 47
let id = Symbol("id");
alert(id.description); // id
“Hidden” properties
Symbols allow us to create “hidden” properties of an object, that no other part of code can occasionally
access or overwrite.
For instance, if we’re working with user objects, that belong to a third-party code and don’t have any id
field. We’d like to add identifiers to them.
As user objects belongs to another code, and that code also works with them, we shouldn’t just add any
fields to it. That’s unsafe. But a symbol cannot be accessed occasionally, the third-party code probably
won’t even see it, so it’s probably all right to do.
Also, imagine that another script wants to have its own identifier inside user, for its own purposes. That may
be another JavaScript library, so that the scripts are completely unaware of each other.
Then that script can create its own Symbol("id"), like this:
// ...
let id = Symbol("id");
There will be no conflict between our and their identifiers, because symbols are always different, even if
they have the same name.
…But if we used a string "id" instead of a symbol for the same purpose, then there would be a conflict:
// ...if later another script the uses "id" for its purposes...
Symbols in a literal
If we want to use a symbol in an object literal {...}, we need square brackets around it.
Like this:
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // not just "id: 123"
};
That’s because we need the value from the variable id as the key, not the string “id”.
For instance:
let id = Symbol("id");
let user = {
name: "John",
age: 30,
[id]: 123
};
Object.keys(user) also ignores them. That’s a part of the general “hiding symbolic properties” principle. If
another script or a library loops over our object, it won’t unexpectedly access a symbolic property.
let id = Symbol("id");
let user = {
[id]: 123
};
There’s no paradox here. That’s by design. The idea is that when we clone an object or merge objects, we
usually want all properties to be copied (including symbols like id).
Lesson 3 jyercia
Objects : The Basics Page 26 of 47
We can only use strings or symbols as keys in objects. Other types are converted to strings.
For instance, a number 0 becomes a string "0" when used as a property key:
let obj = {
0: "test" // same as "0": "test"
};
// both alerts access the same property (the number 0 is converted to string "0")
alert( obj["0"] ); // test
alert( obj[0] ); // test (same property)
Global symbols
As we’ve seen, usually all symbols are different, even if they have the same name. But sometimes
we want same-named symbols to be same entities.
For instance, different parts of our application want to access symbol "id" meaning exactly the same
property.
To achieve that, there exists a global symbol registry. We can create symbols in it and access them
later, and it guarantees that repeated accesses by the same name return exactly the same symbol.
In order to read (create if absent) a symbol from the registry, use Symbol.for(key).
That call checks the global registry, and if there’s a symbol described as key, then returns it,
otherwise creates a new symbol Symbol(key) and stores it in the registry by the given key.
For instance:
Symbols inside the registry are called global symbols. If we want an application-wide symbol,
accessible everywhere in the code – that’s what they are for.
In some programming languages, like Ruby, there’s a single symbol per name.
Symbol.keyFor
For global symbols, not only Symbol.for(key) returns a symbol by name, but there’s a reverse call:
Symbol.keyFor(sym), that does the reverse: returns a name by a global symbol.
Lesson 3 jyercia
Objects : The Basics Page 27 of 47
For instance:
The Symbol.keyFor internally uses the global symbol registry to look up the key for the symbol. So
it doesn’t work for non-global symbols. If the symbol is not global, it won’t be able to find it and
return undefined.
For instance:
System symbols
There exist many “system” symbols that JavaScript uses internally, and we can use them to fine-tune various
aspects of our objects.
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
…and so on.
For instance, Symbol.toPrimitive allows us to describe object to primitive conversion. We’ll see its use very
soon.
Other symbols will also become familiar when we study the corresponding language features.
Summary
Symbol is a primitive type for unique identifiers.
Symbols are created with Symbol() call with an optional description (name).
Symbols are always different values, even if they have the same name. If we want same-named
symbols to be equal, then we should use the global registry: Symbol.for(key) returns (creates if
needed) a global symbol with key as the name. Multiple calls of Symbol.for with the same key return
exactly the same symbol.
Lesson 3 jyercia
Objects : The Basics Page 28 of 47
1. “Hidden” object properties. If we want to add a property into an object that “belongs” to
another script or a library, we can create a symbol and use it as a property key. A symbolic
property does not appear in for..in, so it won’t be occasionally processed together with other
properties. Also it won’t be accessed directly, because another script does not have our
symbol. So the property will be protected from occasional use or overwrite.
So we can “covertly” hide something into objects that we need, but others should not see,
using symbolic properties.
2. There are many system symbols used by JavaScript which are accessible as Symbol.*. We
can use them to alter some built-in behaviors. For instance, later in the lesson we’ll use
Symbol.iterator for iterables, Symbol.toPrimitive to setup object-to-primitive conversion and
so on.
Lesson 3 jyercia
Objects : The Basics Page 29 of 47
let user = {
name: "John",
age: 30
};
And, in the real world, a user can act: select something from the shopping cart, login, logout etc.
Method examples
let user = {
name: "John",
age: 30
};
user.sayHi = function() {
alert("Hello!");
};
user.sayHi(); // Hello!
Here we’ve just used a Function Expression to create the function and assign it to the property
user.sayHi of the object.
let user = {
// ...
};
// first, declare
function sayHi() {
alert("Hello!");
};
Lesson 3 jyercia
Objects : The Basics Page 30 of 47
user.sayHi(); // Hello!
Object-oriented programming
When we write our code using objects to represent entities, that’s called an object-oriented
programming, in short: “OOP”.
OOP is a big thing, an interesting science of its own. How to choose the right entities? How to
organize the interaction between them? That’s architecture, and there are great books on that topic,
like “Design Patterns: Elements of Reusable Object-Oriented Software” by E.Gamma, R.Helm,
R.Johnson, J.Vissides or “Object-Oriented Analysis and Design with Applications” by G.Booch, and
more.
Method shorthand
user = {
sayHi: function() {
alert("Hello");
}
};
To tell the truth, the notations are not fully identical. There are subtle differences related to object
inheritance (to be covered later), but for now they do not matter. In almost all cases the shorter
syntax is preferred.
“this” in methods
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.
The value of this is the object “before dot”, the one used to call the method.
For instance:
let user = {
name: "John",
age: 30,
Lesson 3 jyercia
Objects : The Basics Page 31 of 47
sayHi() {
alert(this.name);
}
};
user.sayHi(); // John
Here during the execution of user.sayHi(), the value of this will be 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 such code is unreliable. If we decide to copy user to another variable, e.g. admin = user and
overwrite user with something else, then it will access the wrong object.
let user = {
name: "John",
age: 30,
sayHi() {
alert( user.name ); // leads to an error
}
};
If we used this.name instead of user.name inside the alert, then the code would work.
In JavaScript, “this” keyword behaves unlike most other programming languages. It can be used in
any function.
Lesson 3 jyercia
Objects : The Basics Page 32 of 47
function sayHi() {
alert( this.name );
}
The value of this is evaluated during the run-time, depending on the context. And it can be anything.
For instance, here the same function is assigned to two different objects and has different “this” in
the calls:
function sayHi() {
alert( this.name );
}
admin['f'](); // Admin (dot or square brackets access the method – doesn't matter)
The rule is simple: if obj.f() is called, then this is obj during the call of f. So it’s either user or admin
in the example above.
function sayHi() {
alert(this);
}
sayHi(); // undefined
In this case this is undefined in strict mode. If we try to access this.name, there will be an error.
In non-strict mode the value of this in such case will be the global object (window in a browser,
we’ll get to it later in the lesson Global object). This is a historical behavior that "use strict" fixes.
Usually such call is an programming error. If there’s this inside a function, it expects to be called in
an object context.
If you come from another programming language, then you are probably used to the idea of a "bound
this", where methods defined in an object always have this referencing that object.
Lesson 3 jyercia
Objects : The Basics Page 33 of 47
In JavaScript this is “free”, its value is evaluated at call-time and does not depend on where the
method was declared, but rather on what’s the object “before the dot”.
The concept of run-time evaluated this has both pluses and minuses. On the one hand, a function can
be reused for different objects. On the other hand, greater flexibility opens a place for mistakes.
Here our position is not to judge whether this language design decision is good or bad. We’ll
understand how to work with it, how to get benefits and evade problems.
let user = {
name: "John",
hi() { alert(this.name); },
bye() { alert("Bye"); }
};
On the last line there is a conditinal operator that chooses either user.hi or user.bye. In this case the
result is user.hi.
Then the method is immediately called with parentheses (). But it doesn’t work right!
As you can see, the call results in an error, because the value of "this" inside the call becomes
undefined.
user.hi();
Why? If we want to understand why it happens, let’s get under the hood of how obj.method() call
works.
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); }
}
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:
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:
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).
Reference type is a special “intermediary” internal type, with the purpose to pass information from
dot . to calling parentheses ().
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 the 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.
Lesson 3 jyercia
Objects : The Basics Page 35 of 47
For instance, here arrow() uses this from the outer user.sayHi() method:
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. Later in the lesson Arrow functions
revisited we’ll go more deeply into arrow functions.
Summary
When a function is declared, it may use this, but that this has no value until the function is called.
That function can be copied between objects.
When a function is called in the “method” syntax: object.method(), the value of this during the
call is object.
Please note that arrow functions are special: they have no this. When this is accessed inside an arrow
function, it is taken from outside.
Tasks
1. What is the result of this code?
let user = {
name: "John",
go: function() { alert(this.name) }
}
(user.go)()
But calls (1) and (2) works differently from (3) and (4). Why?
Lesson 3 jyercia
Objects : The Basics Page 36 of 47
obj = {
go: function() { alert(this); }
};
function makeUser() {
return {
name: "John",
ref: this
};
};
read() prompts for two values and saves them as object properties.
sum() returns the sum of saved values.
mul() multiplies saved values and returns the result.
let calculator = {
// ... your code ...
};
calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );
let ladder = {
step: 0,
up() {
this.step++;
},
down() {
this.step--;
},
showStep: function() { // shows the current step
alert( this.step );
Lesson 3 jyercia
Objects : The Basics Page 37 of 47
}
};
ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1
Modify the code of up, down and showStep to make the calls chainable, like this:
ladder.up().up().down().showStep(); // 1
Lesson 3 jyercia
Objects : The Basics Page 38 of 47
What happens when objects are added obj1 + obj2, subtracted obj1 - obj2 or printed using alert(obj)?
In that case objects are auto-converted to primitives, and then the operation is carried out.
In the lesson Type Conversions we’ve seen the rules for numeric, string and boolean conversions of
primitives. But we left a gap for objects. Now, as we know about methods and symbols it becomes
possible to fill it.
1. All objects are true in a boolean context. There are only numeric and string conversions.
2. The numeric conversion happens when we subtract objects or apply mathematical functions.
For instance, Date objects (to be covered in the chapter Date and time) can be subtracted, and
the result of date1 - date2 is the time difference between two dates.
3. As for the string conversion – it usually happens when we output an object like alert(obj) and
in similar contexts.
ToPrimitive
We can fine-tune string and numeric conversion, using special object methods.
The conversion algorithm is called ToPrimitive in the specification. It’s called with a “hint” that
specifies the conversion type.
"string"
For an object-to-string conversion, when we’re doing an operation on an object that expects a
string, like alert:
// output
alert(obj);
"number"
// explicit conversion
let num = Number(obj);
// less/greater comparison
let greater = user1 > user2;
Lesson 3 jyercia
Objects : The Basics Page 39 of 47
"default"
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, it’s also unclear which conversion should be done.
// 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.
To do the conversion, JavaScript tries to find and call three object methods:
Symbol.toPrimitive
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"
}
let user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
Lesson 3 jyercia
Objects : The Basics Page 40 of 47
}
};
// 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.
toString/valueOf
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:
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}"}`;
},
};
Often we want a single “catch-all” place to handle all primitive conversions. In this case we can
implement toString only, like this:
let user = {
name: "John",
toString() {
return this.name;
}
Lesson 3 jyercia
Objects : The Basics Page 41 of 47
};
In the absence of Symbol.toPrimitive and valueOf, toString will handle all primitive conversions.
Return types
The important thing to know about all primitive-conversion methods is that they do not necessarily
return the “hinted” primitive.
The only mandatory thing: these methods must return a primitive, not an object.
Historical notes
For historical reasons, if toString or valueOf returns an object, there’s no error, but such value is
ignored (like if the method didn’t exist). That’s because in ancient times there was no good “error”
concept in JavaScript.
Further operations
An operation that initiated the conversion gets that primitive, and then continues to work with it,
applying further conversions if necessary.
For instance:
let obj = {
toString() { // toString handles all conversions in the absence of other methods
return "2";
}
};
Binary plus checks the primitive – if it’s a string, then it does concatenation, otherwise it performs
ToNumber and works with numbers.
String example:
let obj = {
toString() {
return "2";
}
};
Lesson 3 jyercia
Objects : The Basics Page 42 of 47
Number example:
let obj = {
toString() {
return true;
}
};
Summary
The object-to-primitive conversion is called automatically by many built-in functions and operators
that expect a primitive as a value.
The specification describes explicitly which operator uses which hint. There are very few operators
that “don’t know what to expect” and use the "default" hint. Usually for built-in objects "default"
hint is handled the same way as "number", so in practice the last two are often merged together.
In practice, it’s often enough to implement only obj.toString() as a “catch-all” method for all
conversions that return a “human-readable” representation of an object, for logging or debugging
purposes.
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 function
Lesson 3 jyercia
Objects : The Basics Page 43 of 47
Constructor functions technically are regular functions. There are two conventions though:
For instance:
function User(name) {
this.name = name;
this.isAdmin = false;
}
alert(user.name); // Jack
alert(user.isAdmin); // false
function User(name) {
// this = {}; (implicitly)
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.
Let’s note once again – technically, any function can be used as a constructor. That is: any function
can be run with new, and it will execute the algorithm above. The “capital letter first” is a common
agreement, to make it clear that a function is to be run with new.
new function() { … }
If we have many lines of code all about creation of a single complex object, we can wrap them in
constructor function, like this:
Lesson 3 jyercia
Objects : The Basics Page 44 of 47
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.
Advanced stuff
The syntax from this section is rarely used, skip it unless you want to know everything.
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 inside the function to know whether it was called with new, “in constructor mode”,
or without it, “in regular mode”.
We can also make both new and regular calls to do the same, like this:
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;
}
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.
Lesson 3 jyercia
Objects : The Basics Page 45 of 47
Probably not a good thing to use everywhere though, because omitting new makes it a bit less
obvious what’s going on. With new we all know that the new object is being created.
Usually, constructors do not have a return statement. Their task is to write all necessary stuff into
this, and it automatically becomes the result.
In other words, return with an object returns that object, in all other cases this is returned.
function BigUser() {
this.name = "John";
return { name: "Godzilla" }; // <-- returns an object
}
And here’s an example with an empty return (or we could place a primitive after it, doesn’t matter):
function SmallUser() {
this.name = "John";
return; // finishes the execution, returns this
// ...
}
Usually constructors don’t have a return statement. Here we mention the special behavior with
returning objects mainly for the sake of completeness.
Omitting parentheses
Omitting parentheses here is not considered a “good style”, but the syntax is permitted by
specification.
Methods in constructor
Using constructor functions to create objects gives a great deal of flexibility. The constructor
function may have parameters that define how to construct the object, and what to put in it.
Lesson 3 jyercia
Objects : The Basics Page 46 of 47
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 );
};
}
/*
john = {
name: "John",
sayHi: function() { ... }
}
*/
Summary
Constructor functions or, briefly, constructors, are regular functions, but there’s a common
agreement to name them with capital letter first.
Constructor functions should only be called using new. Such a call implies a creation of empty
this at the start and returning the populated one at the end.
JavaScript provides constructor functions for many built-in language objects: like Date for dates, Set for
sets and others that we plan to study.
In this lesson we only cover the basics about objects and constructors. They are essential for learning
more about data types and functions in the next chapters.
After we learn that, we return to objects and cover them in-depth in the lessons Prototypes, inheritance
and Classes.
Tasks
1. Is it possible to create functions A and B such as new A()==new B()?
let a = new A;
let b = new B;
Lesson 3 jyercia
Objects : The Basics Page 47 of 47
alert( a == b ); // true
read() asks for two values using prompt and remembers them in object properties.
sum() returns the sum of these properties.
mul() returns the multiplication product of these properties.
For instance:
Store the “current value” in the property value. The starting value is set to the argument
of the constructor startingValue.
The read() method should use prompt to read a new number and add it to value.
In other words, the value property is the sum of all user-entered values with the initial value
startingValue.
Lesson 3 jyercia