JavaScript Property Getters and Setters

In this chapter, we cover the types of object properties.

As a rule, there exist two object property types: data properties and accessor properties. All the properties we represented before are data properties. Accessor properties are relatively new and execute on getting and setting values. However, they seem like a regular property to the external code.

The Usage of Getters and Setters

The so-called getter and setter methods represent accessor properties. They are specified by get and set inside an object literal as shown below:

let obj = {
  get propName() {
    // getter, the code executed when obj.propName id getting
  },
  set propName(value) {
    // setter, the code executed when obj.propName = value is setting
  }
};

The getter will operate once obj.propName is read. The setter will work once it is already assigned.

An example of a user object with a name and a surname is demonstrated below:

let site = {

  siteName: "W3Docs",

  bookName: "Javascript"
};
console.log(site);

The next step is adding a fullName property (it should be "W3Docs Javscsript"). To avoid copy-pasting the existing information, it can be implemented as an accessor like this:

let site = {
  siteName: "W3Docs",
  bookName: "Javascript",
  get fullName() {
    return `Welcome to ${this.siteName}, ${this.bookName} book`;
  }
};
console.log(site.fullName);

As it was told, an accessor property can look like a regular one. That’s its main idea.

The site.fullName is not called as a function. It is read normally, and the getter is performed behind the scenes.

The attempt to assign user.fullName= will lead to an error like here:

let site = {
  get fullName() {
    return `...`;
  }
};
// Error, property has only a getter
site.fullName = "Test";

Adding a setter for site.fullName will help to fix it:

let site = {
  siteName: "W3Docs",
  bookName: "Javascript",
  get fullName() {
    return `${this.name}, ${this.surname}`;
  },
  set fullName(value) {
    [this.siteName, this.bookName] = value.split(" ");
  }
};
site.fullName = "W3 CSS"; // set fullName is executed with the given value
console.log(site.siteName); // W3
console.log(site.bookName); // CSS

The result will be having a virtual fullName property, which is both writable and readable.

About Accessor Descriptors

Now, let’s check out the descriptors for accessor properties. They are different from the descriptors for the data properties. There isn’t any value or writable for accessor properties. Instead, they include get and set functions.

An accessor descriptor may include:

  • get – a no-argument function, operating once a property is read,
  • set – a function that has a single argument, which is called once the property is set,
  • enumerable – the same as for data properties,
  • configurable– the same as for data properties.

For generating a fullName accessor with defineProperty, a descriptor with get and set should be passed like this:

let user = {
  firstname: "Helen",
  lastname: "Down"
};
Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.firstname} ${this.lastname}`;
  },
  set(value) {
    [this.firstname, this.lastname] = value.split(" ");
  }
});
console.log(user.fullName); // Helen Down
for (let key in user) {
  console.log(key); // name, surname
}

An important note: a property must either be an accessor property or a data property.

Trying to put both in the same descriptor will lead to an error like this:

// Error: Invalid property descriptor
Object.defineProperty({}, 'prop', {
  get() {
    return 1
  },
  value: 10
});

Wider Usage of Getters and Setters

Getters and setters can be usefully performed as wrappers over property values for gaining more control over operations.

For prohibiting very short names for the user, you may have a setter name, storing the value within a separate property _name like this:

let book = {
  get name() {
    return this._name;
  },
  set name(value) {
    if (value.length < 3) {
      console.log("Name too short, at least 4 characters needed");
      return;
    }
    this._name = value;
  }
};
book.name = "Javascript";
console.log(book.name); // Javascript
book.name = ""; // Name too short

So, the name is kept in the _name property. The access is implemented through getter and setter.

The Usage for Compatibility

Another significant use of accessors is that they allow controlling a regular data property at any moment by interchanging it with a getter and setter. Here is an example of user objects, applying name and age data properties:

function User(name, age) {
  this.name = name;
  this.age = age;
}
let david = new User("David", 30);
console.log(david.age); // 30

Now, the main issue is how to deal with the old code, which still applies the age property.

The most efficient way of solving the issue is to add a getter for the age property:

function User(name, bday) {
  this.name = name;
  this.bday = bday;
  // age is calculated based on the current date and birthday
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.bday.getFullYear();
    }
  });
}
let david = new User("David", new Date(1990, 10, 5));
console.log(david.bday); // birthday 
console.log(david.age); // the age

So, finally, both the code works, and there is an additional useful property.




Do you find this helpful?

Related articles