Много людей знают, что такое getter'ы и setter'ы во многих языках программирования. Есть эти чудесные штуки и в Javascript, хоть узнал я об этом совсем недавно (темень я необразованная). Речь пойдёт не только и не столько про методы getSomeProperty()/setSomeProperty(...)
, а про более интересную реализацию — псевдо-аттрибуты, при изменении которых будут вызываться функции-обработчики.
Очевидное
Самый простой способ, те самые getSomeProperty()/setSomeProperty(...):
function MyObject(newVal) {
this._secretProperty = newVal;
}
MyObject.prototype = {
getSecretProperty: function() {
return this._secretProperty;
},
setSecretProperty: function(newVal) {
return this._secretProperty = newVal;
}
}
* This source code was highlighted with Source Code Highlighter.
Это — самый простой и очевидный случай. Но и самый плохой. Храня данные в
this
, мы никак не скроем их от «злого хакера». И вполне можно делать так:
var obj = new MyObject();
obj._secretProperty = 'Ха-ха! Я - злостный хакер, ваша защита мне нипочём!';
// Вот так обходится setSecretProperty. -- Капитан Очевидность :)
* This source code was highlighted with Source Code Highlighter.
Шаг 2: прячем данные в замыкании конструктора
function MyObject(newVal) {
var _secretProperty = newVal;
this.getSecretProperty = function() {
return _secretProperty;
}
this.setSecretProperty = function(newVal) {
return _secretProperty = newVal;
}
}
* This source code was highlighted with Source Code Highlighter.
Всё, данные спрятаны, и
this._secretProperty
уже
undefined
. Получай, злостный хакер!
Однако мы принесли в жертву немножко памяти нашего компьютера: создавая методы в конструкторе, а не в прототипе, мы будем выделять память под них для каждого нового экземпляра данного типа. Но об этом можно не беспокоиться. У нас 100% не будет 1000000 таких объектов. Да и объёмы памяти современного ПК вполне позволяют нам такую роскошь. Примеры:
var obj = new MyObject(42);
alert(obj._secretProperty); // undefined
alert(obj.getSecretProperty()); // 42
obj._secretProperty = 9;
alert(obj._secretProperty); // 9 - НО...
alert(obj.getSecretProperty()); // 42 - фуух, а я уже испугался! :-)
obj.setSecretProperty(78);
alert(obj.getSecretProperty()); // 78
* This source code was highlighted with Source Code Highlighter.
Конечно, пока всё просто.
Шаг 3: in more Javascript way
Улучшаем, улучшаем. Я считаю, что подход
getSomeProperty()/setSomeProperty(...)
— слишком громоздок. Можно сделать гораздо более лаконично, а заодно сэкономить память ПК и время разработчиков, которые впоследствии будут этим пользоваться:
function MyObject(newVal) {
var _secretProperty = newVal;
/**
* @param {Object} newVal - новое значение для _secretProperty. Не обязателен.
* Если указан, то secretProperty(...) действует, как setter.
* Если не указан, то secretProperty() действует, как getter.
*/
this.secretProperty = function(newVal) {
if (typeof newVal != "undefined")
_secretProperty = newVal;
return _secretProperty;
}
}
// Примеры:
var obj = new MyObject(42);
alert(obj._secretProperty); // undefined
alert(obj.secretProperty()); // 42
obj.secretProperty(78);
alert(obj.secretProperty()); // 78
* This source code was highlighted with Source Code Highlighter.
Есть параметр, значит setter. Нет параметра — getter. Главное — описать это в аннотации JSDoc :)
И вот на таком способе можно уже и остановиться. Да, он хорош, но мы пойдём дальше!
Legacy syntax
Плавно переходим к более интересным вариантам. Первый из них некоторые называют legacy syntax. Потому, что так «повелось» с тех времён, когда ещё не внедрили синтаксические конструкции Javascript типа get/set (о котрых будет дальше).
var obj = {
real_a: 1
};
// Во всех объектах, включая DOM, могут присутствовать (жаль, но в IE их нет) методы:
// __defineGetter__, __defineSetter__, __lookupGetter__, __lookupSetter__
// Первые два задают getter'ы и setter'ы:
obj.__defineGetter__("a", function() { return this.real_a * 2; });
obj.__defineSetter__("a", function(v) { return this.real_a = v / 2; });
// Вторые два проверяют наличие getter'ов и setter'ов:
alert(obj.__lookupGetter__('a')) // some function-getter for 'a'
alert(obj.__lookupGetter__('b')) // undefined
alert(obj.__lookupSetter__('a')) // some function-setter for 'a'
// Примеры
alert(obj.real_a); // 1
obj.a = 9; // setter в действии
alert(obj.real_a); // 4.5
alert(obj.a); // 9; getter в действии
* This source code was highlighted with Source Code Highlighter.
Данный код прекрасно работает в:
FF 1.0+,
Chrome 0.2+,
Safari 3.0+,
Opera 9.5+
И не работает в:
IE — все версии, включая 8.0
Да, и тут есть несколько интересных особенностей (
важно!):
- если задать только что-то одно (getter/setter), второе не создастся автоматически.
- если до создания getter'а/setter'а в объекте уже было одноимённое свойство — оно затрётся (что логично)
Пример (см. комменты в коде):
var obj = {attr:5};
obj.__defineSetter__('attr', function(val){
// Здесь ни в коем случае нельзя писать:
// this.attr = val * 5
// т.к. это бесконечно зациклит setter.
// Название должно отличаться:
this.v = val * 5;
})
alert(obj.attr); // undefined. Как видим, задание getter/setter "затирает" одноимённый аттрибут, что был до этого.
// Теперь у нас - псевдо-аттрибут с функциями-обработчиками.
// И то, что мы установили setter для 'attr', не создало автоматически одноимённый getter.
obj.attr = 3; // Однако setter вполне исправный :)
alert(obj.v); // 15. Вот сюда setter записал данные.
* This source code was highlighted with Source Code Highlighter.
get/set
А вот этот подход — уже «по Фен-Шую». Смотрим пример:
var obj = {
real_a: 1,
get a() { return this.real_a * 2; },
set a(v) { return this.real_a = v / 2; }
};
// Примеры
alert(obj.real_a); // 1
obj.a = 9; // setter в действии
alert(obj.real_a); // 4.5
alert(obj.a); // 9; getter в действии
// __lookupGetter__/__lookupSetter__ - продолжают работать:
alert(obj.__lookupGetter__('a')) // some function-getter for 'a'
// и т.д.
* This source code was highlighted with Source Code Highlighter.
Данный код прекрасно работает в:
FF 1.0+,
Chrome 0.2+,
Safari 3.0+,
Opera 9.5+
И не работает в:
IE — все версии, включая 8.0
Я повторил предыдущий пример (из legacy syntax, см. выше), но во сколько раз уменьшился объём кода! Это замечательно (казалось бы)!
Но нет. В тех браузерах, где конструкции get/set не поддерживаются, такой код вызовет синтаксическую ошибку. Поэтому я бы пока воздержался от использования (ну или запихивать в try-catch, что не очень).
В целом же — запись через get/set полностью аналогична записи через legacy syntax.
Firefox way
FF пошёл в getter'ах и setter'ах дальше (не знаю, кстати, зачем) и создал +2 велосипеда :)
№1:
var obj = {
get a b() { /**/ },
set a c(v) { /**/ }
};
alert(obj.__lookupGetter__('a')) // function b()
alert(obj.__lookupSetter__('a')) // function c(v)
// и т.д.
* This source code was highlighted with Source Code Highlighter.
По сути, мы можем задавать
именованные функции-обработчики. Вот и всё. Не знаю, зачем это кому-то понадобится.
№2:
function magicGetter() { return 42; };
var obj = {
a getter:function b() { /**/ },
a setter: function(v) { /**/ },
'^_^' getter: magicGetter
};
alert(obj.__lookupGetter__('a')) // function b()
alert(obj.__lookupSetter__('a')) // function(v)
alert(obj.__lookupGetter__('^_^')) // function magicGetter()
alert(obj["^_^"]); // 42
* This source code was highlighted with Source Code Highlighter.
Данный код прекрасно работает
исключительно в:
FF 1.0+
Тут тоже можно задавать
именованные функции-обработчики, но есть 2 преимущества перед предидущим: задание
внешней функции-обработчика и задание getter'ов/setter'ов для аттрибутов
с недопустимыми (для записи через ".") символами.
Хотя всё это можно сделать и через __defineGetter__/__defineSetter__. Потому и велосипеды.
Да, использование любого из этих двух способов приведёт к SyntaxError везде, кроме FF. Помним! :)
IE way
Internet Explorer, как всегда, сказал «мне с вами не по пути» и сделал getter'ы и setter'ы своим собственным способом.
IE 8.0+
Начнём с 8-й версии. Тут реализован метод
Object.defineProperty(...)
Но, как ни жаль, применим он только лишь к DOM-элементам.
// Работает только в IE 8.0 и выше
Object.defineProperty(document.body, "description", {
get : function () {
alert('Getting description...');
return this.desc;
},
set : function (val) {
alert('Setting description...');
this.desc = val;
}
});
document.body.description = "Content container"; // "Setting description..."
alert(document.body.description); // "Getting description..." -> "Content container"
alert(document.body.desc); // "Content container"
// Попробуем повторить не для DOM-элемента:
var obj = {};
Object.defineProperty(obj, "prop", {
get : function () { /**/ }
}); // JS ERROR: Object doesn't support this action. Вот так-то. Пока не могём.
* This source code was highlighted with Source Code Highlighter.
Остаются справедливыми правила:
а) одноимённый аттрибут затирается;
б) один getter не добавляет setter автоматически (и vice versa).
Если всё же захочется применить этот механизм к не-DOM-элементам, то придётся изворачиваться и делать подмену вашего объекта на DOM-элемент.
IE 5.0+
Для версий до IE 8.0 по сути совсем нет механизма getter'ов и setter'ов. Однако есть чудесное событие
onpropertychange
. Присутствует оно только у DOM-элементов. С помощью него можно создать setter. Однако для getter'ов я так ничего и не нашёл. Пример:
document.body.onpropertychange = function() {
var pn = window.event.propertyName;
var pv = window.event.srcElement[window.event.propertyName];
if (pn == "description")
alert(pv);
}
document.body.description = "Content container"; // setter alert "Content container"
alert(document.body.description); // "Content container". Это не getter. Просто при присвоении значения у объекта добавился новый атрибут description
// Можно динамически создать DOM-элемент и навесить на него onproperychange
var el = document.createElement("DIV");
el.onpropertychange = function() {
var pn = window.event.propertyName;
var pv = window.event.srcElement[window.event.propertyName];
if (pn == "description")
alert(pv);
}
el.description = "Content container"; // хм. Странно, ничего не произошло...
// Добавим этот элемент в DOM-модель:
document.appendChild(el);
el.description = "Content container"; // setter alert "Content container"
* This source code was highlighted with Source Code Highlighter.
Тут есть отличия от предидущего
Object.defineProperty
:
- Это — событие, которое отрабатывает сразу после того, как любой атрибут объекта был изменён
- Это — событие, а не псевдо-атрибут, поэтому существующий одноимённый атрибут не затирается, а ведёт себя так, как и должен себя вести обычный атрибут. Хотя о какой одноимённости может тут идти речь? :)
Работает данный подход начиная с IE 5.0 (как
говорит MSDN) и по последнюю на данный момент версию. Ну и только для тех, кто
уже в DOM'е.
Выводы, или пока всё
Я оставлю возможность реализовать небольшой кросс-браузерный фреймворк — вам, товарищи :) Но скажу, что сделать его — реально. Хотя для IE это будет непросто.
Справка
В статье я указал, в каких
наиболее популярных браузерах работают те или иные механизмы getter'ов и setter'ов. Но ведь это далеко не все браузеры. Поэтому уточню:
Firefox 0.9+ | Engine:
Gecko 1.7+ (JS engine: SpiderMonkey/TraceMonkey)
Chrome 0.2+ | Engine:
WebKit 522+ (JS engine: V8)
Safari 3.0+ | Engine:
WebKit 522+ (JS engine: JavascriptCore)
Opera 9.5+ | Engine:
Presto 2.1+ (JS engine: Futhark)
IE 5.0 — IE 7.0 | Engine:
Trident (unversioned) (JS engine: Jscript 5.0 — Jscript 5.7)
IE 8.0+ | Engine:
Trident 4.0+ (JS engine: Jscript 5.8+)
Внимательно посмотрите на Engine/JS engine. Если я позабыл упомянуть ваш браузер, но он использует один из перечисленных движков (важна версия движка), то и работать в нём всё будет так же, как и в упомянутом браузере.
Всё, спасибо за внимание.