Кроссбраузерные аксессоры в JavaScript


    В своём фреймворке AtomJS я активно использую аксессоры — геттеры и сеттеры:

    Foo = atom.Class({
        get bar () {
            return this._bar;
        },
        set bar (bar) {
            this._bar = bar;
        }
    });
    


    Я уже описывал теорию, но в топике я расскажу о том, как заставить их работать во всех современных браузерах, а именно — как разрулить ситацию с тем, что Internet Explorer 9 ничего не знает о __defineSetter__ и подобных методах.



    get/set


    Благо, в этом плане у всех браузеров всё приблизительно одинаково. IE9+, Opera10+, Fx3.5+, Chrome — все поддерживают эту запись и одинаковы что поведением, что синтаксисом. Единственно, что, если в Internet Explorer 9 выскочит ошибка "Предполагается наличие ':' " — проверьте, не перешёл ли браузер в режим «Багодром» «Interner Explorer 7»

    defineProperty


    Тут ничего сложного

    Для нестандартных __defineSetter__ и __defineGetter__ есть альтернатива из EcmaScript5 — Object.defineProperty. По-умолчанию в объектах свойства configurable и enumerable объявляются как true, потому мы можем запросто написать стандартную альтернативу:
    instance.__defineSetter__(propertyName, setterFn);
    instance.__defineGetter__(propertyName, getterFn);
    // =>
    Object.defineProperty(instance, propertyName, {
    	set: setterFn,
    	get: getterFn,
    	enumerable  : true,
    	configurable: true
    });
    


    getOwnPropertyDescriptor


    Для нестандартных __lookupSetter__ и __lookupGetter__ тоже есть альтернатива из EcmaScript5 — Object.getOwnPropertyDescriptor.

    Тут всё чуть менее радостно, но не критично. Секрет в том, что __lookup*__ ищет акссессор по всей цепочке прототипов, в то время, как getOwnPropertyDescriptor — только в личных свойствах:
    Returns a property descriptor for an own property (that is, one directly present on an object, not present by dint of being along an object's prototype chain) of a given object.


    То есть, мы имеем следующую ситацию:
    var MyClass = function () {};
    MyClass.prototype = {
    	get foo() { return 42; }
    };
    var instance = new MyClass();
    console.log(instance.__lookupGetter__('foo')); // function foo() { return 42; }
    console.log(Object.getOwnPropertyDescriptor(instance, 'foo')); // undefined
    


    Хотя геттер на самом деле есть:
    console.log(instance.foo); // 42
    


    1. Мне кажется более правильным и логичным поведение нестандартных свойств
    2. Оно больше подходит к идее моего фреймворка
    3. Важно, чтобы все браузеры вели себя одинаково. Как именно — менее важно

    Потому мы рекурсивно обойдём всю цепочку прототипов при помощи метода Object.getPrototypeOf, пока не упрёмся или в null, или в определенное свойство или в аксессор.

    function getPropertyDescriptor (from, key) {
    	var descriptor = Object.getOwnPropertyDescriptor(from, key);
    	if (!descriptor) {
    		// Если дескриптор не найден - рекурсивно ищем дальше по цепочке прототипов
    		var proto = Object.getPrototypeOf(from);
    		if (proto) return getPropertyDescriptor(proto, key);
    	
    	// Если дескриптор найден, проверяем, что он имеет сеттер или геттер (а не просто значение)
    	} else if ( descriptor.set || descriptor.get ) {
    		return {
    			set: descriptor.set,
    			get: descriptor.get
    		};
    	}
    	// или не найден дескриптор, или это обычное свойство без аксессоров
    	return null;
    };
    


    Собираем все в библиотеку


    Теперь мы можем применить полученные знания и сделать библиотеку для кроссбраузерного указания аксессоров.
    По моим личным наблюдениям нестандартные методы работают чуть быстрее и они требуют меньше хаков, потому возьмём их за умолчание.
    Также, мне нравятся названия lookup и define — они лаконичные и понятные, потому их и используем.
    Содержимое функции lookup для каждого из способов кардинально различаются, потому мы просто создадим две разные функции и не будем делать лишних проверок каждый раз
    (function (Object) {
    	var standard = !!Object.getOwnPropertyDescriptor, nonStandard = !!{}.__defineGetter__;
    
    	if (!standard && !nonStandard) throw new Error('Accessors are not supported');
    	
    	var lookup = nonStandard ?
    		function (from, key) {
    			var g = from.__lookupGetter__(key), s = from.__lookupSetter__(key);
    
    			return ( g || s ) ? { get: g, set: s } : null;
    		} :
    		function (from, key) {
    			var descriptor = Object.getOwnPropertyDescriptor(from, key);
    			if (!descriptor) {
    				var proto = Object.getPrototypeOf(from);
    				if (proto) return accessors.lookup(proto, key);
    			} else if ( descriptor.set || descriptor.get ) {
    				return {
    					set: descriptor.set,
    					get: descriptor.get
    				};
    			}
    			return null;
    		};
    
    	var define = nonStandard ?
    		function (object, prop, descriptor) {
    			if (descriptor) {
    				if (descriptor.get) object.__defineGetter__(prop, descriptor.get);
    				if (descriptor.set) object.__defineSetter__(prop, descriptor.set);
    			}
    			return object;
    		} :
    		function (object, prop, descriptor) {
    			if (descriptor) {
    				var desc = {
    					get: descriptor.get,
    					set: descriptor.set,
    					configurable: true,
    					enumerable: true
    				};
    				Object.defineProperty(object, prop, desc);
    			}
    			return object;
    		};
    		
    	this.accessors = {
    		lookup: lookup,
    		define: define
    	};
    })(Object);
    


    Теперь можно объявлять аксессоры в объектах:
    MyClass = function (param) {
    	var property = param;
    	
    	accessors.define(this, 'property', {
    		set: function (value) {
    			property = value;
    		},
    		get: function () {
    			return property;
    		}
    	});
    };
    
    var instance = new MyClass(42);
    console.log(instance.property); // 42
    
    console.log(accessors.lookup(instance, 'property')); // getter+setter
    


    Наследование


    Теперь расширим немного нашу библиотеку, добавив метод inherit. Он будет получать аксессор свойства с именем key из объекта from, и добавлять его в объект to. Если удачно — вернет true, иначе — false.
    	this.accessors = {
    		lookup: lookup,
    		define: define,
    		inherit: function (from, to, key) {
    			var a = accessors.lookup(from, key);
    
    			if ( a ) {
    				accessors.define(to, key, a);
    				return true;
    			}
    			return false;
    		}
    	};
    


    Этот метод поможет нам написать аналог функции jQuery.extend или Object.merge из MooTools, поддерживающий акссессоры, в то время, как все обычные фреймворки ничего не знают о них:

    var object = jQuery.extend({}, { get foo(){ return null; } });
    console.log( object.__lookupGetter__('foo') ); // undefined
    console.log( object.foo ); // null
    


    Напишем свой вариант (внимание, этот вариант создан в учебных целях и в реальном приложении использоваться не должен)

    function extend(from, to) {
    	for (var i in to) {
    		// пробуем унаследовать аксессор
    		if (!accessors.inherit(from, to, i)) {
    			// если акссессора не унаследовался - пробуем записать напрямую
    			from[i] = to[i];
    		}
    	}
    	return from;
    };
    var object = extend({}, { get foo(){ return null; } });
    
    console.log( object.__lookupGetter__('foo') ); // getter
    console.log( object.foo ); // null
    


    Вывод


    Штука очень удобная. У меня есть два узкоспециализированных, но достаточно мощных фреймворка — AtomJS и LibCanvas, где использование аксессоров сполна оправдало себя. Если вы можете позволить себе отказаться от ослов ниже девятой версии — оно того стоит, получите массу удовольствия.

    Описанное в топике решение, слегка расширенное, изначально реализовано как плагин AtomJS — Accessors.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 31

      +1
      Актуально. Как раз задавался вопросом: как разрулить ситуацию с IE и считал до сих пор эту возможность некроссбраузерной.
        –2
        Сливать пользователей IE на связку эксплойтов, удалять IE, и ставить google chrome, либо что-нить на выбор :)
        +2
        отказаться от ослов ниже девятой версии
        Лет через пять, если повезет. ))
          +1
          Ну кому-то уже повезло)
          +7
          Имхо не совсем оправдано.
          Да — вы получаете более менее удобный интерфейс, более того — проперти можно даже в IE6 запустить(через VBScript)
          Но давайте будем реалистами — jsperf.com/getters-and-setters — доступ к проперти в 10-20 раз медленее чем доступ к переменной.
          И в теже 10-20 раз медленее чем вызов функции( setValue\getValue)
          Это крутовато.
          Кстати — с недавних пор в ФФ4 вызов функции стал раза в 3 быстрее доступа к переменной — кто может обьяснить?
            0
            тесты в студию. у меня другие данные
              0
              повторюсь — jsperf.com/getters-and-setters
                0
                гомэн, не заметил. это кривой бенчмарк со странными результатами. вызов метода, который получает значение поля ну никак не может быть быстрее прямого получения значения поля.
                по моим тестам разница не более 2 раз
                  0
                  такая магия только в самой последней версии хрома.
                  Также резульаты теста ОЧЕНЬ сильно плавают от системы к системе.
                  В смысле под бунтой\маком\виндой на одинаковых билдах браузеров будут стабильно разные результаты.
                  Разные в разы. Фантастика
                    0
                    С хромом всегдатакое будет, у него оптимизации очень сильно отличаются на разных системах.
              0
              Потестил в хроме, гетеры / сеттеры в 3 раза медленнее setValue / getValue / прямого доступа.
                  0
                  Тестил в консоли:

                  a = { a: 1, get aa(){ return this.a; }, set aa(a){ this.a = a; }, getA: function() { return this.a; }, setA: function(a) { this.a = a; } }
                  function test(f, i){ console.time('test'); while(--i > 0) { f(); } console.timeEnd('test'); };
                  test(function(){ a.a }, 1000000);
                  test(function(){ a.aa }, 1000000);
                  test(function(){ a.getA() }, 1000000);
                  test(function(){ a.a=10 }, 1000000);
                  test(function(){ a.aa=10 }, 1000000);
                  test(function(){ a.setA(10) }, 1000000);

                  На jsperf.com/getters-setters-vs-direct результаты получились совсем другие.
                    0
                    потому что у вас тест не правильный.
                    Если вы каждую строчку оборачиваете в функцию — получаете дикий overhead на ее использование.
                    А jsperf дублирует тест много много раз и получает реальные, более точные данные.
                  +1
                  доступ к проперти в 10-20 раз медленее чем доступ к переменной.

                  Я делаю приложение, где выполняются тысячи операций в секунду. И поверьте мне — они не тормозят.
                  lookup и define — да, тормозят, но при должном подходе они вызываются крайне редко)
                  0
                  Эээ, получается ты каждому новому экземпляру отдельно прописываешь ассессоры?

                  MyClass = function (param) {
                  	var property = param;
                  	
                  	accessors.define(this, 'property', {
                  		set: function (value) {
                  			property = value;
                  		},
                  		get: function () {
                  			return property;
                  		}
                  	});
                  };
                  

                  В пицот раз медленнее, чем унаследовать стандартный getProperty/setProperty из прототипа же! Не?

                  И, бтв, если ты в анонимной фукнции пишешь что-то типа
                  	this.accessors = {
                  		lookup: lookup,
                  		define: define
                  	};

                  то, имхо, красивее эту самую фукнцию вызывать через .call(window, Object), вместо просто (Object). Нагляднее как-то, что-ли.
                    +1
                    то, имхо, красивее эту самую фукнцию вызывать через .call(window, Object), вместо просто (Object). Нагляднее как-то, что-ли.

                    А на сервере — нету никакого window ;)

                    Эээ, получается ты каждому новому экземпляру отдельно прописываешь ассессоры?

                    На самом деле это всего лишь пример. Я делаю так:
                    MyClass = function(){};
                    MyClass.prototype = {
                      get property ( ) {},
                      set property (v) {}
                    };
                    

                    А вышеуказанная библиотека нужна в первую очередь для наследования и некоторой специфической магии
                      0
                      А на сервере — нету никакого window ;)

                      А еще на сервере не надо писать кроссбраузерный код :)
                        0
                        Согласен) Но когда делаешь либу — надо предвидеть такие вещи.

                        Скажем, LibCanvas может используется и на сервере для операций с точками, прямоугольниками, или для анимированного изменения свойств.

                        Таким образом, все эти объекты тянут кое-какие костыли в библиотеке и на сервер.
                          0
                            0
                            Ну V8 от SM отличается на копейки. Это же не какой-нибудь там Rhino.

                            Так что под SM скорее всего ничего переписывать не придется.
                              0
                              Не сильно, но отличаются: __noSuchMethod__ (есть в виде форков), let
                      0
                      Если бы можно было игнорировать ослов, жизнь была бы прекрасней :)
                        0
                        А вы просто закройте глаза и представьте осенний солнечный день в парке и сладкую вату.
                          0
                          На работе не всегда получается, а для себя я могу и сходить, зачем же представлять?
                        +1
                        В блоге WebReflection была статья, описывающая подобные ухищрения. Вот исходник библиотечки devpro.it/code/212.html
                          +1
                          Не, ну там ведь не совсем то. Я про настоящие сеттеры, а не про гибридные пишу)
                          На тему того, что описано в статье на WebReflection, я писал мутатор для Мтулуз:
                          var MyClass = new Class({
                             Properties: {
                                foo: {
                                   set: function (foo) {
                                      this._foo = foo;
                                   },
                                   get: function () {
                                      return this._foo;
                                   }
                                }
                             }
                          });
                          
                          var instance = new MyClass();
                          instance.foo(123);
                          alert(instance.foo());
                          


                          Код мутатора:
                          Class.Mutators.Properties = function (properties) {
                            for (var name in properties) (function (name, prop) {
                                this.prototype[name] = function (value) {
                                   if (arguments.length) {
                                      // setter
                                      if (!prop.set) throw new Error('No setter for ' + name);
                                      prop.set.call(this, value);
                                      return this;
                                   } else {
                                      // getter
                                      if (!prop.get) throw new Error('No getter for ' + name);
                                      return prop.get.call(this);
                                   }
                                };
                             }.call(this, name, properties[name]));
                          };
                          
                            0
                            Я вижу, просто для полноты темы решил добавить ссылку
                          0
                          Ох, а мы всё ещё ie6 поддерживаем…
                            0
                            я правильно понял, что работать будет только в:
                            IE10, FF 2.0+, Safari 3.0+, Chrome 1.0+, Opera 9.5+
                            (источник: robertnyman.com/javascript/javascript-getters-setters.html)
                            ?
                              0
                              В ie9 работает. Остальное — правильно

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