YDJKS — this & Object Prototypes: Takeaways II

My personal learnings from You Don’t Know JS: this & Object Prototypes

Anton Paras
10 min readNov 15, 2018

This is the continuation of the 3rd post in a series of 6, where each post covers my personal takeaways from a book in the You Don’t Know JS series, by Kyle Simpson.

In the previous post, we covered the first third of the third installment in the series, this & Object Prototypes. There, we discussed this and the 4 basic rules for determining its value.

In this post, we’ll cover the second third of the book, JS objects. In the next post, we’ll cover the final third of this book, Object Prototypes.

Here’s a brief Table of Contents:

  • Types & “Subtypes” of Object
  • Object Contents
  • Object Duplication
  • Property Descriptors
  • Object Immutability
  • Getters & Setters
  • Property Existence
  • Property Enumeration

Types & “Subtypes” of Object

There are 7 data types in JavaScript (6 primitives, 1 complex):

  1. boolean
  2. number
  3. string
  4. symbol
  5. null
  6. undefined
  7. Object

It’s commonly said that “everything in JS is an object”. However, this is false. Boolean, number, string, symbol, and undefined are all primitives, as is evident when you perform typeof on them.

Note that typeof null actually yields 'object', but this is a longstanding quirk that is generally recognized as a mistake. null is indeed a primitive, not an object.

In addition to the object type, there are 9 “subtypes” of object.

  1. Boolean
  2. Number
  3. String
  4. Symbol
  5. Function
  6. Array
  7. Date
  8. RegExp
  9. Error

In reality, these “subtypes” are actually built-in functions that are intended to be used for constructor calls. When you call one of these functions as a constructor (using the new operator), it produces an object of the corresponding “subtype”.

"Hello World" is a primitive. It’s not an object/subtype of an object. This may be confusing, since we often see JS code that uses the property-access operator on strings. We often see <String>.toUpperCase(), <String>.endsWith(), etc.

As we know, the property-access operator can only be used on objects.

Here’s the explanation: the JS engine boxes primitives in their corresponding Object subtypes when necessary. It’s an implicit operation. Yet, we must still distinguish primitives and boxed primitives. However, Java has a similar mechanism, so this shouldn’t be that surprising.

Object Contents

JS objects don’t truly “contain” values. They contain property names, which act as references to values.

To access an object’s property value, you use the Property Access operator, which comes in 2 notations: dot notation & bracket notation.

Dot notation myObj.propName requires a valid Identifier as the property name (alphanumeric sequence of characters including $ and _. Cannot start with a number).

Bracket notation can accept any UTF-8 compatible string as the property name.

Since bracket notation can accept a string as the property name, you can use computed strings as property names.

Prior to ES6, you couldn’t do this in object literal notation. You could only do it 1-by-1 like so:

var str1 = 'foo';
var str2 = 'bar';
var obj = {};
obj[str + str2] = 49;

Prior to ES6, if you tried this, it would fail

var str1 = 'foo';
var str2 = 'bar';
var obj = {
str1 + str2: 49
};

In ES6+, this is now possible via a feature called Computed Property Names. However, you need to enclose the expression in square brackets.

Something to note: property names in objects are always strings. If you pass a non-string value for an object’s property name, it will be coerced to a string.

Arrays are the obvious exception to this rule. Arrays are indeed objects, but they are a specialized type of object. Accessing an array like arr[<number>] will not coerce the <number> to a string, like it would with a general object.

Note: If you attempt to add an object-property to an array, and the property name looks like a number, the property name will be interpreted as an index. Thus, you will inadvertently modify the value at that index.

Duplicating Objects

Duplicating objects is an interesting situation in JS.

There are 2 main styles of object copying in programming: shallow copying & deep copying. Both types of copying produce new objects, but the difference is that

  1. Shallow copying just copies references to sub-objects. (primitives are fully duplicated)
  2. Deep copying actually duplicates sub-objects.

