Явные возможности JavaScript

в 7:52, , рубрики: best practices, good parts, javascript, production, ненормальное программирование, Промышленное программирование

Image

Начитывая очередную статью про малоизвестные фичи языка JavaScript и втихую пописывая какие-то невменяемые решения в консоли браузера, я часто проговариваю в голове мол ну на проде то конечно все не так!? Ведь язык давно обзавелся огромнейшим комьюнити и имеет удивительно широкий охват промышленной разработки. Раз так, то почему же мы часто забываем про его возможность быть понятным для каждого и буквально пропагандируем все эти специфичные и "запоминаемые" конструкции? Just make it Obvious!

Рассуждения на тему

Эту графоманию можно пропустить.

Если говорить о промышленной разработке, то в подавляющем большинстве случаев требование к коду быть поддерживаемым даже важнее, чем решать поставленную бизнесом задачу. Для многих это очевидно, для некоторых — отчасти (встречаются конечно и редкие Д'Артаньяны). Чем понятнее наш код, тем меньше рисков для него — попасть на пыльную полку, а для нас и наших преемников — заработать проблем с нервной системой.

Не секрет, что JavaScript удивителен своей гибкостью, что является как его величайшим достоинством, так и досадным проклятием. Путь JavaScript-разработчика долог и крайне интересен: мы поглощаем книжку за книжкой, статью за статьей и набираемся уникального опыта, но местами — действительно language-специфичного. Широчайшее распространение языка и в то же время богатое число накопившихся и подкармливаемых неочевидностей способствуют образованию двух фронтов: тех, кто едва ли не боготворит этот язык, и тех, кто смотрит на него как на неуклюжую и качающую права утку.

И все бы ничего, но часто представители обоих фронтов работают на одном проекте. И обычной, всеми принятой практикой является непонимание (нежелание понимать и даже игнорирование) кода друг друга. И в самим деле, "я же на Java-разработчика устраивался, а не это ваше!". Масла в огонь подливают и сами JavaScript-последователи мол "никто на самом деле не знает JavaScript!" да "я могу это в одну строчку написать на js!". Каюсь, что и сам злоупотребляю на досуге ненормальным программированием...

Эту проблему начинаешь чувствовать, когда занимаешь место маргинала и приобретаешь некоторый опыт работы с людьми и их кодом по обе стороны от баррикад. Планинг и прочие митинги проходят продуктивнее, когда все разработчики понимают друг друга не только на уровне бизнес-сторей, но и хоть немного на уровне их имплементации. Пресловутый бас-фактор меньше сказывается на проекте, когда в случае болезни единственного фронт-ендера остальные члены команды не брезгуют поправить какую-то строчку .js файла. Процесс обмена знаниями в команде и за ее пределы становится прозрачнее для всех, когда каждый имеет более детальную картину. Ну и все в том же духе.

Я никого не призываю "фулстекнуться" или "T-шейпнуться" (как сейчас правильно говорить?), но почему бы нам немного не приподнять этот занавес хотя бы со стороны JavaScript-сообщества? Для этого достаточно лишь привнести немного явности в наш код, используя гибкость языка не чтобы выпендриться, а чтобы нас понимали.

Взросление и принятие ответственности

Со своей стороны JavaScript уже давно осознал свою роль не как язык для интерактивности интернет-страничек и "склеивания" их ресурсов, но как мощнейший и достаточный инструмент создания полноценных кросс-платформенных и часто очень даже масштабируемых приложений.

Изначально разработанный для веб-дизайнеров этот "самый неправильно понятый язык программирования" долгое время топтался на месте, несмотря на стремительно растущую популярность и значимость. За 13-14 лет, предшествующие редакции ECMAScript 5.1, трудно вспомнить какие-то важные изменения в стандарте или понять вектор его развития. В то время огромный вклад в формирование экосистемы языка вносило его комьюнити: Prototype, jQuery, MooTools и проч. Получив эту обратную связь от разработчиков, JavaScript проделал значительную работу над ошибками: громкий 6-летний релиз ES6 в 2015 году и теперь уже ежегодные релизы ECMAScript, благодаря переработанному комитетом TC39 процессу внесения новых возможностей в спецификацию.

Что ж, когда наши приложения стали достаточно большими, прототипная модель ООП для описания пользовательских типов перестала себя оправдывать из-за непривычного подхода. Ну серьезно, что это?

function Animal() {
/* Call me via new and I will be the constructor ;) */
}
function Rabbit() {}

Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.constructor = Rabbit;

В языке не появились классы, но появился их синтаксис. И код стал доступен для приверженцев традиционной класс-ориентированной парадигмы:

class Animal {
    constructor() {
    /* Obviously, the constructor is here! */
    }
}
class Rabbit extends Animal {}

Сейчас на стадии кандидата в релиз находятся приватные поля класса. С трудом верится, что рано или поздно мы перестанем смешить друг друга соглашением об именовании приватных свойств через нижнее подчеркивание.

В то же время, в языке, где функция является объектом первого порядка и имеет место постоянная событийность, совершенно обычное дело:

let that = this;
setTimeout(function() {
    that.n += 1;
}, 1000);

И тут начинаются объяснения о контекстах this и замыкании в JavaScript, что отпугивает каждого второго внешнего разработчика. Но во многих случаях, язык позволяет избежать лишних удивлений, явно используя Function.prototype.bind или вовсе так:

setTimeout(() => this.n += 1, 1000);

У нас тоже появились стрелочные функции, и это действительно — функции, а не функциональные интерфейсы (да, Java?). Вместе с расширенным набором методов работы с массивом они также помогают писать привычный декларативный пайплайн вычислений:

[-1, 2, -3, 4]
  .filter(x => x > 0)
  .map(x => Math.pow(2, x))
  .reduce((s, x) => s + x, 0);

Язык по праву считает себя мультипарадигменным. Но вот простой пример про сигнатуру некоторой функции:

function ping(host, count) {
    count = count || 5;
    /* send ping to host count times */
}

Сначала проходящий мимо задастся вопросом мол вероятно функция может принимать только первый аргумент, а потом мол какого черта в этом случае count становится булевом!? И действительно, функция имеет два варианта использования: с указанием count и без. Но это совершенно неочевидно: приходится смотреть в реализацию и понимать. Разобраться может помочь использование JSDoc, но это не общепринятая практика. И здесь JavaScript пошел навстречу, добавив поддержку не перегрузки, но хотя бы дефолтных параметров:

function ping(host, count = 5) { /* ... */ }

Резюмируя, JavaScript обзавелся огромным числом привычных вещей: генераторы, итераторы, коллекции Set и словари Map, типизированные массивы, да даже регулярные выражения начали радовать поддержкой lookbehind! Язык делает все, чтобы быть пригодным для многих вещей и стать дружелюбным для всех.

Благоприятный путь к очевидному

Сам язык — безусловно молодец, и с этим трудно спорить! Но что не так с нами? Почему мы постоянно напоминаем всему миру, что JavaScript все таки какой-то не такой? Давайте посмотрим на примеры некоторых широко используемых приемов и зададимся вопросом их целесообразности.

Приведение типов

Да, JavaScript обладает динамической и слабой системой типов и позволяет проводить операции над чем угодно, неявно выполняя за нас преобразования. Но часто явное приведение типов нам все таки необходимо и можно наблюдать следующее:

let bool = !!(expr);
let numb = +(expr);
let str = ''+(expr);

Эти трюки известны каждому JavaScript-разработчику и мотивируются они тем, что мол так можно "быстро" превратить что-то во что-то: под быстротой здесь понимается короткая запись. Может еще и false записывать сразу как !1? Если разработчик так сильно переживает за печатаемые символы, то в его любимой IDE можно без труда настроить необходимый live template или автокомплит. А если — за размер публикуемого кода, то мы всегда прогоняем его через обфускатор, который знает получше нашего как все это обесчеловечить. Почему не так:

let bool = Boolean(expr);
let numb = Number(expr);
let str = String(expr);

Результат — такой же, только понятен всем.

Для строковых преобразований у нас есть toString, но для численных есть интересный valueOf, который может быть тоже переопределен. Классический пример, который вводит в ступор "непосвященных":

let timestamp = +new Date;

Но ведь есть у Date известный метод getTime, давайте использовать его:

let timestamp = (new Date()).getTime();

Абсолютно незачем эксплуатировать неявное приведение типов.

Логические операторы

Отдельного внимания достойны логические операторы И (&&) и ИЛИ (||), которые в JavaScript не совсем логические: принимают и возвращают значения любого типа. Вдаваться в детали работы вычислителя логического выражения не будем, рассмотрим примеры. Ранее представленный вариант с функцией:

function ping(host, count) {
    count = count || 5;
    /* ... */
}

Вполне может выглядеть следующим образом:

function ping(host, count) {
    // OR arguments.length?
    if (typeof count == 'undefined') {
        count = 5;
    }
    /* ... */
}

Такая проверка и привычней, и в некоторых случаях может помочь избежать ошибки.

Это скорее кажется дикостью для разработчика, который изначально выбрал путь JavaScript. Но для большинства других — вот этот код действительно дикий:

var root = (typeof self == 'object' && self.self === self && self) ||
    (typeof global == 'object' && global.global === global && global);

Да, это компактно, и да, это могут позволить себе популярные библиотеки. Но, пожалуйста, давайте не злоупотреблять этим, так как наш код будут читать не контрибьюторы в JavaScript, а разработчики, решающие бизнес-задачи за выделенные сроки.

Может встретиться и вовсе такой паттерн:

let count = typeof opts == 'object' && opts.count || 5;

Это определенно короче обычного тернарного оператора, но при чтении такого кода первым делом вспоминаешь приоритеты используемых операций.

Если же мы пишем функцию-предикат, которую передаем в тот же Array.prototype.filter, то обернуть возвращаемое значение в Boolean — это хороший тон. Сразу становится очевидно назначение этой функции и не возникает диссонанса у разработчиков, языки которых имеют "правильные" логические операторы.

Побитовые операции

Распространенный пример проверки наличия элемента в массиве или подстроки в строке с помощью побитового НЕ (NOT), который предлагается даже некоторыми учебниками:

if (~[1, 2, 3].indexOf(1)) {
    console.log('yes');
}

Какую проблему это решает? нам не приходится осуществлять проверку !== -1, так как indexOf получит индекс элемента или -1, а тильда прибавит 1 и поменяет знак. Тем самым выражение будет оборачиваться "ложью" в случае индекса -1.

Но избежать дублирования кода можно и по-другому: вынести проверку в отдельную функцию какого-нибудь utils-объекта, как это делают все, чем использовать побитовые операции не по назначению. В lodash для этого есть функция includes, и работает она не через жопу тильду. Можно возрадоваться, так как в ECMAScript 2016 закрепился метод Array.prototype.includes (у строк тоже есть).

Но не тут-то было! Еще тильду (наравне с XOR) используют для округления числа, отбрасывая десятичную часть:

console.log(~~3.14); // 3
console.log(2.72^0); // 2

Но ведь есть parseInt или Math.floor для этих целей. Побитовые операции здесь удобны для быстрого набора кода в консоли, так как они к тому же имеют низкий приоритет перед остальной арифметикой. Но на код-ревью такое лучше не пропускать.

Синтаксис и конструкции языка

Некоторые странные практики трудно отнести к какому-то конкретному разделу. Например, говорят, что скобки при вызове конструктора необязательны и следующие два выражения идентичны:

let rabbit = new Rabbit();

let rabbit = new Rabbit;

И это действительно так! но зачем создавать вопрос на пустом месте? Не каждый язык может похвастать такой "особенностью". А если все таки хочется, то пусть это будет соглашением по всему проекту. Иначе возникает ложное чувство, что есть какая-то разница.

Похожая ситуация с объявлением набора переменных. Синтаксис директив var и let позволяет объявить (и определить) сразу несколько переменных, перечисленных через запятую:

let count = 5, host, retry = true;

Кто-то использует переводы строк для читаемости, но в любом случае такой синтаксис — не частое явление в популярных языках. Никто не даст по рукам и не спросит, если написать так:

let count = 5;
let retry = true;
let host;

Опять таки, если есть соглашение о хорошем стиле на уровне проекта/компании, то вопросов нет. Просто не надо чересчур комбинировать варианты синтаксиса по настроению.

Есть в языке и вовсе специфичные конструкции, как например IIFE — позволяет вызвать функцию сразу по месту ее определения. Весь трюк в том, чтобы парсер распознал функциональное выражение, а не деклорацию функции. И это можно сделать уймой разных способов: классически обернув скобками, через void или любой другой унарный оператор. И в этом нет ничего замечательного! Необходимо выбрать единственный вариант и не отходить от него без необходимости:

(function() {
/* ... */
}());

Не надо использовать операторы, чтобы хакнуть парсер. Когда на проект приходит новичок, хочется погрузить его в бизнес-логику приложения, а не кормить объяснениями откуда были подсмотрены все эти восклицательные знаки и войды. Есть еще вторая классическая запись через скобки и интересный комментарий от Крокфорда по этому поводу.

Появление синтаксиса классов в ES6 не было сопровождено привычными модификаторами доступа. А иногда разработчику хочется и на классах пописать, и приватность соблюсти. Что приводит к такому коду Франкенштейна:

class Person {
    constructor(name) {
        let _name = name;
        this.getName = function() { return _name; }
    }
    toString() {
        return `Hello, ${this.getName()}`;
    }
}

То есть в конструкторе для экземпляра создаются аксессоры, а приватность достигается их доступом к локальным переменным-свойствам через замыкание. Этот пример выглядит вполне даже лакончино, но это совершенно немасштабируемый подход, если вокруг него не построить документированное решение-фреймворк. Господа, давайте использовать либо имеющиеся классы (и ждать стандартизации приватных полей), либо популярный паттерн-модуль. Создавать какое-то промежуточное микс-решение здесь — такое себе, так как классы перестают быть классами, а код — вразумительным.

Подытоживая, здравой мыслью будет поделиться принятым в проекте стайл-гайдом, конфигом для линтера или просто фрагментами кода с коллегами, которые вносят в проект его не-JavaScript составляющую. Язык предлагает несколько вариантов буквально для каждой типовой задачи, поэтому улучшить понимание друг друга и попасть под общий знаменатель не составит труда (ну или почти).

Злоключение

Тема эта конечно холиварная и примеров можно привести гораздо больше, но основной посыл статьи о том, что не следует злоупотреблять неочевидностями в JavaScript там, где этого можно избежать. Природа языка — уникальна: позволяет писать как элегантные и выразительные (в меру "упоротые") решения, так и понятные и доступные для всех. Я в корне не согласен с расхожим мнением, что JavaScript "сам себя наказал" или "похоронен под грудой добрых намерений и ошибок". Потому что сейчас большую часть странностей демонстрирует не язык, а образовавшаяся вокруг него культура разработчиков и (не)равнодушных.

Автор: cerberus_ab

Источник

* - обязательные к заполнению поля