Pull to refresh

Comments 37

Когда я вижу такой код
return (~ memo.indexOf(item) ? null : memo.push(item)), memo;
— хочется обратиться ко всем начинающим (или «продолжающим») JS разработчикам — не пишите так никогда. Совсем никогда. Вы можете подумать, что это какой-то крутой, «джедайский» код — который позволяет очень лаконично выразить вашу бизнес-логику, используя доступные только «гуру» «экзотические» операторы и конструкции.
Это абсолютно не так.

Сравните
return (~ memo.indexOf(item) ? null : memo.push(item)), memo;
и
if(memo.indexOf(item) < 0) memo.push(item); return memo;


Второй вариант на порядок читабельнее, не использует ничего кроме банального if, и (вот где профит-то :-)) — короче на 6 символов.

Он же в виде целой функции, чтобы можно было сразу проверить работоспособность (перенос строки перед return — по желанию)

function uniq$viaReduce (array)
{
    return array.reduce(function (memo, item)
    {
        if(memo.indexOf(item) < 0) memo.push(item); 
        return memo;
    }, []);
};
только логичнее делать сравнение не <0, а ===-1
Ну, на вкус и цвет…
На мой субъективный взгляд "-1" выглядит чуть более «магическим» числом чем «0», для тех кто не знает, что в точности должен возвратить indexOf в случае отсутствия элемента в массиве.
Только в данном случае ===-1 не надо, достаточно == -1
Ведь метод indexOf нам заведомо (согласно спецификации) не может вернуть ничего кроме числа, поэтому нет нужды в дополнительной проверке типа.
Я, согласно здравому смыслу, предпочитаю не думать, достаточно == или нет. В тех (крайне редких) случаях, когда действительно нужен ==, ставлю. Имело бы смысл выбирать, если бы === был дороже, чем ==, так нет же.
Да, соглашусь, тут вопрос того, какой вариант мозг на автомате воспринимает как канонический :)
=== не делает проверку типа. == делает приведение. Разница есть.
Неточно немного. === делает, конечно же, проверку типа. Но и == тоже, и если что — приводит.
Ну в том и смысл, что до приведения типов дело не доходит, когда мы сравниваем заведомо два числовых значения.
Предлагаю не обсуждать дальше разницу в поведении === и == в общем случае — все это давно обсуждено и всем известно. Я специально указал в первоначальном своем комментарии, что он относится только к конкретному фрагменту кода и ни к чему больше.
Рад, что вам понравился этот пример. Вообще, ни он, ни ваш код не нужны, т.к. можно просто использовать библиотечный аналог (о чём я упомянул в статье). Пример чисто синтетический и он реализует не «бизнес-логику», а библиотечную функцию, спека которой заморожена, и, следовательно, это код не подвержен изменениям. Поэтому я позволил себе использовать джедайский подход.

По вашему варианту (если предположить, что это всё таки БЛ) можно сказать, что он более читаемый, но я бы ещё обрамил ветви в фигурные скобки, потому что они имеют свойство расширяться, и тогда скобки уже необходимы. А также, я бы разнёс на разные строчки условие и ветвь then. Это сделает дифф чище, если условие или одна из ветвей будет меняться по отдельности. Ну, и как уже заметили, лучше использовать сравнение с -1, потому что это флаговое значение.

А вообще, мне интересно померить скорость самого подхода с reduce. Возможно он не так быстр, как простой плоский цикл, и стоило бы придраться именно к этому :)
Естественно reduce не так быстр, как плоский цикл, не говоря уже о том, что конкретно для uniq$viaReduce в момент вызова indexOf будет перебираться весь массив в поисках нужного элемента, что раздувает количество шагов цикла в квадрат раз.

Однако, все это может быть совершенно не заметно на массивах размером до, например, 1000 элементов (ну или 10 000, я не знаю). Т.е. разница в несколько десятков миллисекунд, либо меньше.

Очень странно слышать комментарии про фигурные скобки и разнесение ветвей на разные строчки в контексте обсуждения
return (~ memo.indexOf(item) ? null : memo.push(item)), memo;

Т.е. понятно, что это чисто синтетический пример, но я и про свой код могу то же самое сказать.

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

