en.javascript.info/1-js/6-objects-more/4-descriptors-getters-setters/article.md
2015-01-14 10:23:45 +03:00

16 KiB
Raw Blame History

Дескрипторы, геттеры и сеттеры свойств

В этой главе мы рассмотрим возможности, которые позволяют очень гибко и мощно управлять всеми свойствами объекта, включая их аспекты -- изменяемость, видимость в цикле for..in и даже "невидимые" геттеры-сеттеры.

Они поддерживаются всеми современными браузерами, но не IE8-. Точнее говоря, они поддерживаются даже в IE8, но не для всех объектов, а только для DOM-объектов (используются при работе со страницей, это сейчас вне нашего рассмотрения).

[cut]

Дескрипторы в примерах

Основной метод для управления свойствами -- Object.defineProperty.

Он позволяет определить свойство путём задания "дескриптора" -- описания, включающего в себя ряд важных внутренних параметров.

Синтаксис:

Object.defineProperty(obj, prop, descriptor)

Аргументы:

`obj`
Объект, в котором объявляется свойство.
`prop`
Имя свойства, которое нужно объявить или модифицировать.
`descriptor`
Дескриптор -- объект, который описывает поведение свойства. В нём могут быть следующие поля:
  • `value` -- значение свойства, по умолчанию `undefined`
  • `writable` -- значение свойства можно менять, если `true`. По умолчанию `false`.
  • `configurable` -- если `true`, то свойство можно удалять, а также менять его в дальнейшем при помощи новых вызовов `defineProperty`. По умолчанию `false`.
  • `enumerable` -- если `true`, то свойство будет участвовать в переборе `for..in`. По умолчанию `false`.
  • `get` -- функция, которая возвращает значение свойства. По умолчанию `undefined`.
  • `set` -- функция, которая записывает значение свойства. По умолчанию `undefined`.

Чтобы избежать конфликта, запрещено одновременно указывать значение value и функции get/set. Либо значение, либо функции для его чтения-записи, одно из двух. Также запрещено и не имеет смысла указывать writable при наличии get/set-функций.

Далее мы подробно разберём эти свойства на примерах.

Обычное свойство

Обычное свойство добавить очень просто.

Два таких вызова работают одинаково:

var user = {};

// 1. простое присваивание
user.name = "Вася";

// 2. указание значения через дескриптор
Object.defineProperty(user, "name", { value: "Вася" });

Свойство-константа

Для того, чтобы сделать свойство неизменяемым, добавим ему флаги writable и configurable:

//+ run
*!*
"use strict";
*/!*

var user = {};

Object.defineProperty(user, "name", {
  value: "Вася",
  writable: false,    // запретить присвоение "user.name=" 
  configurable: false // запретить удаление "delete user.name"
});

// Теперь попытаемся изменить это свойство.

// в strict mode присвоение "user.name=" вызовет ошибку
*!*
user.name = "Петя";
*/!*

Заметим, что без use strict операция записи "молча" не сработает, а при use strict дополнительно генерируется ошибка.

Свойство, скрытое для for..in

Встроенный метод toString, как и большинство встроенных методов, не участвует в цикле for..in. Это удобно, так как обычно такое свойство является "служебным".

К сожалению, свойство toString, объявленное обычным способом, будет видно в цикле for..in, например:

//+ run
var user = {
  name: "Вася",
  toString: function() { return this.name; }
};

*!*
for(var key in user) alert(key);  // name, toString
*/!*

Мы бы хотели, чтобы поведение нашего метода toString было таким же, как и стандартного.

Object.defineProperty может исключить toString из списка итерации, поставив ему флаг enumerable: false. По стандарту, у встроенного toString этот флаг уже стоит.

//+ run
var user = {
  name: "Вася",
  toString: function() { return this.name; }
};

*!*
// помечаем toString как не подлежащий перебору в for..in
Object.defineProperty(user, "toString", {enumerable: false});

for(var key in user) alert(key);  // name
*/!*

Обратим внимание, вызов defineProperty не перезаписал свойство, а просто модифицировал настройки у существующего toString.

Свойство-функция

Дескриптор позволяет задать свойство, которое на самом деле работает как функция. Для этого в нём нужно указать эту функцию в get.

Например, у объекта user есть обычные свойства: имя firstName и фамилия surname.

Создадим свойство fullName, которое на самом деле является функцией:

//+ run
var user = {
  firstName: "Вася",
  surname: "Петров"
}

Object.defineProperty(user, "fullName", {
  *!*get*/!*: function() {
    return this.firstName + ' ' + this.surname;
  }
});

*!*
alert(user.fullName); // Вася Петров
*/!*

Обратим внимание, снаружи fullName -- это обычное свойство user.fullName. Но дескриптор указывает, что на самом деле его значение возвращается функцией.

Также можно указать функцию, которая используется для записи значения, при помощи дескриптора set.

Например, добавим возможность присвоения user.fullName к примеру выше:

//+ run
var user = {
  firstName: "Вася",
  surname: "Петров"
}

