Этот пост рассчитан на тех, кто знаком с объектами в JavaScript и знает, как прототип определяет поведение объекта, что такое функция-конструктор и как свойство .property конструктора относится к объекту, который он конструирует. Общее понимание синтаксиса ECMAScript 2015 тоже не помешает.
Мы всегда могли создать класс в JavaScript таким образом:
function Person (first, last) {
this.rename(first, last);
}
Person.prototype.fullName = function fullName () {
return this.firstName + " " + this.lastName;
};
Person.prototype.rename = function rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
Person это функция-конструктор, а также класс в JavaScript’овом понимании этого слова. ECMAScript 2015 дает возможность использовать ключевое слово class и т.н. “compact method notation”. Это синтаксический сахар для написания функций и присвоения методов его прототипу (там все чуть сложнее, но сейчас это не важно). Так что мы можем написать класс Person вот так:
class Person {
constructor (first, last) {
this.rename(first, last);
}
fullName () {
return this.firstName + " " + this.lastName;
}
rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
};
Клево. Но под капотом все равно есть функция-конструктор с привязкой к имени Person, и есть объект Person.prototype, который выглядит так:
{
fullName: function fullName () {
return this.firstName + " " + this.lastName;
},
rename: function rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
}
Прототипы это объекы
Если нужно изменить поведение объекта в JavaScript, можно добавить, удалить или изменить методы объекта через добавление, удаление или изменение функций, привязанных к свойствам этого объекта. В этом отличие от многих “классических” языков, в которых есть специальная форма (например, в Руби есть def) для задания методов.
Прототипы в JavaScript это “всего лишь объекты”, и благодаря этому мы можем добавлять, удалять или изменять методы прототипа через добавление, удаление или изменение функций, привязанных к свойствам этого прототипа.
Именно это и делает ECMAScript 5 код выше, и синтаксис class “рассахаривает” его в эквивалентный код.
Прототипы это “всего лишь объекты”, и это означает, что мы можем использовать любые техники, которые работают на объектах. Например, вместо привязки одной функции к прототипу, мы можем совершать массовую привязку с помощью Object.assign:
function Person (first, last) {
this.rename(first, last);
}
Object.assign(Person.prototype, {
fullName: function fullName () {
return this.firstName + " " + this.lastName;
},
rename: function rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
})
И, конечно, мы можем использовать компактный синтаксис если захотим:
function Person (first, last) {
this.rename(first, last);
}
Object.assign(Person.prototype, {
fullName () {
return this.firstName + " " + this.lastName;
},
rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
})
Mixins (примеси)
Так как class “рассахаривает” код в конструктор-функции и прототипы, мы можем использовать примеси вот так:
class Person {
constructor (first, last) {
this.rename(first, last);
}
fullName () {
return this.firstName + " " + this.lastName;
}
rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
};
Object.assign(Person.prototype, {
addToCollection (name) {
this.collection().push(name);
return this;
},
collection () {
return this._collected_books || (this._collected_books = []);
}
})
Мы только что “вмешали” методы по сбору книг в класс Person. Круто, что можно вот так просто писать код, но можно и давать названия:
const BookCollector = {
addToCollection (name) {
this.collection().push(name);
return this;
},
collection () {
return this._collected_books || (this._collected_books = []);
}
};
class Person {
constructor (first, last) {
this.rename(first, last);
}
fullName () {
return this.firstName + " " + this.lastName;
}
rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
};
Object.assign(Person.prototype, BookCollector);
Так можно продолжать сколько захочется:
const BookCollector = {
addToCollection (name) {
this.collection().push(name);
return this;
},
collection () {
return this._collected_books || (this._collected_books = []);
}
};
const Author = {
writeBook (name) {
this.books().push(name);
return this;
},
books () {
return this._books_written || (this._books_written = []);
}
};
class Person {
constructor (first, last) {
this.rename(first, last);
}
fullName () {
return this.firstName + " " + this.lastName;
}
rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
};
Object.assign(Person.prototype, BookCollector, Author);
Зачем использовать примеси
Сборка классов с помощью базовой функциональности (Person) и миксинов (BookCollector и Author) дает некоторые преимущества. Во-первых, иногда функциональность невозможно хорошо разложить на части в красивой древовидной структуре. Авторы книг могут быть корпорациями, а не людьми. И антикварные книжные лавки собирают книги так же, как книголюбы.
Такие примеси, как BookCollector или Author могут быть вмешаны в несколько разных классов. Попытки композиции функциональности с помощью наследования не всегда удачны.
Еще одно преимущество не так очевидно в простом примере, но в продакшн-системах классы могут разрастаться до нелепых размеров. Даже если примесь не используется в нескольких классах, декомпозиция большого класса с помощью примесей помогает удовлетворить принцип принцип единственной обязанности. Каждый миксин может иметь тольк одну область ответственности. Все это упрощает понимание и тестирование.
почему это важно
Существуют другие способы декомпозиции ответственности в классах (например, делегирование и композиция), но суть в том, что если вы решили использовать mixins, то это очень простой путь, потому что в JavaScript нет большого и сложного механизма ООП, который бы загонял вас в четкие рамки.
Например, в Руби использоват миксины легко, потому что с самого начала там есть специальная фича – модули. В других ОО-языках использовать миксины сложно, потому что система классов не поддерживает их, и они не очень вяжутся с мета-программированием.
Комментарии (11)
hell0w0rd
16.06.2015 16:19А еще можно использовать для этого декораторы, которые предложили для ES7 и уже давно есть в babel.
Вот пример:
// create-mixin.js function createMixin(obj) { return (SomeClass) => { Object.assign(SomeClass.prototype, obj); }; } // mixins/say-hello.js const SayHelloMixin = createMixin({ sayHello() { console.log(this.name); } }); // human.js @SayHelloMixin class Human { constructor(name) { this.name = name; } } const nkt = new Human('nkt'); nkt.sayHello();
gro
16.06.2015 17:44Синтаксический сахар, это i++.
С class'ами мы, наконец, можем программировать имея в голове только предметную область, а не думая параллельно, как правильно состыковать прототипы с конструкторами. А то, что под капотом всё те же прототипы, так можно и всё назвать сахаром над машинными командами.Zenitchik
16.06.2015 21:46+1А раньше не могли?
А то, что под капотом прототипы — это прекрасно. Это позволяет использовать оба подхода одновременно.gro
17.06.2015 12:10-2>А раньше не могли?
Раньше, если совсем без всяких обёрток, параллельно с предметной областью нужно было в голове держать низкоуровневую реализацию и следить за тем, чтобы все свойства типа «constructor», «prototype» были правильно установлены.
>А то, что под капотом прототипы — это прекрасно. Это позволяет использовать оба подхода одновременно.
В JS вообще любую вещь можно десятком разных способов сделать.
По моему скромному мнению, это не так чтобы преимущество.
Хотя многие, конечно, думают иначе.Zenitchik
17.06.2015 14:52Установка prototype затруднений не вызывает.
C constructor — отдельная тема. Спецификация неудобна. С точки зрения эксплуатации было бы лучше, чтобы constructor устанавливалось в каждый объект конструктором, а не наследовалось из прототипа, но встроенные объекты сделаны иначе. Увы.
Конечно, без обёртки большой проект не напишешь, посмотрим, как приживётся нативный class.
На счёт прототипов — я в последнее время думаю над такой обёрткой, которая позволила бы удобно использовать оба варианта, причём, чтобы с прототипом тоже можно было работать, и это предсказуемо влияло на отнаследованные от него объекты. Ну, например, у меня несколько комбо-боксов с общим списком, логично было бы положить список в общий прототип группы комбо-боксов, и менять его там, организовав реакцию наследников на его изменения. Пока чёткой архитектуры (а главное — удобного API) для этого не выдумал, но обязательно выдумаю.gro
17.06.2015 15:31Своя обёртка для классов, это, конечно, полезное упражнение для ума.
Но в последнее время это уже несколько моветон.
Уже и ECMA новый и TypeScript и всё остальное. Лучше на что-то другое силы тратить.Zenitchik
17.06.2015 16:16Моветон — если она ничем не лучше аналогов. А если даёт возможность снять классовые шоры — то это стоящая задача.
Мне скорее представляется моветоном зоопарк языков, компилирующихся в другой интерпретируемый язык…gro
17.06.2015 16:22Любой человек, который имел сколько-то плотное общение с JS обязательно делал обёртку над прототипами, которая обязательно снимала шоры и всё такое.
Zenitchik
17.06.2015 21:57Либо брал чужую и допиливал. Но использования прототипности я не видел. Везде просто реализация класса (что теперь можно и нативно сделать).
turbo_exe
хороший способ композиции, но надо соблюдать какие-нибудь name conventions чтобы одна примесь не помешала другой (не перезаписала) что-либо выполнить в рамках контекста this. js в данном случае не плюнет никаким exception'ом.
StreetStrider
Если примешивалка будет немного сложней чем
Object.assign
, то можно сделать предупреждения, и даже более сложные вещи, типа проверки вхождения в миксин.