Understanding JavaScript Accessors: Getters & Setters

Note: This post is work-in-progress learning-note and still in active development and updated regularly.

In the previous learning-note post Deep Dive into JavaScript Property Descriptors, we discussed briefly about property descriptor attributes – writable, enumerable, configurable and how their modification affects property enumeration with for..in loop or Object.keys() method and their use in JSON.stringify() method.

In this learning-note post, defining accessor properties, getters, setters, and creating object properties with object initilizers and <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty" target="_blank" rel="noopener">Object.DefineProperty</a> methods are discussed.

To quote from the MDN:

Property descriptors present in objects come in two main flavors: data descriptors and accessor descriptors. A data descriptor is a property that has a value, which may or may not be writable. An accessor descriptor is a property described by a getter-setter pair of functions. A descriptor must be one of these two flavors; it cannot be both.

Difference between Function & Getter

Let examine the difference between an object created with function property and an accessor descriptor properties (get and set keywords -which are pair of functions too).

// create myCar with two properties
let myCar = {
    make: "Toyota",
    model: "Camry",
    myCarFull: function() {
        return this.make + " " + this.model;
    }
};
//access on console
console.log(myCar.myCarFull());//=> Toyota Camry

In the example above, myCar object three properties: make and model are strings (lines: 3-4) and  myCarFull method is a function (lines: 5-7). To access myCarFull property value, it should be called as myCarFull() function on the console(line: 10).

Now lets create the same myCar object using accessor properties as shown below:

//define myCar object using accessor 
let myCar = {
    make: "Toyota",
    model: "Camry",
    get myCarFull() {
        return this.make + " " + this.model;
    }
};
//display using getter
console.log(myCar.myCarFull); //no function call
//OUTPUT
Toyota Camry

In the example above, myCarFull property is defined using get accessor keyword myCarFull() a function (lines: 15-17)- which looks similar to myCarFull method in previous example (lines: 5-7). To access myCarFull property value, we can call with much simpler syntax like other object property – myCar.myCarFull (lines: 20-22).

The accessor descriptor properties contain (i) simple syntax, (ii) similar syntax for properties and methods, (iii) provide secure & better data quality, and (iv) useful to create a virtual properties & doing behind scenes process.

Accessor Descriptors – The Basic

Basic Syntax
//GETTER SYTAX
{get propName() { ... } } 
//expression for computed property name
{get [expression]() { ... } } 

//SETTER SYNTAX
{set propName(value) { . . . }}
//expression for computed property name
{set [expression](value) { . . . }}

The ECMAScript 2015 (ES6) introduced shorter syntax for getters and setters. In the example above the getter parameters and setter parameters are defined as follows:

  • propName: A property name associated with the getter or setter function.
  • expression: An expression for a computed property name associated with the getter or setter function.
  • value: An alias name for the variable that hold value to be assign to propName.

As defined on MDN (above), descriptors for data properties and accessor properties are different. The accessor descriptor properties do not contain value and writable attributes, but they contain get and set functions, instead. The enumerable and configurable attributes are common in the both.

The accessor properties have the following descriptor attributes:

  • <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get" target="_blank" rel="noopener">get</a> – a function which serves as a getter for the property or undefined if no getter.
  • <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set" target="_blank" rel="noopener">set</a> – a function which serves as a setter for the property, or undefined if no setter.
  • enumerable – this is same as data properties
  • configurable – this is also same as data properties
Creating a Simple Object

Creating a simple object with data property descriptor Object.defineProperty() method.

//create a new object
let myCar = {} //empty object

//add property with data property descritor
Object.defineProperty(myCar, 'make', {
  value: 'Toyota',
  writable: true,
  enumerable: true,
  configurable: true
}); //=> {make: "Toyota"}
//access on console
console.log(myCar.make);//=> Toyota

As shown in the example, the myCar object is created and make property is assigned with 'Toyota' as its value. In the example below, using accessor descriptor, a year property is added to myCar object. Using the ES6 syntax (lines: 13-16). we get the exact same result.