Object.defineProperty(user, "fullName", {

  get: function() {
    return this.firstName + ' ' + this.surname;
  },

*!*
  set: function(value) {
    var split = value.split(' ');
    this.firstName = split[0];
    this.surname = split[1];
  }
*/!*
});

*!*
user.fullName = "Петя Иванов";
*/!*
alert(user.firstName); // Петя 
alert(user.surname); // Иванов

Указание get/set в литералах

Если мы создаём объект при помощи синтаксиса { ... }, то задать свойства-функции можно прямо в его определении.

Для этого используется особый синтаксис: get свойство или set свойство.

Например, ниже объявлен геттер-сеттер fullName:

//+ run
var user = {
  firstName: "Вася",
  surname: "Петров",

*!*
  get fullName() {
*/!*
    return this.firstName + ' ' + this.surname;
  },

*!*
  set fullName(value) {
*/!*
    var split = value.split(' ');
    this.firstName = split[0];
    this.surname = split[1];
  }
};

*!*
alert(user.fullName); // Вася Петров (из геттера)

user.fullName = "Петя Иванов";
alert(user.firstName); // Петя  (поставил сеттер)
alert(user.surname); // Иванов (поставил сеттер)
*/!*

Да здравствуют get/set!

Казалось бы, зачем нам назначать get/set для свойства через всякие хитрые вызовы, когда можно сделать просто функции с самого начала? Например, getFullName, setFullName...

Конечно, в ряде случаев свойства выглядят короче, такое решение просто может быть красивым. Но основной бонус -- это гибкость, возможность получить контроль над свойством в любой момент!

Например, в начале разработки мы используем обычные свойства, например у User будет имя name и возраст age:

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

var pete = new User("Петя", 25);

alert(pete.age); // 25

С обычными свойствами в коде меньше букв, они удобны, причины использовать функции пока нет.

...Но рано или поздно может произойти что-то, что потребует более сложной логики.

Например, формат данных изменился и теперь вместо возраста age хранится дата рождения birthday:

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

var pete = new User("Петя", new Date(1987, 6, 1));

Что теперь делать со старым кодом, который выводит свойство age?

Можно, конечно, найти все места и поправить их, но это долго, а иногда и невозможно, скажем, если вы взаимодействуете со сторонней библиотекой, код в которой -- чужой и влезать в него нежелательно.

Добавление get-функции age позволяет обойти проблему легко и непринуждённо:

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

*!*
  Object.defineProperty(this, "age", { 
    get: function() { 
      var todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    }
  });
*/!*
}

var pete = new User("Петя", new Date(1987, 6, 1));

alert(pete.age); // получает возраст из даты рождения

Таким образом, defineProperty позволяет нам использовать обычные свойства и, при необходимости, в любой момент заменить их на функции, сохраняя полную совместимость.

Другие методы работы со свойствами

[Object.defineProperties(obj, descriptors)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperties)
Позволяет объявить несколько свойств сразу:
//+ run
var user = {}

Object.defineProperties(user, {
*!*
  firstName: {
*/!*
    value: "Петя"
  },

*!*
  surname: {
*/!*
    value: "Иванов"
  },

*!*
  fullName: {
*/!*
    get: function() {
      return this.firstName + ' ' + this.surname;
    }
  }
});

alert( user.fullName ); // Петя Иванов
[Object.keys(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys), [Object.getOwnPropertyNames(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames)
Возвращают массив -- список свойств объекта.

Object.keys возвращает только enumerable-свойства.

Object.getOwnPropertyNames -- возвращает все:

//+ run
var obj = {
  a: 1,
  b: 2,
  internal: 3
};

Object.defineProperty(obj, "internal", {enumerable: false});

*!*
alert( Object.keys(obj) ); // a,b 
alert( Object.getOwnPropertyNames(obj) ); // a, internal, b
*/!*
[Object.getOwnPropertyDescriptor(prop)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor)
Возвращает дескриптор для свойства с `prop`.

Полученный дескриптор можно изменить и использовать defineProperty для сохранения изменений, например:

//+ run
var obj = { test: 5 };
*!*
var descriptor = Object.getOwnPropertyDescriptor(obj, 'test');
*/!*

*!*
// заменим value на геттер, для этого...
*/!*
delete descriptor.value; // ..нужно убрать value/writable
delete descriptor.writable;
descriptor.get = function() { // и поставить get
  alert("Preved :)"); 
};

*!*
// поставим новое свойство вместо старого
*/!*

// если не удалить - defineProperty объединит старый дескриптор с новым
delete obj.test; 

Object.defineProperty(obj, 'test', descriptor);

obj.test; // Preved :)

...И несколько методов, которые используются очень редко:

[Object.preventExtensions(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/seal)
Запрещает добавление свойств в объект.
[Object.seal(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/seal)
Запрещает добавление и удаление свойств, все текущие свойства делает `configurable: false`.
[Object.freeze(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/freeze)
Запрещает добавление, удаление и изменение свойств, все текущие свойства делает `configurable: false, writable: false`.
[Object.isExtensible(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isExtensible), [Object.isSealed(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isSealed), [Object.isFrozen(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isFrozen)
Возвращают `true`, если на объекте были вызваны методы `Object.preventExtensions/seal/freeze`.