JavaScript facilitates shallow copying easily, through Object.assign() and Object spread properties.

Note: Property Descriptors are not preserved with shallow copying an object.

JavaScript does not support deep copying as easily, for 2 main reasons.

  1. What do you do about circular references? Do you throw an error for circular references? Do you copy everything that isn’t a circular reference?
  2. What do you do about functions? Do you make entirely new functions?

Though, it’s possible to deep copy a subset of JS objects: objects that are JSON-compliant.

For JSON-compliant objects, you can deep copy them like so:

var deepCopy = JSON.parse(JSON.stringify(obj));

Property Descriptors

All object properties have a value and 3 characteristics

  • writable : can the value be modified?
  • configurable : can the property’s characteristics be modified? (So this is sort of meta)
  • enumerable : does the property appear in object-property enumerations? (e.g. for...in and Object.keys())

Prior to ES5, there was no way to investigate/manually set these characteristics. Since ES5, there is such a way: property descriptors.

If you were to define an object property normally, like so:

var obj = {
a: 3
};

The default characteristic values are all true.

Writable
Suppose you attempt to modify a property that has writable: false. If strict mode is enabled, that will throw a TypeError. If strict mode is disabled, that will fail silently.

Configurable
If a property has configurable: false, then

  1. Its value's modifiability is still determined by writable.
  2. You can only convert writable from true to false.
  3. You cannot change configurable.
  4. You cannot change enumerable.
  5. You cannot delete the property. If strict mode is enabled, it will throw a TypeError. If strict mode is disabled, it will fail silently.

Enumerable
The enumerable characteristic controls a property’s visibility in enumerations, like for...in and Object.keys(). This will be discussed a bit more later.

Immutability

You may wish to prevent objects from changing. There are several immutability techniques based on ES5 features:

  1. Constant Properties
  2. Object.preventExtensions()
  3. Object.seal()
  4. Object.freeze()

Constant Property
You can force an object property to be constant by configuring it to be

{
writable: false,
configurable: false
}

Object.preventExtensions()
You can prevent an object from receiving additional properties by using Object.preventExtensions().

var myObj = {
a: 2
};
Object.preventExtensions(myObj);

If strict mode is enabled and you attempt to add a property to the object. It will throw a TypeError. If strict mode is disabled, it will simply fail silently.

Object.seal()
Very basically,

Object.seal() = Object.preventExtensions() + Existing Properties become non-configurable (you can still modify their values)

Object.freeze()
Also very basically,

Object.freeze() = Object.preventExtensions() + Existing Properties become constant & non-configurable

Note that you can “deep freeze” an object by recursing through its properties and applying Object.freeze() to any descendant objects.

Getters & Setters

[[Get]]
When you perform a property access on an object (e.g. myObj.a), the engine doesn’t just check for a on myObj and return the corresponding value.

Rather, the engine performs a [[Get]] operation. It’s possible to manually specify the [[Get]] operation. However, the default [[Get]] operation

  1. Checks the object for the specified property name. If it’s there, the corresponding property value is returned.
  2. Otherwise, it checks the object’s [[Prototype]] chain for the specified property name. If it’s there, the corresponding property value is returned.
  3. Otherwise, undefined is returned.

Something worth mentioning: undefined is returned for nonexistent properties. This is different behavior from nonexistent variables.

If a variable doesn’t exist, a ReferenceError is thrown. If an object property doesn’t exist, undefined is returned. It’s a small distinction, but it’s worth pointing out.

[[Put]]
As there is a [[Get]] operation, there is also a [[Put]] operation (you would think it’d be called [[Set]] :/).

Also like [[Get]], there is a default [[Put]] operation, and its behavior isn’t as straightforward as you’d think. It doesn’t just assign a value to an object property.

The primary factor in its behavior is whether or not the given property already exists.

