Pull to refresh

Comments 63

Полностью согласен с вами. На learn.javascript вообще очень доходчиво все расписано. Я часто джунов туда отправляю для просветления.
Вот знаете, эти стрелочные функции для меня всё ещё являются более «невидимыми», чем замыкания. Всё никак не могу привыкнуть к их синтаксису, особенно когда отсутствуют фигурные скобки.

Обычная нотация "вход => выход", что в этом "невидимого"?


А вот замыкания — это синтаксический сахар, неявно расширяющий "вход" т.е. передающий значения/ссылки через "чёрный ход", без их явного перечисления в списке входных параметров.

Потому что никак не могу привыкнуть к тому, что всё ещё считаю, что если это функция, то она должна иметь тело, обрамлённое фигурными скобками, и, если она что-то возвращает, должно использоваться ключевое слово return. А тут взяли и всё выкинули. Ломает мозг.

Просто нужно воспринимать это не как процедуру, возращающую значение, а как математическую функцию, которая отображает входы на выходы, например f(x,y) => sin(x)*cos(y)

Вас не обязывают писать сокращения. Вы должны понимать их, но если вас как и меня они иногда вгоняют в ступор, перепишите на полный вариант со скобками и возвратом. Профит.

let food = 'cheese';
function eat() {
  console.log(food + ' is good');
}
eat();


function liveADay() {
  let food = 'cheese';
  function eat() {
    console.log(food + ' is good');
  }
  eat();
}
liveADay();

Прочтите оба предыдущих примера кода и убедитесь в том, что они эквивалентны.

Куски кода не являются эквивалентными. В первом случае создается глобальная переменная food. Когда во втором коде эта переменная пропадает после окончания вызова liveADay.
Здесь можно поспорить, ведь в обоих случаях выполнение скрипта завершится после вызова eat(), при условии что другого JavaScript кода нет, что сделает глобальность переменной food нерелевантной.
Я понимаю к чему вы клоните. Но если запустить первый скрипт в консоле, то после выполнения кода переменная останется доступной.
Так как функция eat находится внутри функции liveADay, eat «видит» все переменные, объявленные в liveADay. Именно поэтому функция eat может прочитать значение переменной food. Это и называется замыканием.

фигня. вложенные функции даже в Pascal есть, а толку. замыкание — это когда переменные и аргументы функции остаются доступными после её завершения.

Пичалька… Когда меня учили писать хоть как то связанный код, про «область видимости» твердили сто раз, да ещё и рассказывали, что при страничной организации памяти лазить в соседнюю страничку за переменной, не самая здравая идея. А уж использование переменных объявленных внутри функции вне этой функции прям совсем моветон. Но статьи про это не писали. Да и «предположим вам некую часть кода надо выполнить два раза подряд, поэтому можно скопипастить» вызывает судороги. :-(
при страничной организации памяти лазить в соседнюю страничку за переменной, не самая здравая идея

Хм, а почему? И как вообще понять, в какой странице лежит переменная?


А уж использование переменных объявленных внутри функции вне этой функции прям совсем моветон.

Для современных языков программирования это не "моветон", а просто невозможное действие.

В те времена обращение в соседнюю страницу памяти требовало перегрузки регистра страниц, а это лишние такты. Один раз «слазить» ещё туда сюда, а если нон стопом в цикле, можно было потерять и 200% производительности. Понять в какой странице лежит нужная тебе переменная в общем случае нельзя, но точно известно, что все, что объявлено выше, может оказать в другой странице. Не надо проверять — либо явно передай в функцию, либо, если уж совсем невмоготу, перенеси в локальную переменную. Ну а изменение чего либо «выше себя» при многопоточной обработке вообще верный способ потратить сотни часов в попытке понять почему в один поток работает норм, а в несколько разваливается. По использованию переменных я имел ввиду одинаковые имена, конечно же. Это вопрос к зоне видимости. Почти во всех языках локальные переменные не видны снаружи, но дальше вопрос к компилятору. Он либо обеспечивает «правила игры» с точки зрения видимости переменных, либо нет.
Да и «предположим вам некую часть кода надо выполнить два раза подряд, поэтому можно скопипастить» вызывает судороги.
Иногда небольшой кусок кода и вправду лучше скопировать, чем усложнять код и терять время.
Потом найти багу, поменять только в одном месте, в поисках почему же все не так поменять по другому в скопированной части. И вот, здравствуй, новая CVE. Я уж не говорю о расходах по памяти, причём не RAM, а медленный своп.
От ситуации зависит. Думать надо, когда можно скопировать, а когда не стоит.
Звучит как «надо думать где баги делать, а где нет» :-)

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

