
В своём фреймворке 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 выскочит ошибка "
Предполагается наличие ':' " — проверьте, не перешёл ли браузер в режим 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.
