Обзор возможностей современного JavaScript
JavaScript, наверное, самый известный мультипарадигменный язык, в котором очень много неочевидных особенностей. Но тем не менее любим ли мы его или ругаем, факт остается фактом — это основной язык, на котором работает современный web.
В ушедшем году, вышел стандарт ECMAScript 2015 (неформально ES6), который сильно изменил, то к чему мы привыкли. Появилась масса новых возможностей, которые по сути представляют собой современное надмножество языка, пытающегося решить существующие проблемы. Class, let, const, стрелочные функции… разработчик, который ранее не видел код, написанный на ES6 не сразу догадается, что перед ним, по сути, старый добрый JS.
Есть масса прекрасных статей, посвященных современному стандарту. В этом же посте я хочу показать, что нам может предложить современный JS, когда необходимо решить насущную задачу. Например, поздравить всех c Новым Годом.
Императивный подход
Самый простое решение, сделать все императивно. Да, действительно, если следовать принципу «меньше больше — больше меньше», то это будет самое прагматичное решение:
function sayHappyNewYear(year){
console.log('Happy New ' + year + ' Year!');
}
sayHappyNewYear(2016);
Решение простое, и оно будет работать во всех средах, где есть реализация объекта console, но в нем есть изъян: у нас есть не только Новый Год, но и множество других праздников. Создавать для каждого праздника свою отдельную функцию не очень разумное занятие. Если нам необходимо отформатировать сообщения однообразно, то при изменении формата сообщения необходимо будет изменить соответствующий код во всех функциях (да, примеры в статье получились немного искусственными). Можно, конечно, вынести все, что связано с форматированием, в отдельную функцию formatMessage () и пропустить через нее поздравление во всех функциях. Но давайте для начала попробуем отразить предметную область с помощью ООП и посмотреть, чем нам может помочь JavaScript в этой ситуации.
Объектно-ориентированный подход
Как всем вам хорошо известно в JS можно писать в объектно-ориентированном стиле с наследованием на базе прототипов:
function Greeter() {}
Greeter.prototype.doGreeting = function(msg) {
console.log(msg);
}
function NewYearGreeter(currentYear) {
this.currentYear = currentYear;
}
NewYearGreeter.prototype = Object.create(Greeter.prototype);
NewYearGreeter.prototype.constructor = NewYearGreeter;
NewYearGreeter.prototype.doGreeting = function() {
var year = this.currentYear + 1;
var newYearMsg = 'Happy New ' + year + ' Year!';
Greeter.prototype.doGreeting.call(this, newYearMsg);
}
var newYearGreeter = new NewYearGreeter(2015);
newYearGreeter.doGreeting();
Получилось довольно многословное решение, плюс которого в том, что этот код будет работать во всех современных runtime-средах (браузеры, Node.js). Именно из-за многословности реализации объектно-ориентированного подхода в ES6 появились классы, которые знакомы любому программисту, владеющему C++, Java, C#, Python etc. Вот таким образом будет выглядеть пример выше, если использовать классы ES6:
'use strict';
class Greeter {
doGreeting(msg) {
console.log(msg);
}
}
class NewYearGreeter extends Greeter {
constructor(currentYear) {
super();
this.currentYear = currentYear;
}
doGreeting() {
let year = this.currentYear + 1;
let newYearMsg = 'Happy New ' + year + ' Year!';
super.doGreeting(newYearMsg);
}
}
let newYearGreeter = new NewYearGreeter(2015);
newYearGreeter.doGreeting();
Выглядит уже симпатичнее. Но как вы наверное знаете было много споров вокруг классов в JS. Были ярые противники этого новшества. В целом их позиция была ясна — ООП в перспективе порождает проблему «гориллы и банана»:
Проблема объектно-ориентированных языков в том, что они тащат с собой всё своё неявное окружение. Вам нужен был банан –, а вы получаете гориллу с бананом, и целые джунгли впридачу.
Джо Армстронг «Coders at Work»
Поэтому языковые средства, которые делали возможным создание сложной таксономии классов простым способом, воспринималось противоборствующей стороной в штыки. Как бы то ни было классы сейчас это стандарт языка, пользуйтесь, но без фанатизма.
Хорошо, если наследование порождает проблему связности, какой есть альтернативный вариант? Композиция.
Предпочитайте объектную композицию наследованию классов
Гамма, Хелм, Джонсон, Влиссидс «Приёмы объектно-ориентированного проектирования. Паттерны проектирования.»
В JS, композицию можно реализовать разными способами. Вот одно из решений, которое будет работать во всех современных runtime-средах:
function extend(destination, source) {
for (var key in source) {
if (source.hasOwnProperty(key)) {
destination[key] = source[key];
}
}
}
function Greeter() {
this.doGreeting = function(msg) {
console.log(msg);
}
}
function NewYearGreeter(year) {
this.year = year;
this.doNewYearGreeting = function() {
var newYearMsg = 'Happy New ' + this.year + ' Year!';
this.doGreeting(newYearMsg);
}
}
var greeter = new Greeter;
var newYearGreeter = new NewYearGreeter(2016);
extend(newYearGreeter, greeter);
newYearGreeter.doNewYearGreeting();
В этом примере с помощью конструкторов создаются объекты, свойства которых (методы) компонуются в одной сущности (объект newYearGreeter) с помощью служебной функции extend. Современный стандарт позволяет упростить реализацию композиции с помощью Object.assign ():
'use strict';
let greeter = {
doGreeting(msg) {
console.log(msg);
}
};
let newYearGreeter = {
setYear(year) {
this.year = year;
},
doNewYearGreeting() {
let newYearMsg = 'Happy New ' + this.year + ' Year!';
this.doGreeting(newYearMsg);
}
};
Object.assign(newYearGreeter, greeter);
newYearGreeter.setYear(2016);
newYearGreeter.doNewYearGreeting();
Object.assign () по сути делает тоже самое, что и extend, за тем исключением, что его можно использовать «из коробки» при этом компоновать можно любое количество объектов.
Это объектная сторона вопроса. Но JS также предоставляет средства для программирования в функциональном стиле.
Функциональный стиль
Особенность такого подхода заключается в том, что мы больше не оперируем объектами, а оперируем чистыми функциями, композиция которых позволяет решить поставленную задачу. Наш пример слишком маленький, поэтому для примера возьмем другое понятие из функционального программирования — каррирование:
function createGreeter(msg) {
return function(param) {
return msg.replace(/%.*%/, param);
}
}
var greetWithNewYear = createGreeter('Happy New %year% Year!');
console.log(greetWithNewYear(2016));
В ES6 из средств для упрощения программирования в функциональном стиле, самое заметное нововведение — стрелочные функции. Стрелочная функция — это анонимная функция (например, функция сразу после return из примера выше) или лямбда-выражение, как говорит народ из функционального лагеря. Вот таким образом преобразуется наш пример, если мы будем использовать «толстую стрелку»:
function createGreeter(msg) {
return param => msg.replace(/%.*%/, param);
}
var greetWithNewYear = createGreeter('Happy New %year% Year!');
console.log(greetWithNewYear(2016));
Программирование в функциональном стиле дает много преимуществ: код получается компактнее без побочных эффектов, но к нему, конечно, необходимо привыкать, так как при функциональном программировании рассуждения с императивного уровня (как сделать) переходят на декларативный уровень (что сделать).
Что почитать / посмотреть?
Блог Акселя Раушмайера:
Серия статей ES6 in Depth на mozilla.org:
Youtube-канал Маттиаса Йохонсона:
Итоги
JavaScript развивается очень быстро — будущий стандарт готовит другие замечательные нововведения в языке, но то, что мы можем использовать для создания web-приложений уже сегодня по большому счету новая инкарнация языка, цель которой упростить жизнь всем разработчикам.
(_ => ++_)(2015);