Геттеры и сеттеры в 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');
    


    Как быть с наследованием?


    Получить саму функцию геттера или сеттера можно через методы .__lookupGetter__ и .__lookupSetter__.
    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, а функции-геттеры/сеттеры.

    Что следует помнить


    Некоторые замечания от John Resig:
    * Для каждого свойства вы можете установить только один геттер и/или сеттер. Не может быть два геттера или сеттера
    * Единственный способ удалить геттер или сеттер — это вызвать delete object[name];. Эта команда удаляет и геттер и сеттер, потому если вы хотите удалить что-то одно, а другое — оставить, надо сначала сохранить его, а после удаления — снова присвоить
    * Когда вы используете __defineGetter__ или __defineSetter__ он просто тихонько перезаписывает предыдущий геттер или сеттер и даже удаляет само свойство с таким именем.
    * Проверить, поддерживает ли ваш браузер геттеры и сеттеры можно с помощью простого сниппета:
    javascript:foo={get test(){ return "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 Class = this.Class = new Type('Class', function(params){
    
    // сразу перед
    	newClass.$constructor = Class;
    	newClass.prototype.$constructor = newClass;
    	newClass.prototype.parent = parent;
    
    // Необходимо вставить функцию, которая расширит прототип (внимание, прототип, а не объект!) нашего класса:
    implementGettersSetters(newClass.prototype, params);
    


    Отдельным движением надо реализовать наследование геттеров и сеттеров от примесей (Implements). Для этого надо найти встроенные Мутаторы и добавить всего одну строку:
    Class.Mutators = {
    	// ...
    	Implements: function(items){
    		Array.from(items).each(function(item){
    			var instance = new item;
    			for (var key in instance) implement.call(this, key, instance[key], true);
    			// Вот она:
    			implementGettersSetters(this.prototype, instance);
    		}, this);
    	}
    };
    


    Все, теперь сеттеры и геттеры реализуются и мы с лёгкостью можем их наследовать и использовать. Наслаждайтесь)

    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 — удобно быстро создавать нужные объекты.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 68

      +3
      Есть вариант использовать геттеры и сеттеры в Жаваскрипте в ИЕ5+
      habrahabr.ru/blogs/javascript/75158/
      Одна проблема — только в глобальном namespace:( потому этот грязный хак практически неприменим в реальном проекте.
      • UFO just landed and posted this here
          0
          плюсик в конце заметили?:)
          • UFO just landed and posted this here
        0
        Мне кажется, стоило бы дополнить статью вот этим и этим
          0
          дополнил
          +2
          Лучше проверять равенство через ===, а не через ==
            0
            Был дан "+", потом кто-то поставил "-" комменту.
            Мне на рейтинг плевать, а вот почему я не прав, хотелось бы услышать :)
              +1
              Это был не я. Но если вы имели в виду кусок с

              typeof {}.__lookupGetter__ == 'function'

              То там вполне хватило бы "==", потому что typeof в любом случае вернет String (либо undefined) и дополнительная проверка не нужна
                +1
                function count(f, q){
                	console.profile()
                	while(q--){
                		f();
                	}
                	console.profileEnd()
                }
                function f1( x ){
                	return typeof x == 'undefined';
                }
                function f2( x ){
                	return typeof x === 'undefined';
                }
                
                count(f1, 1000);
                count(f2, 1000);
                


                Рез-ты:
                имя/всего/один вызов
                f1/1.012ms/0.108ms
                f2/0.811ms/0.001ms

                Это крохи, но таких сравнений в коде огромное кол-во + легче отловить ошибки несоответствия типов.
                  0

                  function count(f, q){
                  console.profile()
                  while(q--){
                  f(String.ololo);
                  }
                  console.profileEnd()
                  }
                  function f1( x ){
                  return typeof x == 'function';
                  }
                  function f2( x ){
                  return typeof x === 'function';
                  }

                  count(f1, 1000);
                  count(f2, 1000);


                  Профилирование (2.264ms, 1000 вызовов)
                  Профилирование (2.311ms, 1000 вызовов)

                  В любом случае в данном конкретном случае нечего отлавливать, причину я уже писал выше, избыточный код.
                    0
                    во-первых как у вас получилась, что 1.012ms — это 0.108ms за один вызов, а 0.811ms — это 0.001ms
                    такая ситуация действительно есть когда мы проверяем (typeof undefined). потому что происходит сравнение (undefined == 'undefined') и потому оба значения приводится к булевому виду (Boolean (undefined) == Boolean ('undefined')). Именно приведение и занимает это время.

                    И действительно в IE оно будет проверять работать так

                    Но, эта проверка вызывается один раз при инициализации — так какая разница?
                      0
                      Это крохи, но таких сравнений в коде огромное кол-во + легче отловить ошибки несоответствия типов.

                      Кстати, спустя годы можно дополнительно понять, почему спичечные оптимизации не имеют смысла. Последний Хром:
                      function count(f, q){
                          console.time(1)
                          while(q--){
                              f();
                          }
                          console.timeEnd(1)
                      }
                      function f1( x ){
                          return typeof x == 'undefined';
                      }
                      function f2( x ){
                          return typeof x === 'undefined';
                      }
                      
                      count(f1, 100000000); //  181.000ms
                      count(f2, 100000000); // 1034.000ms
                      
                    +2
                    видимо, за отсутствие аргументов.
                  +6
                  for (var i in topics.comments);
                  // vs
                  for (var i in topics.loadComments());

                  Может конечно первая запись более изящна, но вторая намного лучше подходит для продуктивной разработки ибо явно демонстрирует, что произойдет доступ к БД (или какой нибудь другой тяжелый запрос).

                  ЗЫ. Вообще не понимаю этого маниакального стремления прятать в геттерах тяжелые запросы. Эти грабли были уже посещены как минимум в EJB1...2, где из-за таких вот приемчиков генерировался нереальный сетевой трафик и огромное количество запросов к БД на каждый чих.
                    0
                    +1… Скрывать в геттере реализацию получения каких-то данных от БД не есть хорошо… Либо это уже будет не стандартный геттер/сеттер…
                      +2
                      Для меня сомнительны преимущества этих геттеров/сеттеров. Оно вроде как короче и иногда меньше скобок требует, но надо помнить кто из них метод, а кто проперти…

                      Есть такой вариант что когда-то в коде обращались напрямую к некому филду у объекта, а тут мы раз! сделаем её пропертёй и при неизменном клиентском коде легко так подпилим серверный функционал в нужном направлении. Но это тоже сомнительное достоинство.
                        +1
                        Может конечно первая запись более изящна, но вторая намного лучше подходит для продуктивной разработки ибо явно демонстрирует, что произойдет доступ к БД (или какой нибудь другой тяжелый запрос).


                        вот представим такую ситуацию с ORM.

                        var category = Orm.factory('category', id);
                        for (var i in category.topics) {
                          var topic = category.topics[i];
                          for (var k in topic.comments) {
                            var comment = topic.comments[k];
                            comment.author;
                          }
                        }
                        


                        Отлично, работает. Но при нагрузках начинает глючить. Лезем разбиратся и офигеваем от количества запросов. Меняем кусок:

                        var category = Orm.factory('category', id)
                          .includes('topics', 'topics.comments', 'topics.comments.author');
                        for (var i in category.topics) {
                          var topic = category.topics[i];
                          for (var k in topic.comments) {
                            var comment = topic.comments[k];
                            comment.author;
                          }
                        }
                        


                        Орм делает один запрос с джоинами, или 4 запроса без джоинов и мы получаем отлично работающее приложение с тем же айпи, вместо:

                        var category = Orm.factory('category', id)
                          .includes('topics', 'topics.comments', 'topics.comments.author');
                        for (var i in category.loadTopics()) {
                        

                        А ведь топики уже загружённые уже, а судя по коду можно подумать, что они опять грузятся)

                        Но вообще — это инструмент, я никого не заставляю им пользоватся, если кто-то не согласен с таким подходом
                          0
                          А ведь топики уже загружённые уже, а судя по коду можно подумать, что они опять грузятся)

                          Ну так надо назвать метод по другому :-))
                            +1
                            Что-то не приходит сходу название, подскажете?
                              0
                              getTopics()
                                +1
                                а чем это отличается от .topics? все равно по названию нельзя понять, что там происходит тяжёлое обращение к базе.
                                  0
                                  Ничем, и не должно если УЖЕ загружено и это следует из предыдущего кода.
                                  0
                                  На самом деле точно такое же двусмысленное имя. Из имени совершенно не следует, что топики уже загружены с сервера в объект и метод их только копирует из внутреннего представления. Ну разве что при использовании в браузере это чуть привычно, что get — быстро получить, а load — загрузить сервера. А если Node.js/Rhino/Jaxer? Реализовать lazy loading такое название тоже не мешает.
                                    0
                                    так название и, возможно, документация должны говорить, что делает этот метод.
                                    А от неправильного использования можно защититься с помощью разных проверок. Например, я бы сделал, чтобы get кидал исключение, если данные не были ранее загружены.
                                      0
                                      более того, реализация может менятся. сначала мы в этом методе грузим из базы. а потом — переписываем и грузим из кеша. потом переписываем и грузим в конструкторе. пользователю должно бытьт абсолютно всё равно. потому не вижу разницы между object.prop и object.getProp()
                            +1
                            браузеры без поддержки свойств упадут при парсинге, а не исполнении.
                            0
                            Сдаётся мне, «изящный вариант» неоптимален — зачем всю работу проделывать при конструировании каждого объекта если можно сделать это один раз? Вобщем, с технической стороны это не вариант, да и изящества я не обнаружил.
                              0
                              я вас не понял, какую работу проделывать при конструировании каждого объекта?
                              0
                              А мне лично ближе аксессоры а-ля jQuery:

                              var Foo = new Class({
                              	test: function ( value ) {
                              		if( value === undefined ) {
                              			console.log('test getted');
                              			return 'test';
                              		}
                              		console.log('test setted');
                              		return this;
                              	}
                              });
                              


                              Это удобнее тем, что сеттер может ещё вернуть какое-то значение: контекст для цепочек или удачность операции.
                              Так, правда есть немного надуманные или религиозные проблемы.
                                0
                                согласен, тоже люблю, когда сеттер возвращает значение.
                                0
                                Лично мне не хватает геттера и сеттера, которые будут вызываться при обращении к несуществующему свойству или методу.

                                Чтобы вместо

                                function Foo(bar) {
                                	this._bar = bar;
                                };
                                Foo.prototype = {
                                	set bar (bar) {
                                		this._bar = bar;
                                	},
                                	get bar () {
                                		return this._bar;
                                	}
                                };
                                


                                можно было так

                                function Foo(bar) {
                                	this._bar = bar;
                                };
                                Foo.prototype = {
                                	__set (name, arg) {
                                		this[name] = arg;
                                	},
                                	__get(name) {
                                		return this[name];
                                	}
                                };
                                

                                  0
                                  Хотел поддержать, но потом подумал ещё раз и понял что так можно запутать код и при корявом использовании отлов ошибок станет адом.
                                    +1
                                    Ну собственно сами по себе геттеры/сеттеры вводят в заблуждение скрывая вызов метода при обращении к свойству, так что запутаться можно в любом случае.
                                    А вот выше приведенная конструкция бывает очень полезна, хоть и редко, но как говориться — метко.
                                      0
                                      Решается аналогом в апи: Foo.set( 'name', argument );
                                      А отловить ошибки становится проще.
                                    0
                                    https://developer.mozilla.org/en/Javascript/Reference/Global_Objects/Object/noSuchMethod
                                      0
                                      про noSuchMethod я знаю, но к сожалению это не кроссбраузерно
                                        +1
                                        ну геттеры и сеттеры — тоже. для сервера подходит отлично
                                    +1
                                    Геттеры и сеттеры это то, чего ещё пока полноценно не хватает в JS, да. Ещё бы возможность следить за состоянием поля. Как здесь: https://developer.mozilla.org/en/Javascript/Reference/Global_Objects/Object/watch
                                      0
                                      посмотрите в сторону YUI3 — там есть атрибуты и события изменения значения аттрибутов
                                        0
                                        Да, я в курсе. Но предпочитаю нативную поддержку. Буду ждать.
                                      0
                                      Я думаю ваш патч не будет принят из-за несовместимости с некоторыми браузерами.
                                        +1
                                        этот патч совместим со всеми браузерами. точнее не так.
                                        те, кто не использует геттеры и сеттеры — ничего не узнают про этот патч, он на них не повлияет никак. а вот те, кто использует — сможет использовать их и в Мутулз.
                                          0
                                          этот патч совместим со всеми браузерами. точнее не так.
                                          те, кто не использует геттеры и сеттеры — ничего не узнают про этот патч, он на них не повлияет никак. а вот те, кто использует — сможет использовать их и в Мутулз.

                                          Если я начну использовать геттеры/сеттеры в своем скрипте, а он попадет в неподдерживаемый браузер, то что тогда?
                                          Мой код
                                          for(i in obj.property) { /* ... */ }
                                          

                                          не сделает ничего, а должен бы был пройтись по массиву данных которые вернул бы геттер, выдернув их откуда-то, и обработать их. Вот, стало заметно, что геттеры не поддерживаются…

                                          Лучше испозовать мутаторы, и оформить это как плагин, тогда: хочу — подключил, не хочу — и не надо.

                                          Код который не работает во всех браузерах указаных на оффициальном сайте MooTools (а там пишет IE6+) не пройдет модерацию.
                                            0
                                            Там уже так и предположили. Если вы будете использовать геттеры/сеттеры, и они попадут в неподдерживаемый браузер — то он отвалится на этапе компиляции с синтаксической ошибкой в вашем коде, будет этот патч в мутулз или нет.

                                            С Мутаторами решение там предложили и оно хуже, потому что:
                                            1. Не такое удобное (надо заносить все такие свойства в Мутатор Properties)
                                            2. Что самое главное — не наследуется через Implements, потому примешать код с геттерами и сеттерами не получит.
                                        +1
                                        Гетеры и сеттеры IRL:
                                          0
                                          Ну с сеттером ясно, а женщину за что ж вы так?
                                            0
                                            Тонкий намек на опечатку в статье: «Все, теперь сеттеры и гетеры реализуются и мы с лёгкостью можем их наследовать и использовать. Наслаждайтесь)».
                                        0
                                        getted? setted? английская языка оченно сложное.
                                          0
                                          минус за то, что подъебнули. исправил.
                                            0
                                            простите, не вижу, где
                                              +1
                                              не видите что?
                                                0
                                                не вижу, где что исправлено.
                                                  0
                                                  Я тоже не вижу, что и где исправлено.

                                                  Smth is set; smth is got. Ибо глаголы эти — неправильные: get/got/got, set/set/set.
                                                    –1
                                                    вместо того, чтобы подъебывать и разводить оффтоп, можно в личку написать, что не так.
                                                    исправил я опечатку в этом месте, а про irregular — забыл. у нас на экономике английский не преподают, а за 5 лет без практики много чего может вылететь.
                                                      0
                                                      Тебе помогают по существу сделать топик лучше, а ты ещё и не доволен?

                                                      P. S.: Интересно, откуда вообще берется желание использовать по факту незнакомый язык. Пиши на том, что знаешь.
                                                        –2
                                                        а ты мне на хами.
                                                        по существу топик лучше не стал. исправилась грамматическая ошибка, которая не влияла на понимаемость топика.
                                                        более того, ошибка указана в недопустимом тоне. если хочешь помочь человеку сделать топик лучше — можно написать в личку, где и что исправить. что, обычно, нормальные люди и делают, вместо того, чтобы разводить в топике про Javascript обсуждение моего знания английского языка
                                                        И не указывай мне, что делать. На чём хочу — на том пишу. И судя по рейтингу топика, большинству понравилось, как я пишу.
                                                        Уверен, если ты напишешь пару текстов на английском, то лингвисты тебе тоже тыкнут носом в несколько ошибок, умник.
                                                          0
                                                          1. Конкретные цитаты хамства с моей стороны последуют?

                                                          2. Налицо взаимоисключающие параграфы: рекомендации, что делать другим, раздаём, от аналогичных рекомендаций в свой адрес боли пониже спины испытываем. Забавно.

                                                          3. Гипотетические лингвисты не услышат от меня ничего, кроме «спасибо». Но это ж я, а не.
                                                            –2
                                                            я бы посмотрел на вас после подъёбок и рекомендаций перестать пользоваться английским языком от гипотетических лингвистов.
                                                              0
                                                              По секрету сообщу, что скиллы «моральная устойчивость» и «чувство собственного достоинства» неплохо прокачиваются.

                                                              Кстати, где ж примеры моего хамства?
                                                                –2
                                                                а ты ещё и не доволен?

                                                                  –1
                                                                  Живо представил муки души поэта от настолько прямо поставленного вопроса :)
                                                                    –1
                                                                    у меня больше нету желания разводить флейм и кормить тролля
                                                                      –2
                                                                      Однако ж, всем известно, кто первый кричит, «держи вора» :)

                                                                      Топик ты поправил, молодец. Надеюсь, «спасибо» говорить когда-нибудь научишься.
                                            +1
                                            Только мне кажется, что этот синтаксис геттеров и сеттеров выбивается из общей красивости js? Начиная с именования в стиле __xxx__, которое явно из питона притянуто (и вроде бы ранее в js не встречается), и заканчивая имхо неприлично большим количеством методов в object для поддержки геттеров и сеттеров.

                                            Такое ощущение, что это расширение стандарта делал какой-то другой человек, отличный от изначальных идеологов js.
                                              0
                                              Так и есть: в стандарте не слова о __xxx__ методах. Getters и setters это часть стандарта ES5. Автор либо поленился изучить вопрос, либо поленился об этом написать. Вместо того чтобы рассказать о стандарте и его поддержке, он рассказывал о proprietary свойствах.

                                              Вот почитайте лучше это: dmitrysoshnikov.com/ecmascript/es5-chapter-1-properties-and-property-descriptors/

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