en.javascript.info/1-js/9-object-inheritance/02-property-accessors/article.md
Ilya Kantor f99574f53b up
2016-11-14 16:31:21 +03:00

5.8 KiB

Property getters and setters [todo move to objects?]

There are two kinds of properties.

The first kind is data properties. We already know how to work with them, actually, all properties that we've been using yet are data properties.

The second type of properties is something new. It's accessor properties. They are essentially functions that work on getting and setting a value, but look like regular properties.

[cut]

Getters and setters

Accessor properties are represented by "getter" and "setter" methods. In an object literal they are preprended with words get and set:

let obj = {
  *!*get propName()*/!* {
    // getter, the code executed on getting obj.propName
  },

  *!*set propName(value)*/!* {
    // setter, the code executed on setting obj.propName = value
  }
};

The getter works when obj.propName is read, the setter -- when it is assigned.

For instance, we have a user object with name and surname:

let user = {
  name: "John",
  surname: "Smith"
};

Now we want to add a "fullName" property, that should be "John Smith". Of course, we don't want to copy-paste existing information, so we can implement it as an accessor:

let user = {
  name: "John",
  surname: "Smith",

*!*
  get fullName() {
    return `${this.name} ${this.surname}`;
  }
*/!* 
};

*!*
alert(user.fullName); // John Smith
*/!*

From outside, an accessor property looks like a regular one. That's the idea of accessor properties. We don't call user.fullName as a function, we read it normally, and it runs behind the scenes.

As of now, fullName has only a getter. If we attempt to assign user.fullName=, there will be an error.

Let's fix it by adding a setter for user.fullName:

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },

*!*
  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
*/!* 
};

// set fullName is executed with the given value.
user.fullName = "Alice Cooper";

alert(user.name); // Alice
alert(user.surname); // Cooper

Now we have a "virtual" property. It is readable and writable, but in fact does not exist.

We can either work with a property as a "data property" or as an "accessor property", these two never mix.

Once a property as defined with `get prop()`, it can't be assigned with `obj.prop=`, unless there's a setter too.

And if a property is defined with `set prop()`, then it can't be read unless there's also a getter.

Accessor descriptors

Differences between data properties and accessors are also reflected in their descriptors.

For accessor properties, there is no value and writable, but instead there are get and set functions.

So an accessor descriptor may have:

  • get -- a function without arguments, that works when a property is read,
  • set -- a function with one argument, that is called when the property is set,
  • enumerable -- same as for data properties,
  • configurable -- same as for data properties.

For instance, to create an accessor fullName with defineProperty, we can pass a descriptor with get and set:

let user = {
  name: "John",
  surname: "Smith"
};

*!*
Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.name} ${this.surname}`;
  },

  set(value) {
    [this.name, this.surname] = value.split(" ");
  }
*/!* 
});

alert(user.fullName); // John Smith

for(let key in user) alert(key); 

Smarter getters/setters

A combination of getter/setter can be used to validate property values at the moment of assignment.

For instance, if we want to forbid too short names for user, we can store name in a special property _name, at the same time providing smart getter/setter for it:

let user = {
  get name() {
    return this._name;
  },

  set name(value) {
    if (value.length < 4) {
      throw new Error("Name is too short, need at least 4 characters");
    }
    this._name = value;
  }
};

user.name = "Pete"; 
alert(user.name); // Pete

user.name = ""; // Error

Technically, the "real" name is stored in user._name, so the outer code may access it. In Javascript there's no way to prevent reading an object property. But there is a widely known agreement that properties starting with an underscore "_" are internal and should not be touched from outside.

For compatibility

One of great ideas behind getters and setters -- they allow to take control over a property at any moment.

For instance, we start implementing user objects using data properties name and age:

function User(name, age) {
  this.name = name;
  this.age = age;
}

let john = new User("John", 25);

alert( john.age ); // 25

...But sooner or later, things may change. Instead of age we may decide to store birthday, because it's more precise and convenient:

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;
}

let john = new User("John", new Date(1992, 6, 1));

Now what to do with the old code that still uses age?

We can try to find all such places and fix them, but that takes time and not always possible with 3rd party libraries. And besides, age is a nice thing to have in user, right? In some places it's just what we want.

Adding a getter for age mitigates the problem:

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;

*!*
  // age is calculated from the current date and birthday
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    }
  });
*/!*
}

let john = new User("John", new Date(1992, 6, 1));

alert( john.birthday ); // birthday is available
alert( john.age );      // ...as well as the age