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: Firefox FF 1.0+, Icon: Chrome Chrome 0.2+, Icon: Safari Safari 3.0+, Icon: Opera Opera 9.5+
    И не работает в:
    Icon: IE 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.

    Данный код прекрасно работает в:
    Icon: Firefox FF 1.0+, Icon: Chrome Chrome 0.2+, Icon: Safari Safari 3.0+, Icon: Opera Opera 9.5+
    И не работает в:
    Icon: IE 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.

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

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

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 26

      +1
      По сути, мы можем задавать именованные функции-обработчики. Вот и всё. Не знаю, зачем это кому-то понадобится.
      Я думаю это на будущее для strict mode из ECMAscript 5, там хотят избавиться от arguments.callee.
      Internet Explorer, как всегда, сказал «мне с вами не по пути» и сделал getter'ы и setter'ы своим собственным способом.
      defineProperty тоже, кстати, из ECMAscript 5, так что тут все хорошо, Microsoft стоит упрекать только в том, что этот функционал доступен для встроенных объектов.
        0
        Да, defineProperty — это неплохо. Вот только плохо, что все, кроме IE, не реализовали defineProperty. А IE — напротив, не реализовал get/set/__define[GS]etter__
          +2
          удивительно, один IE реализовал стандарт ECMAscript, а остальные не хотят этого делать
            0
            Когнитивный диссонанс :)
              0
              Удивительно, но стандарт не предоставляет возможностей, аналогичных __lookupGetter__, т.е. программно невозможно понять это геттер\сеттер или обычный объект, что бывает иногда необходимо.
          +1
          Прекрасный пост. Беру на заметку.
            –3
            В примере для Шаг 2 Вы сделали ошибку:
            Вы описываете функцию-конструктор, но вместо this._secretProperty = newVal; написали var _secretProperty = newVal;

              +1
              Это как раз таки и есть суть шага 2, переменная _secretProperty объявлена через var и является локальной, доступна только внутри функции (в данном случае функции-конструктора), getter'ы/setter'ы получают к ней доступ через механизм замыканий, иначе кроме как через них получить к ней доступ извне не получится, вот соот-но и выходит приватное свойство.
                0
                Теперь понятно :)
                0
                Вам следует почитать про замыкания в JS.
                +1
                document.body.onpropertychange = function() {
                  var pn = event.propertyName;
                  var pv = event.srcElement[event.propertyName];
                  if (pn == "description")
                    alert(pv);
                }


                * This source code was highlighted with Source Code Highlighter.


                Для IE 5.0+ event не определен, event = window.event
                  +1
                  IE 5.0 — IE 7.0 | Engine: Trident (unversioned) (JS engine: Jscript 5.0 — Jscript 7.0)
                  IE 8.0+ | Engine: Trident 4.0+ (JS engine: Jscript 8.0+)


                  С версиями напутали, поправьте пожалуйста.
                    0
                    Спасибо, исправлю
                  +14
                  > Это — самый простой и очевидный случай. Но и самый плохой. Храня данные в this, мы никак не скроем их от «злого хакера».

                  Ох уж эти злые хакеры. Они хотят изменить мои объекты, сволочи!!!

                  На самом деле мотивацией введения getter/setter является абстракция данных, а не «злые хакеры» (data abstraction != data hiding).
                  • UFO just landed and posted this here
                      0
                      Спасибо, исчерпывающая информация о геттерах/сеттерах.
                      А в IE8 всё-таки красивый синтаксис…
                      • UFO just landed and posted this here
                          0
                          Насчет других браузеров незнаю, а та же опера например позволяет выполнять яваскрипт на текущей странице через адресную строку, ну и возможно с помощью userjs можно вклиниться в работающий скрипт, насчет этого не уверен.
                          • UFO just landed and posted this here
                              0
                              Я неверно высказался. Тут скорее защита не от хакеров, а от других разработчиков :)
                          0
                          жаль, что для IE нужно так извращаться:( — я бы не хотел чтобы все мои объекты на поверку были дом нодами:(
                            0
                            Да, это наверное самое главное препятствие перед использованием данных техник на практике
                            0
                            на самом деле с ИЕ не так все плохо mitasovr.habrahabr.ru/blog/75158/
                              0
                              Cупер, спасибо за инфу, буду знать. В Интернете я её не нашёл (хотя я просто позабыл про VBscript)
                              0
                              Сам случайно наткнулся, пока не знаю зачем, но наверняка такую технику еще для чего нибудь можно использовать :)
                                0
                                эх, не туда коммент запостил (

                              Only users with full accounts can post comments. Log in, please.