И комментарий я написал именно ради того, чтобы этот ваш термин не вводил никого в заблуждение.
А, теперь я вас понял. Вы указываете на то, что слово «джедайский» несёт позитивный оттенок, и это проецируется на код. Вероятно, стоит добавить в статью после примера более конкретный дисклеймер. Спасибо.
По теме стоит отметить, что indexOf — вообще плохое название для действия «проверить, содержит ли массив данный элемент». Не говоря уже обо всей этой чернухе с -1. В js явно не хватает метода с номальным именем, возвращающего булево значение.
Я так понимаю, вы хотите метод навроде exists(item) → Boolean? Это было бы и вправду круто, но в защиту indexOf скажу, что у него есть одно достоинство. Зачастую, если элемент найден мы должны с ним что-то сделать далее, а для этого нужен его индекс. Т.е. возврат indexOf сначала используется для проверки «есть-нету», и если «есть», то далее с этим что-то сделать (splice, например, или получение по индексу).
Я не то что бы «за» этот способ, но чего это вдруг бинарные операции стали «экзотическими», как и принцип работы с запятой?
Приравняйте «экзотические» к «редко используемые», и ответ найдется сам собой. Усредненный «JS программист в вакууме» за 3, например, года своей карьеры с такими конструкциями может сталкиваться пару раз, даже с учетом кода сторонних плагинов и библиотек. Не надо только говорить, что такому в программировании не место, что надо развивать кругозор и т.д. — по состоянию рынка труда на текущий день у нас таких спецов все равно большинство, и это надо всегда учитывать при написании кода.
Если оператор «запятая» крайне вредна, а тернарный оператор вообще не к месту, то унарное «не» в конструкции ~array.indexOf(foo) давно устоялось и смотрится явно красивее сравнения с магическими цифрами 0 и -1. Если перед indexOf унарное «не» — явно проверка вхождения, сравнение с чем-то — еще нужно подумать. Улучшать читаемость кода для говнокодеров в ущерб себе я, например, не намерен.
А я скажу, что такого программиста нужно учить, в том числе и такими примерами, чтобы его не пугали в коде конструкции вроде этой if (ret ^ not) { .. }. А не то в итоге мы получаем людей, которые не знают базовых понятий, регеспов, зачем parseInt второй параметр, не понимающих 5.67e8 или .911, прототипы и т.п.
К rock и RubaXa у меня вопрос — вы веб программистов нанимали когда-нибудь на работу? собеседование проводили? искали подрядчиков на аутсорсе? какой процент из соискателей (или потенциальных исполнителей) готов сходу разобраться в ~indexOf или разглядеть XOR оператор в (ret ^ not)? и за какие деньги они готовы потом работать?

а то у меня ощущение что мы с разных точек зрения на это смотрим
Да, и порой складывается крайне печальное впечатление, базы нет, не говоря уже о знание самого языка. И в этом есть «наша» вина. Просто порой мне кажется, что javascript программист, это какой то недалекий человек, с отсутствием долговременной памяти. В какой-то момент этот человек, вместо битовых масок начинает писать код повышенной бредовости, якобы читаемый. А ведь битовые операции, как и всю булевскую логику давно в школе преподают.

Ведь проблема не только с битовыми операциями, замакания, функция-конструктор, область видимости и т.п. для многих магия, это печально.
Абсолютно не соглашусь с вами, во всем, за исключением действительно странного примера с тернарным оператором, тернарность которого тут и правда не нужна. Я убежден, что крокфорданизация JavaScript преследует немного иную цель, нежели писать все согласно заветам, и эта цель — единообразие.

Если команда решает, что нужно по возможности использовать тернарки, то нужно следовать этому. Если команда решает, что нужно использовать лексему `~[].indexOf()` для проверки вхождения — нужно этому следовать. Если в команде принято иначе — нужно придерживаться общих правил.

В JavaScript есть некоторые «неудобные конструкции» — hoisting, автоподстановка точки-с-запятой, которая не срабатывает на строках оканчивающихся или начинающихся на скобочки и операторы, которые могут быть как унарными, так и бинарными. Но все это не значит, что этими штуками грушновато пользоваться. Я считаю это глупым. Посмотрите, сколько команд не используют точку с запятой, посмотрите сколько дейстивительно великолепных JS-разработчиков пишут вызов функций через тернарные операторы. TJ Holowaychuk, Isaac Schlueter, да у каждого состоявшегося разработчика есть предпочтения, и это не просто нормально. Это отлично!

Совершенно не имеет значения, какие лексемы и практики языка использовать, а какие нет, главное, делать это единообразно в рамках всей команды. Если вы не знакомы с побитовым оператором `~` — это значит, что нужно разобраться с ним. Если вы знакомы — в чем проблема запомнить лексему? Научиться практикам команды для начинающего разработчика практически тождественно тому, чтобы запомнить стайлгайд, возможность разобраться с острыми углами стайлгайдов — это комплимент, который вам сделала команда.

Для примера сравните такие конструкции:

function (rights) {
  return ~[rights].indexOf('write')
    ? fs.writeFile(...)
    : next(throw new Error());
}


function (rights) {
  if ([rights].indexOf('write') !== -1) {
    return fs.writeFile(...)
  } else {
    return next(throw new Error());
  }
}


function (rights) {
  if ([rights].indexOf('write') !== -1) {
    return fs.writeFile(...)
  }
    return next(throw new Error());
}


По-моему, нет никаких причин, не писать так, как в первом примере, если а) У вас нет желания научиться в тернарные и битовые операторы б) У вас нет договоренности с командой.

В любом случае, указывать другим как писать код — это весело, но нужно иметь свою голову на плечах и понимать причины тех или иных гайдов. А читаемость кода — я понимаю исключительно как единообразный стиль, остальное — комформизм.
ОК, давайте по порядку:
1. Насчет договоренностей в команде — вопросов нет, можно договориться хоть в LISP-стиле писать, если это нужно для решения задачи. Только помимо членов вашей команды есть еще масса людей, которые могут ваш код читать потом — если результат вашей работы передается дальше как часть большого проекта, если вы пишете общую либу, если вы используете ваш код в качестве примеров в документации, докладах или вот в статье на хабре. Ну или вот вы написали все прекрасно, сдали заказчику, потом решили чем-то другим заняться, а через 3 года ему другая уже команда должна доработать это все. Возможно за другие деньги совсем. Я понимаю что как программисту вам на это пофиг, но это не значит, что так надо делать. Есть еще такое слово как ответственность.

