Pull to refresh

Геттеры и сеттеры в Javascript

Reading time 5 min
Views 48K
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 — удобно быстро создавать нужные объекты.
Tags:
Hubs:
+80
Comments 68
Comments Comments 68

Articles