//assign another property with Accessor descriptor
let year = 2018;
Object.defineProperty(myCar, 'year', {
   get() { return year; },
   set(yearMade) { year = yearMade; },
   enumerable: true,
   configurable: true
});
//access on console
console.log(myCar.year); //OUTPUT => 2018

// with ES6 - assign another property with Accessor descriptor
let year = 2018;
Object.defineProperty(myCar, 'year', {
    get: function() { return year; },
    set: function(yearMade) { year = yearMade; },
    enumerable: true,
    configurable: true
});
//access on console
console.log(myCar.year);//OUTPUT => 2018

The value & get descriptor can’t be mixed (as shown below) and throws a TypeError.

// add property and define with value
Object.defineProperty(myCar, 'model', {
  value: 'Lexus,
  get() { return model; }
}); // throws typeError
//OUTPUT
Uncaught TypeError: Invalid property descriptor.
Cannot both specify accessors and a value or writable attribute,

To quote from The Modern JavaScript Tutorial:

Accessor properties are only accessible with get/set

  • A property can either be a “data property” or an “accessor property”, but not both.
  • Once a property is defined with get prop() or set prop(), it’s an accessor property. So there must be a getter to read it, and must be a setter if we want to assign it.
  • Sometimes it’s normal that there’s only a setter or only a getter. But the property won’t be readable or writable in that case.

Defining Getters & Setters

Getters and Setters can be defined either using an object initilizers, or it can also be added to any object at any time using getter and setter method.

1. With Object Initializer

This creates a virtual or pseudo property myCar (line 6) for the car object (line 2).

//create and initialize car object
let car = {
  make: "Toyota",
  model: "Camry",

  get myCar() {
    return `${this.make} ${this.model}`;
  },

  set myCar(value) {
    [this.make, this.model] = value.split(" "); 
    //can also be written as
    // value = value.split(" ");
  }
};
// access myCar property value on console
console.log(car.myCar);//OUTPUT => Toyota Camry

// set myCar is executed with the given value
car.myCar = "Honda Civic";
//access car property on console
console.log(car.myCar);//OUTPUT =>Honda Civic
console.log(car.make); //=> Honda
console.log(car.model);//=> Civic

When a new value is assigned to myCar (line 20), it runs the setter which assigns "Honda Civic" to myCar value array. Therefore, when access myCar value it outputs "Honda Civic"(line 22).

Tip: The MDN documentation states that “it is not possible to simultaneously have a getter bound to a property and have that property actually hold a value, although it is possible to use a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#Defining_a_getter_on_new_objects_in_object_initializers" target="_blank" rel="noopener">getter</a> and a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set#Defining_a_setter_on_new_objects_in_object_initializers" target="_blank" rel="noopener">setter</a> in conjunction to create a type of pseudo-property.

2. With Object.defineProperty Method

Defining Getter in an existing property with Object.defineProperty() method:

//create and initialize car object
let car = {
  make: "Toyota",
  model: "Camry",
};
//add property with Object.defineProprty
Object.defineProperty(car, 'myCar', {
  get() {
    return `${this.make} ${this.model}`;
  },
  set(value) {
    [this.make, this.model] = value.split(" ");
  }
});
// access myCar
console.log(car.myCar); //OUTPUT => Toyota Camry

//set new value to myCar
car.myCar = "Honda Civic";

//access property value on console
console.log(car.make); //OUTPUT => Honda
console.log(car.model); //OUTPUT => Civic
console.log(car.myCar); //OUTPUT => Honda Civic
3. Deleting Getters and Setters

The getters and setters can be deleted with delete keyword.

//delete myCar property
delete car.myCar; //logs true

//access myCar properties values
console.log(car.myCar); //OUTPUT => undefined
console.log(car.make); //OUTPUT => Toyota
console.log(car.model); //OUTPUT => Camry
4. Get vs Object.defineProperty Method

From the example above, it was shown that using get keyword and object.defineProperty() we get similar results.

However, if it is used to define object properties in classes, then there is difference between the two. When properties of an object in classes is defined using get keyword then are defined on the object prototype (and thus they are inherited) where as using Object.definePropery() the object properties are defined in on the instance (or static) and are not inherited.