2. Не надо, пожалуйста, валить в кучу тернарные операторы, побитовые операторы, а также (это к комментарию от RubaXa выше) замыкания, области видимости и т.д. У вас какой-то бинарный программист получается — он либо знает только if-else и все (видимо только вчера basic изучил в школе), либо знает и активно использует все операторы и конструкции языка, которые в нем только есть. Это крайности.
Если посмотреть объективно, то можно выстроить некоторую шкалу встречаемости различных конструкций в коде (в любом JS коде, усредненно), где var-if-else-for будут на первых местах где-то, замыкания и тернарники на 5-10, а побитовые операторы и возврат значения после запятой (return expression, bar) — на 100 по частоте использования.

Опять же, если вы действительно пишите какой-то математический алгоритм или активно работаете с числами, можно и нужно использовать побитовые операторы. Они для этого и сделаны в таких языках как JS. Но без реальной нужды их лучше не вставлять в код, если вы не пишете just for fun и только для себя.
Поверьте, даже в мыслях не было валить всё в кучу, просто такова картина после собеседования. Конечно, если вам нужен человек, который будет натягивать jQuery-плагины, то ему всего этого не надо, но если вы делаете что-то серьезное, нужно знать базу и тонкости языка.
1. С первым аргументом не соглашусь. Это скорее вопрос запоминания лексических конструкций языка. Я считаю абсолютно нормальным, если я берусь дорабатывать чей-то код и нахожу там какие-то непривычные мне лексемы. Стиль написания кода у всех разный, и я с этим лучше смирюсь, чем решу, что теперь все нужно заново переписать. Да и вообще боттл-нек не тут, если речь о ресурсах на поддержку.

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

Кроме того, мне кажется, что вы преувеличиваете идеальный код. Это не более чем элементарные вещи: DRY, KISS, настроенный линтинг, стайлгайды, тесты, лаконичные методы и тд. Возможно, это еще и модульность: блек-бокс — это потрясающе — передаешь что-то на вход, получаешь что-то на выходе. Подробности реализации не имеют значения, так как они должны стабильно решать задачи, не более того. Да и в коммерческих проектах идеальный код не всегда возможен — есть задачи, которые нужно было «вчера и на оленях», тут блек-боксы очень помогают.

Даже код в примере (хотя он и в правду наркоманский, и запятая — это очень не ок, а используй concat([item]) вместо push(item) она и не нужна будет) решает свою задачу — показать пример реализации метода, не вдаваясь в подробности.

Стремиться быть всем понятным — это утопия, и лично я против того, чтобы следовать ей. Но стремиться ыть понятным своей команде — это очень важно, и этому следовать нужно.
Не смотря на некоторую магию в коде спасибо за внятное разъяснение. И особое спасибо за интересные факты в конце статьи.
да, занятно. хотя иногда кажется: ну что там еще можно сказать про js. а оказывается есть что.
Иногда натыкаешься на очень странные вещи, странные подходы.
Например, пару месяцев назад мне на глаза попалась вот такая либа: github.com/tarruda/s-pipe. Это JS, это потоки (stream) и это закос под LISP (цитирую: «lisp-inspired chaining syntax»).
Как-то наткнулся на декораторы функций, основанные на действии valueOf. Сразу понимаешь, что не всё ещё сказано и использовано, и в JS ещё вагон разных интересных штук.
Сейчас вспомнил школу, когда учитель литературы допытывал вопросами типа «Что автор стихотворения хотел сказать этими словами?»…
Мне кажется, Брендан, закладывая в Javascript именно такие принципы, даже и не представлял, что спустя годы программисты будут строить вокруг языка столько интересных вещей…
Вот только не функция, а метод.
На мой взгляд, идеальный вариант разобраться в методах — это заглянуть в исходник (или в код полифила, как в данном случае). А ещё лучше предварительно [попытаться] реализовать их самому, а потом уже посмотреть, что забыли, что не учли а чего не знали об этом методе. Тогда всё очень хорошо и на долго запоминается. Прям в мышечной памяти:)

А статья неплохая, спасибо.
UFO just landed and posted this here
Добавлю от себе еще один туториал, который дает возможность понять не только функциональное программирование но и реактивное. Ребята из организации Rx создали вот такой туториал reactive-extensions.github.io/learnrx/
Да, я помню, как Гвидо это говорил, но забыл поискать ссылку. Я хотел добавить это в статью, в пункт про Python. Тем более есть на хабре, спасибо.
Тем самым он еще больше внес путаницы в язык.
Это авторитетное, но еще одно личное мнение :)
Хорошая функция reduce, я её в парсинге после первичного разбора строки использую…
Sign up to leave a comment.

Articles