Шта? Хуками нельзя пользоваться в классах.

Вот именно. И прочитать об этом негде, а хочется. И вот так бы писать хотелось бы тоже:


export default class MyComponent extends Component {
  render () {
    const {
      app_context: {greatest_variable},
      other_context: {other_variable},
      custom_variable
  } = this

    return (
       <>
           <div>{greatest_variable}</div>
           <div>{other_variable}</div>
           <div>{custom_variable}</div>
       </>
    )
  }

  get custom_variable() {
    const {
      app_context: {count: app_count},
      other_context: {count: other_count},
    } = this

    return app_count + other_count
  }

  app_context = useContext(my_super_context)
  other_context = useContext(my_other_context)
}

*Это синтетический пример кода. В настоящее время в нём поддерживается всё кроме хуков.

Прочитать об этом можно в документации: ru.reactjs.org/docs/hooks-overview.html#but-what-is-a-hook

По поводу твоего примера, я бы так написал:

const MyComponent = () => {
  const {greatest_variable, count: app_count} = useContext(my_super_context)
  const {other_variable, count: other_count} = useContext(my_other_context)

  const custom_variable = app_count + other_count

  return (
    <>
      <div>{greatest_variable}</div>
      <div>{other_variable}</div>
      <div>{custom_variable}</div>
    </>
  )
}

export default MyComponent

И короче, и более предсказуемо, и легче читается. Ну а совмещать хуки с классами не получится, т.к. они изначально позиционировались как замена классам. У классов же есть всё, что предлагают хуки, не вижу смысла пытаться это совмещать.

Хуки завезли не чтобы классы заменить, а чтобы дать возможность использовать возможности классов в функциональных компонентах.


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

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

И в чём же заключается надругательство над концепцией? Вы случайно не путаете область видимости и время жизни?

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

Но в чём надругательство над концепцией-то? Переменная объявлена в некотором блоке кода — переменная видна внутри этого блока кода, и не важно сколько там вложенных блоков внутри, и являются ли эти блоки функциями...


Со стеком области видимости концептуально никак не связаны, это всего лишь детали реализации. Ну а "висение в памяти неизвестно сколько" — это общее свойство любых объектов в языках со сборщиком мусора.

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

А с чего это функция не функция?

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

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

Родительский скоуп — он такой же, как и глобальный скоуп, только не глобальный.

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

Вот только родительский скоуп не обладает недостатками глобальных переменных.


Напомню почему глобальные переменные ругают:


  1. конфликты имён — мимо, переменных в родительском скоупе в нормальной программе не настолько много;
  2. непонятно где используются — мимо, если не писать километровые функции то всё понятно;
  3. нельзя сбросить в начальное состояние или сделать копию при необходимости — мимо, ничто не мешает вызвать родительскую функцию ещё раз.
1. конфликты имён — мимо, переменных в родительском скоупе в нормальной программе не настолько много

2. непонятно где используются — мимо, если не писать километровые функции

4. время жизни как у глобальных — т.е. вечное, пока исполняется программа. А если это браузер с двумя десятками открытых вкладок, то сами понимаете…
Это всё и приводит к тому, о чем я написал в первом комментарии.
время жизни как у глобальных — т.е. вечное, пока исполняется программа.

С фига ли?

А сколько по-вашему, если я в замыкании вешаю обработчик на кнопку, которая никогда не нажмется?

Столько, сколько время жизни у функции (если она одна, конечно), которая замкнута на эти переменные. Что, в общем-то, логично.

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


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

Кнопок на сайте может быть и 100, и у всех обработчики, а нажать юзер может только одну.
Неопытный или недобросовестный разработчик очень легко может захватить и удерживать в памяти ненужные данные, в этом главная беда замыканий. Вообще это проблема всех языков с GC, но в JS стоит особенно остро.
С управляемой памятью разработчики перестали стрелять себе в ногу — они стали стрелять в голову юзеру.
время жизни как у глобальных — т.е. вечное, пока исполняется программа.

Это в какой-то конкретной имплементации, возможно, так. Но вот как раз концептуально это не так.

GC не тронет мусор если на него есть ссылки. А вот забота о том, чтобы не оставалось ссылок на ненужные объекты — ваша.


Глобальная переменная не обязательно должна жить вечно.
Её например можно удалить:


delete window.my_global_variable

В следующем раунде GC заметит, что на данные больше никто не ссылается и удалит их из памяти.


