Pull to refresh

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

Reading time 4 min
Views 6.8K

В своём фреймворке 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.
Tags:
Hubs:
+43
Comments 31
Comments Comments 31

Articles