If the specified property already exists, the default [[Put]] will proceed as follows:

  1. If the property is an accessor descriptor (discussed below), call the given “setter”.
  2. If the property is a data descriptor, with writable: false
    - If strict mode is enabled, throw a TypeError.
    - If strict mode is disabled, fail silently.
  3. Otherwise, set the value to the existing property.

If the property doesn’t exist on the object yet, some other [[Prototype]] related mechanics will kick in, which we will discuss later.

As of ES5, when defining an object property, you can add custom get and set functions. These functions enable you to perform custom operations when geting and seting a property.

You can add custom getters & setters to accessor descriptors.

Previously, we discussed data descriptors, which you can define like so:

Object.defineProperty(myObj, 'a', {
value: 3,
writable: false,
configurable: false,
enumerable: true
});

In addition to data descriptors, there are also accessor descriptors. Accessor descriptors have the following characteristics:

{
configurable,
enumerable,
get,
set
}

So, like data descriptors, accessor descriptors have configurable and enumerable. Unlike data descriptors, accessor descriptors don’t have value and writable. If you attempt to add these characteristics to accessor descriptors, a TypeError will be thrown.

You can add getters & setters via object literal syntax or explicit Object.defineProperty() syntax.

// Object literal syntaxvar obj = {
a: 2,
get b() {
return this.a * 2;
},
set b(val) {
this.a = val;
}
};
// Object.defineProperty syntax
function Dog() {
var name;
Object.defineProperty(this, 'name', {
configurable: true,
enumerable: true,
get: function () {
return name;
},
set: function (newName) {
name = newName;
}
});
}

Suppose an object’s property has a getter. If you perform a property access
operation on that property, that getter is invoked. You will get whatever the
getter returns. This is why it’s important to recognize that a [[Get]]
operation is performed each time you RHS access a property. You can’t just
assume that the property access directly returns the property’s value to you.
For the default [[Get]] operation, this is true. But as is evident with custom
getters, this is clearly not the case.

The same goes for setters.

Also, you can omit getters and setters from an accessor descriptor. If you don’t specify a getter for an accessor descriptor, attempts to access the property will simply yield undefined. If you don’t specify a setter for an accessor descriptor, you can never modify anything about the property.

These are sort of contrived situations, but worth mentioning.

Property Existence

You can check if an object has a property by using

Object.hasOwnProperty(<property-name-as-string>)

This will only check the given object’s properties. It will not check the properties of objects on its [[Prototype]] chain. The in operator is used for that, but we’ll discuss that later.

hasOwnProperty can distinguish non-existent properties from properties that exist and are explicitly set to undefined.

For example,

var obj = {
a: undefined
};
obj.a; // undefined
obj.b; // undefined
obj.hasOwnProperty('a'); // true
obj.hasOwnProperty('b'); // false

Note: hasOwnProperty is a method on Object.prototype. It’s possible that a given object doesn’t like to Object.prototype and would not have this method. E.g. objects created by Object.create(null) fit this description. In such a case, attempting to use hasOwnProperty on that object would fail.

For that case, you can do

Object.prototype.hasOwnProperty.call(<obj>, <prop-name>);

which will always work.

And a note on in's behavior: in checks if a property name exists in an object. That’s why you can’t/shouldn’t use it for arrays. It won’t work as you expect it to.

var arr = [4];4 in arr; // false'length' in arr; // true because such a property name exists on the array OBJECT

Property Enumeration

To check if a property is enumerable, use

Object.propertyIsEnumerable(<prop-name>);

There are 3 types of property enumerations, and they all behave differently.

  1. Object.keys()
  2. Object.getOwnPropertyNames()
  3. for...in

Object.keys(<obj>) acts on the given <obj> and returns all enumerable property names.

Object.getOwnPropertyNames(<obj>) acts on the given <obj> and returns ALL property names, both enumerable and non-enumerable.

for...in acts on the given <obj> and its [[Prototype]] chain and lists all enumerable property names.

That’s all for now, check back for the 3rd and final post for this book. It will cover object prototypes!

--

--