Стоит учитывать, что объекты записываются по ссылке. И если удалить ссылку (например в window), сам объект продолжит существовать. Если в другом месте приложения по прежнему присутствует ссылка на объект — сборщик мусора пройдёт мимо.


Есть масса способов выстрелить себе в ногу, помешав сборщику мусора делать свою работу. В том числе не очевидные.

С замыканиями примерно та же история

Неа, с замыканиями нет "примерно той же истории". Хорошо сделанные (не знаю, как конкретно в JS) замыкания — это то, на чем (частично) держится удобный читаемый функциональный код. Типовой пример:


User GetUserByName(string name)
{
  return _users.Single(u => u.Name == name);
}

В данном случае Single — это общеупотребимая функция с одним параметром вида T -> bool. Ну и как это нормально переписать без замыканий?

Насчет «удобно» я и не спорил. Сам иногда пользуюсь.
«Читаемо» — ну это уже на вкус и цвет, как говорится. На мой взгляд, например, не очень.
Меня больше беспокоит, не как это написать, а как это исполнять. Но это совсем другая тема, тут моя ошибка.

«Читаемо» — ну это уже на вкус и цвет, как говорится. На мой взгляд, например, не очень.

А как сделать лучше, только чтобы оно выполняло ту же задачу и было консистентно?


Меня больше беспокоит, не как это написать, а как это исполнять.

Я не знаю, как в JS, а в C#, откуда этот пример взят, это "исполнять" тривиально: на этапе компиляции создается анонимный класс, в поля которого записываются все захваченные переменные, функция становится его единственным методом, этот метод вызывается. Все, профит.

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

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

А что в этом хорошего, кроме визуального шума?


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

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

А что в этом хорошего

Дисциплинирует и вносит ясность для разработчика и компилятора.
А чем это отличается от ситуации «передали большой ненужный объект как параметр»?

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

Предлагаю на том и порешить.
Дисциплинирует

А какая мне польза от этой дисциплины?


вносит ясность для разработчика и компилятора.

А компилятору в примере выше и так понятно, какая переменная захвачена, зачем ему это дополнительно объяснять? А если понятно компилятору — то понятно и разработчику, мне IDE наглядно показывает, где переменная объявлена.


Ну это надо осознанно и умышленно идти против здравого смысла.

Ну так и при захвате большого объекта надо осознанно и умышленно идти против здравого смысла. Не вижу разницы.


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

_users.Single(u => u.Name == name);

Это кстати удобный читаемый функциональный — ООП


А так будет без замыканий и функционально:


function getUserByName(name, users)
{
  for(let user of users) {
      if(user?.name === name) {
          return user
      }
  }
}
А так будет без замыканий

Да я тоже могу написать без замыканий. Только переиспользовать кусок кода


if(user?.name === name) {
  return user
}

не получится. А смысл именно в этом. Собственно, вот вам чистое функциональное:


getUserByName name users = users |> tryFirst (fun u -> u.name == name)
u -> u.name

Точка для доступа к свойству объекта. Предлагаю признать ничью.


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


Например ФП и ООП отлично дополняют друг друга.
Всему своё место и время.

Точка для доступа к свойству объекта

Неа. Это record, а не объект.


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

Вам это не удалось.

Хорошо. Вы победили. Спасибо за ликбез.


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


А начиналось то всё с хорошо читаемого кода, хоть и с сахаром, ООП, замыканиями… и чего там ещё не в ходу у ортодокса, но хорошо — читаемого.


За чистоту кода, а не подхода.

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

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


Точнее даже как, в обоих случаях код можно еще сократить и сделать еще более идиоматичным (и более читаемым для тех, кто язык знает), но это было бы менее понятно в данной дискуссии, где я ориентировался на тех, кто язык не знает.

Точка для доступа к свойству объекта. Предлагаю признать ничью.

Ну вот вам без точек для доступа к объекту:


import qualified Data.List.Safe as List

getUserByName name users = List.head $ filter (\u -> Name u == name) users
Здесь просто написано, а в том (кстати очень замечательном учебнике) как то сложно…

Замыкание — это то, что здесь описано в "призраке вызова функции". В предыдущих примерах только доступ к внешним переменным.

Пока, это самое доходчивое для меня, описание замыканий.

Замыкания — это зло. При частом использовании создают абсолютно не читаемый код… Как раз сейчас такой ковыряю…

Замыкания — это зло.

Плохо примененные замыкания — зло.

Sign up to leave a comment.