//initialize myWorld class
class MyWorld {
  get hello() {
    return 'World';
  }
}

//create new object instance
const objName = new MyWorld();
//access 
console.log(objName.hello); //OUTPUT => World
// access
console.log(Object.getOwnPropertyDescriptor(objName, 'hello')); 
// OUTPUT => undefined
//access with
console.log(Object.getOwnPropertyDescriptor
  (Object.getPrototypeOf(objName), 'hello'));
//OUTPUT
{get: ƒ, set: undefined, enumerable: false, configurable: true}
5. Using Computed Property Name

Starting ECMAScript 6 (ES6), computed name can be used in object initializer syntax. Quoting from the MDN, “the computed property name allows you to put an expression in brackets [], that will be computed and used as the property name“.

Computed property can be used for Get function method.

//assign value to a var
let car = 'Toyota';
//modify var value using get function
let myCar = {
  get [car]() { return 'Ford'; }
};
//access computed property value
console.log(myCar.Toyota); //OUTPUT => Honda

In the example above, [car] in line 5 is computed property  name which is computed from car var (line 2).

Computed property value can be use used to assign a new value to Set function.

//initialize car 
let car = 'Toyota';
// initialize myCar object
let myCar = {
  model: 'Corolla',
  set [car](value) { this.model = value; }
};
//retrieve value
console.log(myCar.model); //OUTPUT =>Corolla
//assign value
myCar.Toyota = "Camry"; //logs "Camry"
console.log(myCar.model); //OUTPUT => Camry

//reassign value
myCar.Toyota = "Lexus"; // logs "Lexus"
console.log(myCar.model); //OUTPUT => Lexus

In the example above, [car] in line 14 is computed property name in Set function which is computed from car var (line 10).

Note: In a separate learning-post Computed Property Names is discussed briefly.

Object.defineProperty Method

Inheritance

Accessors properties are inherited. Quoting from the MDN, “if an accessor property is inherited, its get and set methods will be called when the property is accessed and modified on descendant objects. If these methods use a variable to store the value, this value will be shared by all objects.

//create an myCar empty function
function vehicle() {
}
let car; //create car variable 
Object.defineProperty(vehicle.prototype, "Toyota", {
  get() {
    return car;
  },
  set(x) {
    car = x;
  }
});
//initialize myCarA & myCarB object instances
let myCarA = new vehicle();
let myCarB = new vehicle();
//reassign property value 
myCarA.Toyota = "Honda";
console.log(myCarB.Toyota); //OUTPUT => Honda

In the example above (adopted from MDN), the value of myCarA object is assigned as “Honda” (line 17) and now the value is shared with myCarB object instance too (line 18).

Class Methods

With ES6, accessor descriptors (getter and setter) can also be used to define prototype methods of class function. Using Vehicle analogy from above:

//initialize Vehicle class
class Vehicle { 
  constructor (make, model) {
    this.make = make;
    this.model = model;  
  }     
  
  //initialise msg method
   message () {       
    return `Bring your ${this.make} ${this.model} to dealer.`    
  }
  //using get accessor method
  get reminder() {   
    return this.myReminder();
  }
  //initialize myRemindermethod
  myReminder () {   
   console.log( this.message() ); 
  }
};
//initialize myCar object instance
const myCar = new Vehicle('Toyota', 'Camry');
//invoke myCar.myReminder()
console.log(myCar.myReminder());
//OUTPUT
Bring your Toyota Camry to dealer.

In the example above, revisiting the Vehicle class function described above with get accessor descriptor method (line 13) we get the same exact output.

Wrapping Up

In this learning-note post, defining accessor properties, getters, setters, and creating object properties with object initilizers and <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty" target="_blank" rel="noopener">Object.DefineProperty</a> methods are discussed.

Useful Resources & Links

While preparing this post, I have referred the following references extensively. Visit original link for additional information.

  • Defining Getter and Setter | MDN JavaScript Guide
  • Getter and Setter | MDN JavaScript Reference
  • <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty" target="_blank" rel="noopener">Object .define Property()</a> | MDN JavaScript Reference
  • Property getters and setters | The Modern JavaScript Tutorial