В своём фреймворке 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.