Javascript — очень изящный язык с кучей интересных возможностей. Большинство из этих возможностей скрыты одним неприятным фактором — Internet Explorer'ом и другим дерьмом, с которым нам приходится работать. Тем не менее, с приходом мобильных телефонов с актуальными браузерами и серверного JavaScript с нормальными движками эти возможности уже можно и нужно использовать прям сейчас. Но по привычке, даже при программировании для node.js мы стараемся писать так, чтобы оно работало в IE6+.
В этой статье я расскажу про интересный и не секретный способ указывать изящные геттеры и сеттеры и немножко покопаемся в исходниках Mootools. Частично это информация взята из статьи John Resig, частично лично мой опыт и эксперименты.
Что такое Геттеры и Сеттеры, надеюсь знают все. Обычно и кроссбраузерно это выглядит так:
Можно пойти дальше и написать более изящный вариант:
Но есть более удобный способ, который работает во всех серверных движках и современных браузерах, а именно Firefox, Chrome, Safari3+, Opera9.5+ — задание сеттера и геттера для свойства так, чтобы продолжать обращатся к свойству, как свойству. У такого подхода есть несколько преимуществ:
1. Более изящная запись. Представим ORM:
2. Если апи, которое базируется на свойствах уже есть и его нельзя менять (а очень нужно).
Есть два способа задать такой геттер/сеттер:
Из этого можно получить лёгкий способ определения, поддерживает ли браузер геттеры или не поддерживает:
Получить саму функцию геттера или сеттера можно через методы
Таким образом нашему target передадутся не значения родительского source, а функции-геттеры/сеттеры.
Некоторые замечания от John Resig:
Мутулз не поддерживает по-умолчанию такую возможность. И, хотя я уже предложил патч, мы можем с лёгкостью (слегка изменив исходники) заставить его понимать геттеры и сеттеры.
Итак, какая наша цель?
Более того, в классах унаследованных через Implements и Extends тоже должны работать геттеры и сеттеры родительского класса. Все наши действия будут происходить в файле [name: Class] внутри анонимной функции.
Во-первых, внутри функции, в самом верху, определим функцию, которая перезаписывает только геттеры и сеттеры. И хотя мы отказалась от устаревших браузеров — стоит застраховаться.
Конечно, если наш скрипт с такими геттерами попадёт в устаревший браузер, то он просто упадёт, но это страховка от того, чтобы кто-то случайно не взял этот файл и не прицепил его к себе на сайт, а потом недоумевал, что такое с ишаком.
Мы видим, что если __lookupGetter__ не поддерживается, то функция просто ничего не сделает.
Теперь заставляем работать getterы и setterы во время создания класса и наследования (Extends). Для этого:
Отдельным движением надо реализовать наследование геттеров и сеттеров от примесей (Implements). Для этого надо найти встроенные Мутаторы и добавить всего одну строку:
Все, теперь сеттеры и геттеры реализуются и мы с лёгкостью можем их наследовать и использовать. Наслаждайтесь)
В этой статье я расскажу про интересный и не секретный способ указывать изящные геттеры и сеттеры и немножко покопаемся в исходниках 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, enumerableObject.create
— удобно быстро создавать нужные объекты.