четверг, 29 сентября 2011 г.

Getters & Setters в Javascript

Много людей знают, что такое 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.

Данный код прекрасно работает в:
Icon: FirefoxFF 1.0+, Icon: ChromeChrome 0.2+, Icon: SafariSafari 3.0+, Icon: OperaOpera 9.5+
И не работает в:
Icon: IEIE — все версии, включая 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.

Данный код прекрасно работает в:
Icon: FirefoxFF 1.0+, Icon: ChromeChrome 0.2+, Icon: SafariSafari 3.0+, Icon: OperaOpera 9.5+
И не работает в:
Icon: IEIE — все версии, включая 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.

Данный код прекрасно работает исключительно в:
Icon: FirefoxFF 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'ов. Но ведь это далеко не все браузеры. Поэтому уточню:
Icon: FirefoxFirefox 0.9+ | Engine: Gecko 1.7+ (JS engine: SpiderMonkey/TraceMonkey)
Icon: ChromeChrome 0.2+ | Engine: WebKit 522+ (JS engine: V8)
Icon: SafariSafari 3.0+ | Engine: WebKit 522+ (JS engine: JavascriptCore)
Icon: OperaOpera 9.5+ | Engine: Presto 2.1+ (JS engine: Futhark)
Icon: IEIE 5.0 — IE 7.0 | Engine: Trident (unversioned) (JS engine: Jscript 5.0 — Jscript 5.7)
Icon: IEIE 8.0+ | Engine: Trident 4.0+ (JS engine: Jscript 5.8+)

Внимательно посмотрите на Engine/JS engine. Если я позабыл упомянуть ваш браузер, но он использует один из перечисленных движков (важна версия движка), то и работать в нём всё будет так же, как и в упомянутом браузере.

Всё, спасибо за внимание.

Комментариев нет: