Object properties
What are properties?
Object properties are basically JavaScript variables, except they are attached to objects.
The property name is called a key (which involves more than just an identifier, as explained below).
The assigned value is either a primitive or an object.
Unassigned properties are initialized with the value undefined
.
When using the same key for multiple properties, the last property will overwrite the rest.
The most common ways to assign a property to an object is within an object literal (via key: value,
) or
by using the dot-notation (object.identifier = value;
). Both will be explained in detail in this and next chapters.
A property with a function value is called a "method", which, in JavaScript, is merely a name for this kind of properties. In JavaScript, a method is a property that acts just as any other property.
Property identifiers
Valid identifiers are only allowed to contain letters, $
, _
, and digits (0-9), but may not start with a digit.
However, property names (keys) do not have to be valid identifiers:
keys may include spaces, hyphens, they can start with a number or even consist of only digits.
In JavaScript all keys are converted to strings (unless they are symbols).
A key can be any string (case-sensitive), including an empty string.
In an object literal we can use any valid key. In a dot-notation only valid identifiers are allowed.
// object literal:
const myObject = {
myProp: 1,
myProp: "Hello y'all",
propObj: {myProp: true},
a: 1,
"a": 2,
313: "Some value",
"my prop": null
}
console.log(myObject.myProp); // logs: "Hello y'all"
console.log(myObject.propObj.myProp); // logs: true
console.log(myObject.unassignedProp); // logs: undefined
console.log(myObject.a); // logs: 2
// console.log(myObject.9InvalidIdentifier); // SyntaxError: identifier starts immediately after numeric literal
In the object literal in the example above keys a
and "a"
refer to the same property.
You may enclose keys in quotes, but common practice is to omit them, unless
the key is not a valid identifier: then quotation marks are required.
An exception is a key being a number. Although not valid identifiers, number keys do not require quotation marks,
in fact, common practice is to omit quotation marks, just like with valid identifiers.
Creating properties
Properties can be created and assigned to an object directly at creation of the object (explained in the next chapter),
by using Object.defineProperty()
(explained below), or
by using the dot-notation or square bracket notation, as shown in the next example.
const bird = {};
const name = "Tweety";
function fly() {
return "Flap flap";
};
bird.plumageColor = "yellow"; // dot-notation
bird["species"] = "canary"; // square bracket notation
bird.name = name;
bird.fly = fly;
console.log(bird.plumageColor); // logs: "yellow"
console.log(bird.species); // logs: "canary"
console.log(bird.name); // logs: "Tweety"
console.log(bird.fly()); // logs: "Flap flap"
Accessing properties
The dot-notation (objectName.propertyName
)
or square bracket notation (objectName[propertyName]
) can be used to create properties, but they can also be used to access properties, i.e., to read them
or to assign (new) values to them.
The dot-notation requires that the property name is a valid identifier (and not enclosed in quotation marks). To access properties with non-valid identifiers, you need to use the square bracket notation instead of the dot-notation. The key in the square brackets needs to be in quotation marks (a string literal), except when the key is a number. If the key is a valid identifier and not in quotation marks, it is interpreted as a variable reference: thus the bracket notation can also be used to assign variable values to property names. The square bracket notation can also be used to access computed keys.
const myObject = {};
myObject["my property"] = "Hello World!";
myObject.myProperty = "My property name is a valid variable identifier";
// myObject[myProperty] = "Hello World!"; // ReferenceError: myProperty is not defined
myObject[""] = "I'm a property without a name";
myObject[3] = 3;
console.log(myObject[3] === myObject["3"]); // logs: true
console.log(myObject[4 - 1]); // logs: 3
let myVariable = {};
myObject[myVariable] = true;
myVariable = null;
myObject[myVariable] = false;
console.log(myObject[myVariable] === myObject["null"]); // logs: true
console.log(myObject.myVariable); // logs: undefined
for (key in myObject) {
console.log(`${key} (${typeof key}) : ${myObject[key]} (${typeof myObject[key]})`);
}
/* logs (in arbitrary order):"
// 3 (string) : 3 (number)
// my property (string) : Hello World! (string)
// myProperty (string) : My property name is a valid variable identifier (string)
// (string) : I'm a property without a name (string)
// [object Object] (string) : true (boolean)
// null (string) : false (boolean)
"*/
If the key is an array, it will convert to a string by concatenating the array elements, separated by commas, to one string
(like method array.join(',')
would do).
const myObject = {};
let myVariable = ["A", true, 2];
myObject[myVariable] = 1;
console.log(myObject); // logs: { "A,true,2": 1 }
console.log(myObject["A,true,2"]); // logs: 1
myVariable = [];
myObject[myVariable] = 2;
console.log(myObject[""]); // logs: 2
// .join(',') also works but is redundant:
myVariable = ["A", true, 2].join(',');
myObject[myVariable] = 3;
console.log(myObject["A,true,2"]); // logs: 3
Using the dot-notation is often referred to as chaining.
An expression like a.b.c.b
is a "chain" of objects being each others properties.
Chaining is very common in JavaScript (and many other languages).
const a = {
b: function() {
return { c: {d: 313}}
}
}
console.log(a.b().c.d); // logs: 313
Optional chaining (?.
)
Declaring a property without explicit initialization assigns the default value undefined
(as with variables).
Trying to access a property of a property that has a nullish value
(null
or undefined
), will throw an error.
Calling a method (or any function) that does not exist throws an error.
const myObj = {}
console.log(myObj.prop); //logs: undefined // (declaration without explicit initialization)
console.log(myObj.prop.a); // TypeError: myObj.prop is undefined
console.log(myObj.method()); // TypeError: myObj.method is not a function
We can use optional chaining (?.
) to prevent the errors.
If the value of the property before ?.
is nullish, the expression short circuits and returns undefined
instead of throwing an error.
When used with function calls, and the function does not exist, it returns undefined
instead of throwing an error.
const myObj = {}
console.log(myObj.prop); //logs: undefined
console.log(myObj.prop?.a); //logs: undefined
console.log(myObj.method?.()); //logs: undefined
Using ?.
in an optional function call is not really "chaining".
So, to be more precise, the ?.
operator is used for optional function calls and for optional chaining.
If, in optional chaining, the directly preceding operand is nullish, the evaluation of the chaining expression will be terminated. Subsequent property accesses or method calls in the chaining will be ignored.
const myObj = null;
let x = 0;
const prop1 = myObj?.doSomething(x++);
console.log(x); // logs: 0 // x was not incremented
const prop2 = myObj?.a.b;
// This does not throw an error, because evaluation has already stopped at
// the first optional chain
Optional chaining cannot be used on non-declared objects and you cannot assign a value to an optional chaining expression.
const myNullish = null;
console.log(myNullish?.prop); //logs: undefined
console.log(myObj?.prop); // ReferenceError: myObj is not defined
const someObj = { prop: {} }
someObj.prop?.a = 2; // SyntaxError: invalid assignment left-hand side
The optional chaining operator can also be used with bracket notation.
const myObj = {}
console.log(myObj.prop?.["my prop"]); //logs: undefined
console.log(myObj[313]?.["my prop"]); //logs: undefined
let myArray;
console.log(myArray?.[1]); // logs: undefined
console.log(myArray[1]); // TypeError: myArray is undefined
Suppose we want to get an HTML element's text content and store it in a constant.
const text = document.querySelector('#my_elem').textContent;
// TypeError: document.querySelector(...) is null
If that HTML element does not exist, querySelector
returns null
.
Accessing the textContent
property subsequently throws an error.
To avoid the error, we need to confirm document.querySelector(...)
to be non-nullish
before accessing the textContent
property.
const elm = document.querySelector('#my_elem');
const text =
(elm === null || elm === undefined) ? undefined : elm.textContent;
console.log(text); // logs: undefined
Or with optional chaining:
const text = document.querySelector('#my_elem')?.textContent;
console.log(text); // logs: undefined
In the next example the nullish coalescing operator (??
) is used
after optional chaining in order to provide a default value for when the optional chaining returns undefined
.
function getUserName(user) {
const userName = user?.name ?? "No name available";
return userName;
}
let undef;
console.log(getUserName(undef)); // logs: "No name available"
console.log(getUserName(5)); // logs: "No name available"
console.log(getUserName()); // logs: "No name available"
console.log(getUserName({
name: "Jake",
country: "UK",
})); // logs: "Jake"
// The function also provides the default value "No name available"
// if "user" is an object without the "name" property, although this is not because of the
// optional chaining. This is because declaring a property without explicit initialization
// assigns the default value "undefined".
console.log(getUserName({
country: "The Netherlands",
})); // logs: "No name available"
Property attributes and Object.defineProperty()
Properties have internal attributes or flags, often denoted in two pairs of square brackets.
Property attributes are for instance [[enumerable]]
, [[configurable]]
or [[writable]]
, all three set to true
by default, or the property's [[value]]
, set to undefined
by default (as we have seen).
With method Object.defineProperty()
you can define a new property on an object or modify an existing property on an object and,
at the same instant, set or change the attributes of that property.
const myObject = {};
Object.defineProperty(myObject, 'someProperty', {
value: 313,
writable: true,
enumerable: true,
configurable: true
});
console.log(myObject.someProperty); // logs: 313
BTW. Also method Object.defineProperties()
is available to define multiple properties with their attributes.
Once you defined a property as non-configurable, you cannot use Object.defineProperty()
again to change the property's attributes (you'll get an error).
You cannot even change the property to configurable.
const myObject = {};
Object.defineProperty(myObject, 'someProperty', {
value: 313,
configurable: false
});
Object.defineProperty(myObject, 'someProperty', {
value: 314
}); // logs: TypeError: can't redefine non-configurable property "someProperty"
Object.defineProperty(myObject, 'someProperty', {
configurable: true
}); // logs: TypeError: can't redefine non-configurable property "someProperty"
The attributes of a property are listed as properties in an object: {value: 313, writable: true, enumerable: true, configurable: true}
.
This object is called the property's descriptor.
Each property is "under the hood" associated with its own descriptor.
You can use method Object.defineProperty()
to set the property's descriptor (as shown above), and
you can use method Object.getOwnPropertyDescriptor()
to return the property's descriptor.
const myObject = {};
Object.defineProperty(myObject, 'someProperty', {
value: 313
});
console.log(myObject.someProperty = 314); // logs: 314
const descriptor = Object.getOwnPropertyDescriptor(myObject, 'someProperty');
console.log(descriptor); // logs: { value: 313, writable: false, enumerable: false, configurable: false }
Note that changing the property's value through regular dot notation or bracket notation does not change its attribute [[value]]
.
Conversely, changing the [[value]]
attribute does change the property's value.
Also note that, contrary to defining a property via simple assignment or via an object literal, using Object.defineProperty()
to define a property will set
[[enumerable]]
, [[configurarable]]
and [[writable]]
to default value false
.
Enumerability of properties
The [[enumerable]]
attribute determines whether or not a property is visited when the object's properties are traversed or enumerated,
such as in for...in
loops or the
Object.keys
method.
Traversing object properties may also use the object's iterator,
such as in for...of
loops.
Objects with an iterator are called iterables. Iterating over iterables is different from enumerating object properties.
Loops and methods that enumerate properties can generally not be used to iterate over properties using the iterator, and vice versa.
Unlike iterating over iterables, enumerating properties happens in an arbitrary order that is browser/platform dependent.
Iterating iterables will be explained later this tutorial.
// create an object with { b: 2 } as its prototype:
const myObject = Object.create({ b: 2 });
myObject.a = 1;
Object.defineProperty(myObject, 'c', {
enumerable: false,
value: 3
});
console.log(myObject.c); // logs: 3
// next for...in loop traverses all of the enumerable properties,
// including those in the prototype chain.
// Property c is skipped because it is not enumerable.
for (let propertyName in myObject) {
console.log(propertyName + ": " + myObject[propertyName]);
}
// logs:
// "a: 1"
// "b: 2"
// Object.keys returns an array with the names ("keys")
// of only the enumerable own properties,
// so not of those in the prototype chain.
console.log(Object.keys(myObject)); // logs: [ "a" ]
Note that all properties, enumerable or not, string or symbol, own or inherited, can be accessed with dot notation or bracket notation.
Next to for...in
and Object.keys
, JavaScript provides a number of built-in methods to query or traverse object properties.
Whether a querying method returns true
or false
or whether a property may be visited when traversing the properties,
depends on the property being enumerable or not,
on having a string key or symbol key, and
on the property being the object's own or inherited.
MDN web docs article
"Enumerability and ownership of properties"
provides an overview of all methods and how they treat the different types of properties.
To see if a specified property is in a specified object, you can use the in
operator,
or, if you want to check for only non-inherited properties, you can use the Object.hasOwn()
method.
const name = {
firstName: "John",
lastName: "Doe",
fullName: function() {
return this.firstName+" "+this.lastName;
}
}
console.log('firstName' in name); // logs: true
console.log('toString' in name); // logs: true // toString is an inherited method
console.log(Object.hasOwn(name, 'firstName')); // logs: true
console.log(Object.hasOwn(name, 'toString')); // logs: false
Accessor functions
Accessor functions (not to be confused with "property accessors" being the dot notation or the bracket notation) are also called "accessor methods" or simply "accessors". There are two types of accessor functions; getters and setters, the latter also called mutator methods.
- A getter is a function, bound to an object property, that gets the value of that property when the property is accessed.
- A setter is a function, bound to an object property, that sets the value of that property when the property is assigned a value.
We call a property with a setter and/or getter a accessor property. Accessor properties are not methods, although they look like methods. Accessor properties do not have a function value, in fact, they do not have a value at all, as will be explained below. A property may have a setter, but not a getter, or vice versa, but most often a setter is used in conjunction with a getter on the same property, as will be explained below.
In previous examples, objects were presented that had a property storing the first name and a property storing the last name of a person and a
fullName
method that returned the combination of both first and last name.
Instead of a method fullName
we could also use a getter, which is kind of similar to using a method, but with a few distinctive differences.
Attempting to directly assign a value to a getter property will not change the property: the property will always and only return the return value of the getter function.
If fullName
in the example below would have been a method, it could have been overwritten, which would have destroyed the construct that this property's
value (only) depends on two other properties. You can say that property fullName
(that only has a getter but no setter) is now read-only
(which could also have been achieved by changing a method's attributes [[writable]]
or [[configurable]]
).
const person = {
firstName: "John",
lastName: "Doe",
// define a getter by prefixing the getter function with keyword "get":
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
console.log(person.fullName); // logs: "John Doe"
person.fullName = "Jane Roe"; // In strict mode this statement throws: TypeError: setting getter-only property "fullName"
console.log(person.fullName); // logs: "John Doe"
person.fullName(); // throws a TypeError: person.fullName is not a function
PS: Note that setting a property with only a getter throws a TypeError
in strict mode.
In non-strict mode, the assignment (person.fullName = "Jane Roe";
) is silently ignored.
Note that person.fullName
is not a method call; in fact a method call
person.fullName()
throws an error.
Also note that the getter function name refers to the name of the property (the key), not to the name of the getter itself, which is rather confusing.
Furthermore, a getter function cannot have any parameters.
A setter function must have precisely one parameter.
Assigning a value to a setter property always invokes the setter function.
In most cases a setter function is used in conjunction with a getter function for the same property, to get the property's value.
This creates a "pseudo-property", as if the property is a regular property, but
with the possibility of returning a computed, composed or conditional value.
Something like the example below could have been achieved with separate getName
and setName
methods instead,
but not as compact and intuitive as with getter and setter.
const person = {
get name() {
return this._name;
},
set name(value) {
value = value.trim(); // removing possible whitespace from both ends of the string
if (value === '') { return }
this._name = value;
}
}
console.log(person.name); // logs: undefined
person.name = "John Doe"; // Assigning a value to a setter property invokes the setter function.
console.log(person.name); // logs: "John Doe"
person.name = " ";
console.log(person.name); // logs: "John Doe"
person.name("John Doe"); // logs: TypeError: person.name is not a function
PS: Note that a class
would be more appropriate for person
than an object
.
In fact, getter/setter constructions are most suitable in classes.
Later this tutorial more about classes.
Again, person.name
is not a method call, in fact, person.name("John Doe")
throws an error.
Note that the setter implicitly introduces a "hidden" third property: _name
.
This property is said to be private or protected (as opposed to public properties):
the property is not supposed to be accessed from outside the object, although JavaScript is not enforcing inner access in the object only.
In classes, JavaScript enforces inner access though.
A widespread convention is that such properties in objects are prefixed with an underscore _
.
What will console.log(person._name);
log to the console?
As with a getter, it is not possible to have a setter on a property that, at the same time, holds an actual value.
Accessing a property with a setter bound, and not a getter, always returns undefined
.
const myObj = {
set prop(value) { }
}
myObj.prop = "Hello";
console.log(myObj.prop); // logs: undefined
Be aware that something like the next example creates infinite recursion:
this.prop = value
invokes the setter again and again and...
const myObj = {
set prop(value) {
this.prop = value;
}
}
myObj.prop = "Hello"; // InternalError: too much recursion
Technically you can use a setter to set an other property than the one the setter is bound to, but it is probably more appropriate to use a method for cases like this.
const myObj = {
prop: 2,
set setProp(value) {
this.prop = this.prop * value;
}
}
myObj.setProp = 2;
console.log(myObj.prop); // logs: 4
Data descriptor & accessor descriptor
So, accessor properties do not have a value, hence, they cannot have a [[value]]
attribute.
Any property's property descriptor must be of one of the next two flavors:
- Accessor descriptor containing a getter (
[[get]]
attribute) and a setter ([[set]]
attribute) and not a[[value]]
and[[writable]]
attribute. - Data descriptor, containing a
[[value]]
, which may or may not be[[writable]]
and not a[[get]]
and[[set]]
attribute.
A property with a data descriptor is a data property and a property with an accessor descriptor is an accessor property.
You can use method Object.defineProperty()
to set the property's descriptor, and
you can use method Object.getOwnPropertyDescriptor()
to return the property's descriptor. This provides an other way to define a setter and getter on a property than
defining them within an object literal, as used so far.
function Person(age) {
this.age = age;
}
const person1 = new Person(45);
// "person1" is an object created with constructor function "Person"
// Object "person1" received data property "age" via this constructor function
console.log(person1.age); // logs: 45
Object.defineProperty(person1, 'name', {
get() { return this._name; },
set(value) {
value = value.trim();
if (value === '') { return }
this._name = value;
},
enumerable: true,
configurable: true
});
console.log(person1.name); // logs: undefined
person1.name = "John Doe";
console.log(person1.name); // logs: "John Doe"
person1.name = " ";
console.log(person1.name); // logs: "John Doe"
console.log(Object.getOwnPropertyDescriptor(person1, 'age')); // logs: { value: 45, writable: true, enumerable: true, configurable: true }
console.log(Object.getOwnPropertyDescriptor(person1, 'name')); // logs: { get: get(), set: set(value), enumerable: true, configurable: true }
PS: Constructor functions will be explained in the next chapter.
Deleting properties
With the delete
operator you can delete an own property of an object, including getter and getter properties.
To delete a property,
you can use a statement in which the delete
operator is followed by the property in dot-notation or in square bracket notation.
const User = {
name: "full name",
avatar: "nickname"
};
const user1 = Object.create(User);
user1.name = 'Marge Bouvier';
user1.nComments = 2;
console.log(user1.nComments); // logs: 2
delete user1.nComments;
console.log(user1.nComments); // logs: undefined
// after deletion of an own property the object will use the property from the prototype chain, if it exists:
delete user1["name"];
console.log(user1["name"]); // logs: 'full name'
// 'delete' only deletes own properties:
delete user1.avatar;
console.log(user1.avatar); // logs: 'nickname'
After deletion, both the value and the property are removed. Non-configurable properties cannot be deleted (in strict mode it throws an error).
const myObject = {a: 1, b: 2};
delete myObject.a;
myObject.b = undefined;
console.log(myObject.a); // logs: undefined
console.log(myObject.b); // logs: undefined
console.log('a' in myObject); // logs: false
console.log('b' in myObject); // logs: true
Object.defineProperty(myObject, 'c', {
value: 313,
configurable: false
});
// in strict mode next will throw a TypeError.
delete myObject.c;
console.log(myObject.c); // logs: 313
Deleting from the global object
Generally, variables and constants are not properties of an object, so they cannot be deleted (in strict mode it throws an error).
However, var
variable declarations in global scope do create properties of the
global object,
but they also cannot be deleted: they are set non-configurable at creation. The same thing holds for function declarations in global scope.
let a = 2;
console.log('a' in globalThis); // logs: false
console.log(Object.getOwnPropertyDescriptor(globalThis, 'a')); // logs: undefined
delete a; // this has no effect; in strict mode this will throw a TypeError.
console.log(a); // logs: 2
var a = 2;
console.log('a' in globalThis); // logs: true
console.log(Object.getOwnPropertyDescriptor(globalThis, 'a')); // logs: { value: 2, writable: true, enumerable: true, configurable: false }
delete globalThis.a; // this has no effect; in strict mode this will throw a TypeError.
console.log(globalThis.a); // logs: 2
function f() {};
console.log('f' in globalThis); // logs: true
console.log(Object.getOwnPropertyDescriptor(globalThis, 'f')); // logs: { value: f(), writable: true, enumerable: true, configurable: false }
delete f; // this has no effect; in strict mode this will throw a TypeError.
console.log('f' in globalThis); // logs: true
PS: In the above examples globalThis
is used to reference the global object.
The globalThis
reference is the modern and now standard way to access the global object across environments, whereas
in the past, accessing the global object required different references in different JavaScript environments
(window
in a browser, global
in Node.js etc.).
In non-strict mode, creating an implicit global variable
(assigning a value to an undeclared variable) also creates a property of the global object. But now
the property is configurable (it is not a var
variable), so now you can delete the property from the global object.
Of course it is also possible to directly assign a property to the global object.
// In strict mode, "a = 5" in the function declaration throws an error.
(function f() { a = 5; })();
console.log('a' in globalThis); // logs: true
delete a;
console.log('a' in globalThis); // logs: false
globalThis.b = 2;
console.log('b' in globalThis); // logs: true
delete globalThis.b;
console.log('b' in globalThis); // logs: false