- JavaScript Object Accessors
- JavaScript Getter (The get Keyword)
- Example
- JavaScript Setter (The set Keyword)
- Example
- JavaScript Function or Getter?
- Example 1
- Example 2
- Data Quality
- Example
- Example
- Why Using Getters and Setters?
- Object.defineProperty()
- A Counter Example
- Property getters and setters
- Getters and setters
- Accessor descriptors
- Smarter getters/setters
- Using for compatibility
- Геттеры и сеттеры в Javascript
- Стандартные геттеры
- Нативные геттеры/сеттеры
- Через объект:
- Через методы __defineGetter__ и __defineSetter__:
- Определяем поддержку браузером
- Как быть с наследованием?
- Что следует помнить
- MooTools
- Интересные ссылки:
JavaScript Object Accessors
ECMAScript 5 (ES5 2009) introduced Getter and Setters.
Getters and setters allow you to define Object Accessors (Computed Properties).
JavaScript Getter (The get Keyword)
This example uses a lang property to get the value of the language property.
Example
// Create an object:
const person = firstName: «John»,
lastName: «Doe»,
language: «en»,
get lang() return this.language;
>
>;
// Display data from the object using a getter:
document.getElementById(«demo»).innerHTML = person.lang;
JavaScript Setter (The set Keyword)
This example uses a lang property to set the value of the language property.
Example
const person = <
firstName: «John»,
lastName: «Doe»,
language: «»,
set lang(lang) <
this.language = lang;
>
>;
// Set an object property using a setter:
person.lang = «en»;
// Display data from the object:
document.getElementById(«demo»).innerHTML = person.language;
JavaScript Function or Getter?
What is the differences between these two examples?
Example 1
const person = <
firstName: «John»,
lastName: «Doe»,
fullName: function() <
return this.firstName + » » + this.lastName;
>
>;
// Display data from the object using a method:
document.getElementById(«demo»).innerHTML = person.fullName();
Example 2
const person = <
firstName: «John»,
lastName: «Doe»,
get fullName() <
return this.firstName + » » + this.lastName;
>
>;
// Display data from the object using a getter:
document.getElementById(«demo»).innerHTML = person.fullName;
Example 1 access fullName as a function: person.fullName().
Example 2 access fullName as a property: person.fullName.
The second example provides a simpler syntax.
Data Quality
JavaScript can secure better data quality when using getters and setters.
Using the lang property, in this example, returns the value of the language property in upper case:
Example
// Create an object:
const person = firstName: «John»,
lastName: «Doe»,
language: «en»,
get lang() return this.language.toUpperCase();
>
>;
// Display data from the object using a getter:
document.getElementById(«demo»).innerHTML = person.lang;
Using the lang property, in this example, stores an upper case value in the language property:
Example
const person = <
firstName: «John»,
lastName: «Doe»,
language: «»,
set lang(lang) <
this.language = lang.toUpperCase();
>
>;
// Set an object property using a setter:
person.lang = «en»;
// Display data from the object:
document.getElementById(«demo»).innerHTML = person.language;
Why Using Getters and Setters?
- It gives simpler syntax
- It allows equal syntax for properties and methods
- It can secure better data quality
- It is useful for doing things behind-the-scenes
Object.defineProperty()
The Object.defineProperty() method can also be used to add Getters and Setters:
A Counter Example
// Define setters and getters
Object.defineProperty(obj, «reset», get : function ()
>);
Object.defineProperty(obj, «increment», get : function ()
Object.defineProperty(obj, «decrement», get : function ()
>);
Object.defineProperty(obj, «add», set : function (value)
>);
Object.defineProperty(obj, «subtract», set : function (value)
>);
// Play with the counter:
obj.reset;
obj.add = 5;
obj.subtract = 1;
obj.increment;
obj.decrement;
Property getters and setters
The first kind is data properties. We already know how to work with them. All properties that we’ve been using until now were data properties.
The second type of property is something new. It’s an accessor property. They are essentially functions that execute on getting and setting a value, but look like regular properties to an external code.
Getters and setters
Accessor properties are represented by “getter” and “setter” methods. In an object literal they are denoted by 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 :
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 `$$`; > >; alert(user.fullName); // John Smith
From the 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: the getter 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 user = < get fullName() < return `. `; >>; user.fullName = "Test"; // Error (property has only a getter)
Let’s fix it by adding a setter for user.fullName :
let user = < name: "John", surname: "Smith", get fullName() < return `$$`; >, 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
As the result, we have a “virtual” property fullName . It is readable and writable.
Accessor descriptors
Descriptors for accessor properties are different from those for data properties.
For accessor properties, there is no value or writable , but instead there are get and set functions.
That is, 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 `$$`; >, set(value) < [this.name, this.surname] = value.split(" "); >>); alert(user.fullName); // John Smith for(let key in user) alert(key); // name, surname
Please note that a property can be either an accessor (has get/set methods) or a data property (has a value ), not both.
If we try to supply both get and value in the same descriptor, there will be an error:
// Error: Invalid property descriptor. Object.defineProperty(<>, 'prop', < get() < return 1 >, value: 2 >);
Smarter getters/setters
Getters/setters can be used as wrappers over “real” property values to gain more control over operations with them.
For instance, if we want to forbid too short names for user , we can have a setter name and keep the value in a separate property _name :
let user = < get name() < return this._name; >, set name(value) < if (value.length < 4) < alert("Name is too short, need at least 4 characters"); return; >this._name = value; > >; user.name = "Pete"; alert(user.name); // Pete user.name = ""; // Name is too short.
So, the name is stored in _name property, and the access is done via getter and setter.
Technically, external code is able to access the name directly by using user._name . But there is a widely known convention that properties starting with an underscore «_» are internal and should not be touched from outside the object.
Using for compatibility
One of the great uses of accessors is that they allow to take control over a “regular” data property at any moment by replacing it with a getter and a setter and tweak its behavior.
Imagine we started 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 property?
We can try to find all such places and fix them, but that takes time and can be hard to do if that code is used by many other people. And besides, age is a nice thing to have in user , right?
Adding a getter for age solves 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
Now the old code works too and we’ve got a nice additional property.
Геттеры и сеттеры в Javascript
Javascript — очень изящный язык с кучей интересных возможностей. Большинство из этих возможностей скрыты одним неприятным фактором — Internet Explorer’ом и другим дерьмом, с которым нам приходится работать. Тем не менее, с приходом мобильных телефонов с актуальными браузерами и серверного JavaScript с нормальными движками эти возможности уже можно и нужно использовать прям сейчас. Но по привычке, даже при программировании для node.js мы стараемся писать так, чтобы оно работало в IE6+.
В этой статье я расскажу про интересный и не секретный способ указывать изящные геттеры и сеттеры и немножко покопаемся в исходниках Mootools. Частично это информация взята из статьи John Resig, частично лично мой опыт и эксперименты.
function Foo(bar) < this._bar = bar; >Foo.prototype = < get bar () < return this._bar; >, set bar (bar) < this._bar = bar; >>;
Стандартные геттеры
function Foo(bar) < this._bar = bar; >; Foo.prototype = < setBar : function (bar) < this._bar = bar; >, getBar : function () < return this._bar; >>; var foo = new Foo; foo.setBar(123); alert(foo.getBar());
Можно пойти дальше и написать более изящный вариант:
function Foo(bar) < var _bar = bar; this.bar = function (bar) < if (arguments.length) _bar = bar; else return _bar; >>; var foo = new Foo; foo.bar(123); alert(foo.bar());
Нативные геттеры/сеттеры
Но есть более удобный способ, который работает во всех серверных движках и современных браузерах, а именно Firefox, Chrome, Safari3+, Opera9.5+ — задание сеттера и геттера для свойства так, чтобы продолжать обращатся к свойству, как свойству. У такого подхода есть несколько преимуществ:
1. Более изящная запись. Представим ORM:
for (var i in topics.comments); // vs for (var i in topics.loadComments());
2. Если апи, которое базируется на свойствах уже есть и его нельзя менять (а очень нужно).
Есть два способа задать такой геттер/сеттер:
Через объект:
function Foo(bar) < this._bar = bar; >; Foo.prototype = < set bar (bar) < this._bar = bar; >, get bar () < return this._bar; >>; var foo = new Foo; foo.bar = 123; alert(foo.bar);
Через методы __defineGetter__ и __defineSetter__:
function Foo(bar) < var _bar = bar; this.__defineGetter__("bar", function()< return _bar; >); this.__defineSetter__("bar", function(val)< _bar = bar; >); >; var foo = new Foo; foo.bar = 123; alert(foo.bar);
Определяем поддержку браузером
Из этого можно получить лёгкий способ определения, поддерживает ли браузер геттеры или не поддерживает:
return (typeof <>.__defineGetter__ == 'function');
Как быть с наследованием?
function extend(target, source) < for ( var prop in source ) < var getter = source.__lookupGetter__(prop), setter = source.__lookupSetter__(prop); if ( getter || setter ) < if ( getter ) target.__defineGetter__(prop, getter); if ( setter ) target.__defineSetter__(prop, setter); >else a[i] = b[i]; > return target; >
Таким образом нашему target передадутся не значения родительского source, а функции-геттеры/сеттеры.
Что следует помнить
* Для каждого свойства вы можете установить только один геттер и/или сеттер. Не может быть два геттера или сеттера
* Единственный способ удалить геттер или сеттер — это вызвать delete object[name]; . Эта команда удаляет и геттер и сеттер, потому если вы хотите удалить что-то одно, а другое — оставить, надо сначала сохранить его, а после удаления — снова присвоить
* Когда вы используете __defineGetter__ или __defineSetter__ он просто тихонько перезаписывает предыдущий геттер или сеттер и даже удаляет само свойство с таким именем.
* Проверить, поддерживает ли ваш браузер геттеры и сеттеры можно с помощью простого сниппета:
javascript:foo=>;alert(foo.test);
MooTools
Мутулз не поддерживает по-умолчанию такую возможность. И, хотя я уже предложил патч, мы можем с лёгкостью (слегка изменив исходники) заставить его понимать геттеры и сеттеры.
Итак, какая наша цель?
var Foo = new Class(< set test : function () < console.log('test is set'); >, get test : function () < console.log('test is got'); return 'test'; >, >); foo.test = 1234; // test is set alert(foo.test); // test is get
Более того, в классах унаследованных через Implements и Extends тоже должны работать геттеры и сеттеры родительского класса. Все наши действия будут происходить в файле [name: Class] внутри анонимной функции.
Во-первых, внутри функции, в самом верху, определим функцию, которая перезаписывает только геттеры и сеттеры. И хотя мы отказалась от устаревших браузеров — стоит застраховаться.
var implementGettersSetters = (typeof <>.__lookupGetter__ == 'function') ? function (target, source) < for (var key in source) < var g = source.__lookupGetter__(key), s = source.__lookupSetter__(key); if ( g || s ) < if (g) target.__defineGetter__(key, g); if (s) target.__defineSetter__(key, s); >> return target; > : function (target, source) < return target; >;
Конечно, если наш скрипт с такими геттерами попадёт в устаревший браузер, то он просто упадёт, но это страховка от того, чтобы кто-то случайно не взял этот файл и не прицепил его к себе на сайт, а потом недоумевал, что такое с ишаком.
Мы видим, что если __lookupGetter__ не поддерживается, то функция просто ничего не сделает.
Теперь заставляем работать getterы и setterы во время создания класса и наследования (Extends). Для этого:
// после var = new Type('Class', function(params)< // сразу перед newClass.$constructor = Class; newClass.prototype.$constructor = newClass; newClass.prototype.parent = parent; // Необходимо вставить функцию, которая расширит прототип (внимание, прототип, а не объект!) нашего класса: implementGettersSetters(newClass.prototype, params);
Отдельным движением надо реализовать наследование геттеров и сеттеров от примесей (Implements). Для этого надо найти встроенные Мутаторы и добавить всего одну строку:
Все, теперь сеттеры и геттеры реализуются и мы с лёгкостью можем их наследовать и использовать. Наслаждайтесь)
var Foo = new Class(< initialize : function (name) < this.name = name; >, set test : function () < console.log(this.name + ' test is set'); >, get test : function () < console.log(this.name + ' test is got'); return 'test'; >, >); var Bar = new Class(< Extends : Foo >); var Qux = new Class(< Implements : [ Foo ] >); var foo = new Foo('foo'); foo.test = 1234; // foo test is set alert(foo.test); // foo test is got var bar = new Bar('bar'); bar.test = 1234; // bar test is set alert(bar.test); // bar test is got var qux = new Qux('qux'); qux.test = 1234; // qux test is set alert(qux.test); // qux test is got
Интересные ссылки:
Object.defineProperty — средство для создания свойств с очень широкими настройкам, такие как writable, get, set, configurable, enumerable
Object.create — удобно быстро создавать нужные объекты.