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

JavaScript Getters and Setters


It is with much happiness that I think I can finally say, without seeming like a fool, that: "JavaScript Getters and Setters are now prevalent enough to become of actual interest to JavaScript developers." Wow, I've been waiting a long time to be able to say that.
I want to start by giving a whirlwind tour of Getters and Setters and why they're useful. Followed by a look into what platforms now support Getters and Setters as to make them relevant.

Getters and Setters

Getters and Setters allow you to build useful shortcuts for accessing and mutating data within an object. Generally, this can be seen as an alternative to having two functions with an object that are used to get and set a value, like so:
{
    getValue: function(){
        return this._value;
    },
    setValue: function(val){
        this._value = val;
    }
}
The obvious advantage to writing JavaScript in this manner is that you can use it obscure values that you don't want the user to directly access. A final result looking something like the following (using a closure to store the value of a newly constructed Field):
function Field(val){
    var value = val;
   
    this.getValue = function(){
        return value;
    };
   
    this.setValue = function(val){
        value = val;
    };
}
Some example results:
var field = new Field("test");
field.value
// => undefined
field.setValue("test2")
field.getValue()
// => "test2" 
 
Now, centered around this concept, is where getters and setters come into play. They allow you to bind special functions to an object that look like normal object properties, but actually execute hidden functions instead. The end result looks something like this:
var field = new Field("test");
field.value
// => test
field.value = "test2";
field.value
// => "test2" 
 
So let's look at how you would go about setting something like that up. Mimicking the "hidden value property" style of before, our code would look something like this:
function Field(val){
    var value = val;
   
    this.__defineGetter__("value"function(){
        return value;
    });
   
    this.__defineSetter__("value"function(val){
        value = val;
    });
}
Now, if we wanted to, instead, define getters and setters within the context of our object prototype (and where having "private" data is less of a concern) we can then use an alternative object syntax for that.
function Field(val){
    this.value = val;
}
Field.prototype = {
    get value(){
        return this._value;
    },
    set value(val){
        this._value = val;
    }
};
The syntax for getters and setters is typically what scare people the most about the feature. But after a little bit of use, it's easy to get over.
Here's another example, allowing a user to access an array of usernames (but denying them access to the original, underlying user objects.
function Site(users){
    this.__defineGetter__("users"function(){
        // JS 1.6 Array map()
        return users.map(function(user){
            return user.name;
        });
    };
}
As a bonus, here's a method that I've written that can help you to extend one object with another (a common JavaScript operation) while still taking into account getters and setters:
// Helper method for extending one object with another
function extend(a,b) {
    for ( var i in b ) {
        var g = b.__lookupGetter__(i), s = b.__lookupSetter__(i);
     
        if ( g || s ) {
            if ( g )
                a.__defineGetter__(i, g);
            if ( s )
                a.__defineSetter__(i, s);
         } else
             a[i] = b[i];
    }
    return a;
}
This code is from the Server-Side Browser Environment that I wrote about a week ago. You'll notice that, when looking through the code that I make liberal use of getters and setters to create this mock environment.
Additionally, in my custom extend() method you'll notice two new methods: __lookupGetter__ and __lookupSetter__. These are immensely useful, once you start dealing with getters and setters.
For example, when I did my first pass at writing an extend() method, I started getting all sorts of errors - I was thoroughly confused. That's when I realized that two things were happening with the simple statement: a[i] = b[i];
If a setter existed in object a, named i, and a getter existed in object b, named i, a[i]'s value was being set not to the other setter function, but to the computed value from b's getter function. The two __lookup*__ methods allow you to access the original functions used for the methods (thus allowing you to write an effective extend method, for example).
A couple things to remember:
  • You can only have one getter or setter per name, on an object. (So you can have both one value getter and one value setter, but not two 'value' getters.)
  • The only way to delete a getter or setter is to do: 'delete object[name];' Be aware, that this command is capable of deleting normal properties, getters and setters. (So if all you want to do is remove a setter you need to backup the getter and restore it afterwards.)
  • If you use __defineGetter__ or __defineSetter__ it will silently overwrite any previous named getter or setter - or even property - of the same name.

Platforms

Now, here is where things get really interesting. Up until just recently, talking about actually using getters and setters (outside of the context of Firefox/Gecko) was pretty much not happening.
However, look at this line up:
Browsers
  • Firefox
  • Safari 3+ (Brand New)
  • Opera 9.5 (Coming Soon)
I used the following snippet to test browsers:
javascript:foo={get test(){ return "foo"}};alert(foo.test);
Additionally, the following JavaScript environments support Getters and Setters
And as do the following ECMAScript implementations
  • ActionScript
  • JScript.NET 8.0

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. Если я позабыл упомянуть ваш браузер, но он использует один из перечисленных движков (важна версия движка), то и работать в нём всё будет так же, как и в упомянутом браузере.

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

Getters And Setters With JavaScript – Code Samples And Demos


Not many people know it, but you can use “real” getters and setters in JavaScript if you want to.

De-Facto Offerings

Firefox 2.0+, Safari 3.0+, Google Chrome 1.0+ and Opera 9.5+ support a de-facto way of getters and setters:
01.var lost = {
02.loc : "Island",
03.get location () {
04.return this.loc;
05.},
06.set location(val) {
07.this.loc = val;
08.}
09.};
10.lost.location = "Another island";
11. 
12.// lost.location will now return "Another island"
And on DOM elements:
01.HTMLElement.prototype.__defineGetter__("description"function () {
02.return this.desc;
03.});
04.HTMLElement.prototype.__defineSetter__("description"function (val) {
05.this.desc = val;
06.});
07.document.body.description = "Beautiful body";
08. 
09.// document.body.description will now return "Beautiful body";

Via Object.DefineProperty

The future, and ECMAScript standardized way, of extending objects in all sorts of ways is throughObject.defineProperty. This is how Internet Explorer chose to implement getters and setters, but it is unfortunately so far only available in Internet Explorer 8, and not in any other web browser. Also, IE 8 only supports it on DOM nodes, but future versions are planned to support it on JavaScript objects as well.
Getter and setters in IE8+:
01.Object.defineProperty(document.body, "description", {
02.get : function () {
03.return this.desc;
04.},
05.set : function (val) {
06.this.desc = val;
07.}
08.});
09.document.body.description = "Content container";
10. 
11.// document.body.description will now return "Content container"

Test Cases

Conclusion

Kudos for Microsoft to take the step to implement Object.defineProperty, although it’s sad that it’s only available for DOM elements as of now. Also, just as we have consistency between web browsers with innerHTMLXMLHTTPRequest etc, it would have been really desirable if Microsoft would have supported the several years-old de-facto way of implementing getters and setters.
So, Microsoft slowly treads forward, which is good, but at the same unfortunately gives us yet another case of doing something different to make it work in Internet Explorer (more about this can be read inECMAScript getters and setters interoperability).
Anyway, getters and setters are here today, and with some feature detection, you can use it to implement some nice things. Happy coding! :-)