Just JavaScript by Dan Abramov
Source:
Very generally speaking, values are the entities existing in JavaScript.
There are two categories of values, each with various types:
"john"
7
and 3.14
true
and false
null
undefined
{ name: "john", score: 20 }
[1, 2, 3]
(x) => x * 2;
Sat Jul 04 2020 19:15:32 GMT-0300
/\s+/
To check the type of a value, use the typeof
operator together with the value. You will receive back a string with the name of the type.
typeof 75; // "number"
typeof 'whoa'; // "string"
typeof undefined; // "undefined"
typeof { name: 'john' }; // "object"
Primitive values are standalone entities that cannot be changed.
Numbers and strings, the best known primitives, exist in an infinite number. Each number and each string is an immutable standalone entity. (To be precise, there are 18 quintillion number values, so they are not infinite, but for our purposes here they can be treated as if they were.)
As for strings, there is one value for every conceivable string, i.e., all conceivable strings already exist and are always there to be referenced. An empty string counts as a string. All strings have built-in properties that make them seem like objects—but even so, strings are not objects.
let friend = 'john';
friend.length; // 4
friend[0]; // "j"
friend[1]; // "o"
The remaining primitives—the Boolean, Null and Undefined types—exist in a limited number: true
, false
, null
and undefined
are four immutable standalone entities. There can be no third member of the Boolean type just as much as there can be no second null
or undefined
. There are only two values of the Boolean type, only one value of the Null type, and only one value of the Undefined type.
Type is uppercased and value
is monospaced with a green background. The type Undefined is the name of the group; the value undefined
is the entity belonging to that group.
The primitive undefined
represents an unintentionally missing value—something that should be there is now missing. It often crops up when you declare a variable and try to use it without having assigned a value to it.
let pet; // variable with no value assigned
console.log(pet); // `undefined`
The primitive null
is an intentionally missing value—think of "none" or "nothing". You are asserting that there is nothing there.
null
is not an object
typeof null
returns "object"
but null
is not, and does not behave like, an object. This is a historical accident and has to be treated as an exception to the rule that typeof
always returns the real type of the primitive inside a string.
When you calculate the result of two number values like 1 + 2
, you are not "changing" any number values. You are just creating an expression containing two primitive values, an expression that evaluates to the single primitive value 3
.
When you concatenate two string values like "john " + "smith"
, you are not "changing" any string values. You are just creating an expression containing two primitive values, an expression that evaluates to the single primitive value "john smith"
.
When you combine two boolean values like true && false
, you are not "changing" any boolean values. You are just creating an expression containing two primitive values, an expression that evaluates to the single primitive value false
.
Primitives in an expression always evaluate to a single primitive, i.e. primitives combine with the operator to become a single primitive. In this sense, an expression can be thought of as a question: What is 1 + 2
? The answer is the evaluated result: 3
. When you enter an expression in the browser console, you expect a single value as an answer.
Primitive values are immutable: inalterable, unchangeable, untouchable. You are free refer to primitives in your code, but your code cannot affect primitives in any way. Primitives are visible to you, but they exist outside your reach. They always stay as they are—primitives are read-only.
Primitives cannot be changed, even when it might look like they can.
let name = 'ray';
// attempted mutation (unsuccessful)
name[1] = 'o'; // highlight-line
console.log(name); // → "ray"
Look at the highlighted line. If not in strict mode, JavaScript will ignore your command on that line, because you cannot mutate a string. In strict mode, JavaScript will throw an error.
Special values are objects, arrays, and functions. Unlike primitives, special values are standalone entities that can be changed. You code can manipulate special values. If a special value exists and your code changes it, that special value will remember and reflect that change.
There is one object value for every object literal {}
we execute. There is one function value for every function expression function() {}
we run. Unlike primitives, which exist from the beginning, special values are brought into existence by your code. The number of existing objects and functions will be that which your code has caused to exist.
Objects include key-value mappings as well as arrays, dates, regexes and others.
typeof { name: 'john' }; // "object" (key-value mapping)
typeof [1, 2, 3]; // "object" (Array)
typeof new Date(); // "object" (Date)
typeof /\w+/; // "object" (Regex)
typeof Math; // "object" (module)
Remember: primitives are immutable, so you can only refer to (i.e., "summon") primitives, but not change them. String values always exist as they are.
let brother = 'john';
let friend = 'john';
Objects are special values. Unlike primitives, special values allow for change. Objects can therefore be created, reshaped and destroyed.
let hero = { name: 'john' };
let villain = { name: 'john' };
Every time we use the object literal, we are bringing into existence a brand new object value. Here, two objects have come into existence, and each of the two labels has been connected to one of those objects.
To destroy an object, you have make sure that no label is connected to the object. If the object has no wire connected to it, that is, if the object cannot be referenced and therefore cannot be used, the garbarge collector will eventually destroy the object. You cannot destroy an object directly.
let john = {
surname: 'smith',
score: 30,
};
Here john
is a variable pointing to an object, and this object contains key-value pairs, namely the properties surname
and score
pointing to the values "smith"
and 30
respectively.
Properties are just like variables: both are labels with wires pointing to values. Properties do not contain values—properties point to values.
Accessing a property in an object is an expression:
john.surname; // → "smith"
We first evaluate the label john
to the object { surname: "smith", score: 20 }
. Now that we have the object, we find in it the property surname
and follow the property's wire to the value "smith"
. Remember that an object cannot have two properties with the same name.
If you happen to access a non-existing property in an object, as in john.spouse
, JavaScript will return undefined
. This does not mean that the object john
has that property spouse
pointing to undefined
. It simply means that the object john
does not have a property spouse
. Remember that accessing a property is an expression, a question, to be answered by JavaScript.
When you assign a value to a property, you are detaching the wire of the property from whichever target value the property was pointing to, and you reattach that wire to a new value.
john.score = 300;
An object that seems to "contain" another object, in fact, has a property pointing to that other object. Objects do not "contain" objects!
let john = {
surname: 'smith',
score: 20,
friend: { name: 'paul' },
};
The smaller object { name: "paul" }
is not "inside" the larger object { surname: "smith", score: 20, friend: { name: "paul" }}
. The two objects are two distinct values—the larger object simply has a property pointing to that smaller object. If you change the smaller object, the larger object will keep pointing to that smaller object in its changed version. If another big object has a property pointing to that small object, the change will be reflected there as well.
Properties always point at values! There are no nested objects.
If you try to access a property that does not exist on an object, JavaScript will not find it and therefore return undefined
. But you can instruct JavaScript to keep looking elsewhere by setting the hidden property __proto__
in the object.
const person = {
legs: 2,
};
const john = {
__proto__: person, // highlight-line
};
john.legs; // → 2
The expression john.legs
returns 2
. The lookup chain searches for the property legs
in the object john
, does not find the property legs
in john
, follows the hidden property __proto__
pointing to the object person
, and finds it there.
The hidden property __proto__
on the highlighted line is a property connecting an object to another object: its prototype. Any object may be connected to another (its prototype!) and the prototype chain can be indefinitely long.
If the prototype does not hold the sought-after property, then JavaScript returns undefined
. Only when it runs out of prototypes without finding the property does JavaScript return undefined
.
With __proto__
we instruct JavaScript to keep looking for any missing properties in another object. They have to be missing, so JavaScript will not keep looking if the property we are looking for is already in the first object.
The built-in method hasOwnProperty()
checks if a property exists in an object, excluding properties in an object's prototype.
Prototypes only matter when accessing (i.e., reading) properties in objects. Prototypes play no role when writing (i.e., adding) properties to objects.
The Object Prototype
The Object Prototype is the primordial object that every object points to, by default, through the hidden property __proto__
.
let john = {};
Even if we do not set the hidden property __proto__
in john
, it will still be there, pointing to the Object Prototype. This Object Prototype is where methods like hasOwnProperty()
and toString()
are stored!
let john = {};
john.__proto__; // → Object Prototype
If you cut off the __proto__
link, the ordinary object will not have access to all the methods stored in Object Prototype.
let john = {
__proto__: null,
};
john.hasOwnProperty; // → undefined
If you add a property in Object Prototype, every ordinary object will have access to the newly added property!
let john = {};
john.__proto__.soul = true;
let paul = {};
paul.soul; // → true
Do not manipulate __proto__
directly!
As with objects, whenever you create a function, you are causing a new function to come into existence—functions do not exist from the beginning, as primitives do. Functions can be created, altered and destroyed.
For example, see below how ten distinct functions hello()
are created and passed into console.log()
. We create a function every time we use a function declaration.
for (let i = 0; i < 10; i++) {
console.log(function hello() {});
}
If you assign a function to a variable, the label will point to the function itself. Assigning a function to a variable is not the same as assigning the value returned by the function to a variable.
function getName() {
return 'john';
}
let retrieveName = getName; // variable points to function
let name = getName(); // variable points to return value
console.log(retrieveName); // function itself is logged out
console.log(name); // return value is logged out
A variable is a label with a wire connected to a target value, whether primitive or special. The connection always starts at the label and ends at the target value. This wire allows you to refer to that particular value by a custom name inside your code.
let bestFriend = 'john';
When you reassign a variable, you are reconnecting (i.e. "reattaching") the wire of the label to another target value, i.e. you are removing the end of the wire from the target value, and attaching that end to another target value. The label where the wire originates, the start of the wire, remains where it is.
bestFriend = 'mary';
Below we declare a variable and assign a value to it:
But what if the right side of reassignment contains, not a value, but an expression? Remember, an expression is a combination of primitive values that can be evaluated down into a single primitive. If a reassignment contains an expression, JavaScript first evaluates the expression and then uses the evaluated result in the assignment.
let surname = 'smith';
let fullName = 'john ' + surname;
JavaScript first evaluates the expression "john " + surname
into "john " + "smith"
and that other expression into the single value "john smith"
. Having evaluated the expression to a single value, JavaScript uses that single value in the assignment let fullName = "john smith"
.
In fact, we have been referring to expressions as two or more primitives that can be evaluated down into a single result, but we can be more precise. There also exist single-primitive expressions: literal expressions. A literal is both a question and its own answer. If you input a literal in the browser console, it will answer back with that same literal. let surname = "smith";
is therefore an assignment containing an expression, because "smith"
is an expression itself. If you then enter that label in the browser console, it will answer back with the attached value.
Evaluation precedes assignment:
let a = 5;
let b = a;
a = 0;
In other words...
a
to the value 5
.a
to 5
, then connect the label b
to the value 5
.a
from wherever it was pointing to, and reattach the label a
to the value 0
.As a result, in the end a
points to 0
and b
points to 5
.
The same holds true for objects.
let first = {};
let second = first;
let third = second;
third = {};
In other words...
first
to it.first
to that original object and connect the label second
to that original object.second
to that original object and connect the label third
to that original object.third
from the original object, and reattach the label third
to this other new object.Evaluation also precedes the passing of arguments:
function addTwo(num) {
num += 2;
}
let score = 10;
addTwo(score);
console.log(score);
In other words...
addTwo
.score
to the value 10
.score
to the value 10
and then pass in the value 10
to the function. (We do not, and cannot, pass in the variable!)num
to the value 10
, then immediately evaluate num
to 10
, we evaluate 10 + 2
to 12
, we detach the label num
from the 10
it was pointing to, and we reattach the label num
to 12. The function ends and returns nothing.score
to the value 10
(see step 2) and then we log the value 10
to the console.At no point is the variable score
reassigned.