Наша история начинается с твита от Томаша Чакоми, в котором он просит вас представить этот вопрос, который возникает на собеседовании по кодированию.
Что касается вопроса о том, как я отреагирую на это на собеседовании, мне кажется, это зависит от того, в чем суть вопроса. Если на самом деле вопрос в том, каково значение tree, они могут отказаться или ожидать, что я просто вставлю его в свою консоль, а затем скажу им, чтобы они отключились.
Если, однако, вопрос в том, как вы решите эту проблему, что ж, тогда это довольно интересно и приводит к множеству причуд в отношении JavaScript и того, как работает компилятор. Итак, в этой статье я собираюсь погрузиться в эту неразбериху и изучить, какие интересные идеи могут возникнуть.
Я также стримил процесс решения этой проблемы на Twitch. Он длинный, но дает другой взгляд на то, как решать такие проблемы по частям.
Мысли высокого уровня
Во-первых, давайте перейдем к копируемому состоянию:
let b = 3, d = b, u = b; const tree = ++d * d*b * b++ + + --d+ + +b-- + + +d*b+ + u
Я сразу замечаю здесь несколько вещей. Я подумал, что с компилятором могут быть какие-то трюки. см. JavaScript обычно добавляет точку с запятой в конец каждой строки , если нет выражения, которое не может быть завершено. В этом случае +
в конце каждой строки сообщает компилятору не завершать этот оператор.
Первая строка просто создает три переменные, для всех которых установлено значение 3
. 3
- это примитивное значение, поэтому каждый раз, когда создается копия, это делается по значению, и поэтому создаются новые переменные со значением 3
. Если JavaScript присвоил эти переменные по ссылке, то каждая новая переменная будет указывать на предыдущую используемую переменную, но не создает для себя все значение.
"Дальнейшее чтение"
Приоритет операторов и ассоциативность
Это ключевые концепции для решения этой ужасной проблемы с деревьями. Я объясню каждый момент, но на высоком уровне они определяют порядок, в котором оценивается комбинация выражений JavaScript.
Приоритет оператора
Вопрос: в чем разница между этими двумя выражениями?
3 + 5 * 5 5 * 5 + 3
По результату разницы нет. Любой, кто помнит школьные уроки математики, помнит, что прежде чем сложить, нужно умножить. Я помню это как BODMAS, или Скобки от деления, умножения, сложения, вычитания. В JavaScript у нас есть та же концепция, называемая приоритетом оператора, и она просто означает порядок, в котором мы оцениваем выражения. Если мы хотим заставить 3 + 5
выполнить оценку первым, мы просто делаем следующее
(3+5) * 5
Скобки заставляют сначала вычислять эту часть выражения, потому что приоритет оператора ()
выше, чем у *
.
Каждый оператор JavaScript имеет приоритет, и при таком большом количестве операторов в дереве нам нужно понимать, в каком порядке будут выполняться вычисления. Тем более, что ++
и --
изменят значения b
и d
, нам нужно знать, когда эти выражения оценивается по отношению к остальной части дерева.
Важно: Таблица приоритетов операторов и дополнительная информация
Ассоциативность
Ассоциативность используется для определения того, какие выражения порядка вычисляются в заданных операторах с равным приоритетом. Например:
a + b + c
В этом выражении у нас нет приоритета оператора, поскольку у нас есть только один оператор. так мы оцениваем это как (a + b) + c
или как a + (b + c)
?
Я знаю, что ответ тот же самый, но компилятор должен знать, чтобы он мог сначала выбрать один, а затем продолжить. В этом случае (a + b) + c
является ответом, потому что оператор +
является левоассоциативным, то есть сначала он вычисляет левое выражение.
"Почему бы им просто не сделать все ассоциативным?" Я слышал, вы спросите.
Что ж, рассмотрим следующее:
a = b + c
Если мы последуем нашей левой ассоциативной формуле из предыдущего, это оставляет нас с
(a = b) + c
Но подожди. Это выглядит странно, и я не это имел в виду. Если бы мы хотели, чтобы это выражение работало, используя только левую ассоциативность, нам нужно было бы сделать что-то вроде этого:
a + b = c
который становится (a + b) = c
, или для большей ясности мы сначала делаем a + b
, а затем присваиваем c
значению этого результата.
Если бы мы думали таким образом, JavaScript был бы гораздо более запутанным, и причина, по которой мы различаем ассоциативность для разных операндов, состоит в том, чтобы сделать код более читабельным. Когда вы читаете a = b + c
, порядок оценки кажется естественным, хотя под капотом творится некоторая хитрость и используются как левый, так и правый ассоциативные операнды.
Теперь вы могли заметить, что с a = b + c
возникает проблема ассоциативности. Поскольку обе операции имеют разную ассоциативность, как узнать, какое выражение разрешить в первую очередь? Ответ - тот, который имеет более высокий приоритет оператора, как в предыдущем разделе! В этом случае +
имеет приоритет, поэтому он оценивается в первую очередь.
Я добавил более подробное объяснение в конце, или вы можете прочитать больше здесь.
Понимание того, как оценивается наше дерево выражений
Теперь, когда у нас есть представление об этих концепциях, мы можем начать понимать нашу проблему. В игре много операторов, и нет скобок, чтобы прояснить, когда все происходит с нашим жалким человеческим разумом. Итак, давайте добавим скобки, просто перечислив все используемые операторы вместе с их приоритетом и ассоциативностью.
(operator with variable x): (precedence) (associativity) x++: 18 n/a x--: 18 n/a ++x: 17 r --x: 17 r +x: 17 r *: 15 l x + y: 14 l = : 3 r
Скобки
Здесь стоит упомянуть, что правильно добавить скобки - непросто. Я удостоверился, что ответ оценивался правильно в каждой точке, но это не гарантирует, что мои скобки всегда были правильными! Если кому-то известен инструмент для автоматического добавления скобок, дайте мне знать.
Давайте выясним, в каком порядке оцениваются объекты, и добавим скобки для отображения. Я сделаю это поэтапно, чтобы показать, как я пришел к окончательному результату, просто работая с операторами с наивысшим приоритетом вниз.
постфикс ++
и постфикс --
const tree = ++d * d*b * (b++) + + --d+ + +(b--) + + +d*b+ + u
унарный +
, префикс ++
и префикс --
У нас есть небольшая проблема, но я начну с оценки унарного оператора +
, пока мы не дойдем до проблемного места.
const tree = ++d * d*b * (b++) + + --d+ (+(+(b--))) + (+(+(d*b+ (+ u))))
А теперь у нас есть немного хитрости.
const tree = ++d * d*b * (b++) + + --d+ (+(+(b--))) + (+(+(d*b+ (+ u))))
Я выделил проблемную область. --
и +()
имеют одинаковый приоритет. Итак, как нам узнать, в каком порядке оценивать? Позвольте мне сформулировать проблему проще
let d = 10 const answer = + --d
Помните, здесь +
не означает сложение, но означает унарный плюс или положительный результат. Думайте об этом так, как если бы вы думали о -1
, только это +1
.
Ответ заключается в том, что мы оцениваем справа налево, потому что операторы этого приоритета правоассоциативны.
Итак, наше выражение переписано в + (--d).
Чтобы попытаться понять это, представьте, что все операторы были бы одинаковыми. в этом случае + +1
будет таким же, как (+ (+1))
, следуя той же логике, что 1 — 1 — 1
будет таким же, как ((1 — 1) — 1)
. Обратите внимание, как правоассоциативные операторы приводят к обозначению скобок, противоположному левоассоциативным операторам?
применение этой логики к проблемному месту в нашем выражении дает нам:
const tree = ++d * d*b * (b++) + (+ (--d)) + (+(+(b--))) + (+(+(d*b+ (+ u))))
И, наконец, заполнение скобок для нашего последнего ++
дает нам:
const tree = (++d) * d*b * (b++) + (+ (--d)) + (+(+(b--))) + (+(+(d*b+ (+ u))))
Multiplication (*)
Нам снова приходится иметь дело с ассоциативностью, но на этот раз все с тем же оператором и все левоассоциативны. По сравнению с последним шагом это должно быть пустяком!
const tree = ((((++d) * d) * b) * (b++)) + (+ (--d)) + (+(+(b--))) + (+(+((d*b) + (+u))))
Мы находимся в точке, где можем начать реально оценивать вещи. Мы также могли бы добавить дополнительные скобки для оператора присваивания, но я думаю, что это создаст больше путаницы, чем устранит, поэтому я оставил это. Обратите внимание, что приведенное выше выражение является просто более сложным x = a + b + c
Мы могли бы отменить некоторые унарные операторы, но сейчас я оставлю их для информации на всякий случай, если они важны.
Разделив выражение на несколько частей, мы сможем понять отдельные этапы оценки и перейти к ним.
let b = 3, d = b, u = b; const treeA = ((((++d) * d) * b) * (b++)) const treeB = (+ (--d)) + (+(+(b--))) const treeC = (+(+((d*b) + (+u)))) const tree = treeA + treeB + treeC
Теперь, когда мы это сделали, мы можем приступить к изучению различных значений по мере их оценки. Начиная с treeA
ДеревоА
let b = 3, d = b, u = b; const treeA = (((++d) * d) * b) * (b++)
Первое, что будет оцениваться здесь, будет ++d
, который будет возвращать 4
и увеличивать d
.
// b = 3 // d = 4 ((4 * d) * b) * (b++)
Затем 4*d
, и мы знаем, что в этот момент d равно 4, поэтому 4*4
равно 16.
// b = 3 // d = 4 (16 * b) * (b++)
На этом этапе интересно то, что мы собираемся умножить на b до того, как b будет увеличено, потому что мы вычисляем слева направо. 16 * 3 = 48
// b = 3 // d = 4 48 * (b++)
Ранее мы говорили о том, что ++
имеет более высокий приоритет операторов, чем *
, поэтому это можно записать как 48 * b++
, но здесь есть больше уловок, поскольку возвращаемое значение b++
- это предварительно увеличенное значение, а не Почта. Таким образом, в то время как b будет заканчиваться на 4, значение, на которое мы будем умножать, будет 3.
// b = 3 // d = 4 48 * 3 // b = 4 // d = 4
а 48 * 3
равно 144
, поэтому, как только наша первая часть вычислена, как b, так и d равны 4, а результат выражения будет 144
let b = 4, d = 4, u = 3; const treeA = 144 const treeB = (+ (--d)) + (+(+(b--))) const treeC = (+(+((d*b) + (+u)))) const tree = treeA + treeB + treeC
TreeB
const treeB = (+ (--d)) + (+(+(b--)))
На данный момент я вижу, что унарные операторы на самом деле ничего не делают. Если мы просто отменим их, мы сможем значительно упростить выражение.
// b = 4 // d = 4 const treeB = (--d) + (b--)
Мы уже видели этот трюк. --d
вернет нам 3
и b--
вернет нам 4
, и обоим теперь будет присвоено 3 к моменту вычисления выражения.
const treeB = 3 + 4 // b = 3 // d = 3
Итак, теперь наша проблема выглядит примерно так:
let b = 3, d= 3, u = 3; const treeA = 144 const treeB = 7 const treeC = (+(+((d*b) + (+u)))) const tree = treeA + treeB + treeC
TreeC
Наконец-то в финише!
// b = 3 // d = 3 // u = 3 const treeC = (+(+((d*b) + (+u))))
Давайте начнем с очистки этих надоедливых унарных операторов
// b = 3 // d = 3 // u = 3 const treeC = (+(+((d*b) + u)))
Это один, но мы должны быть немного осторожны здесь со скобками и т. Д.
// b = 3 // d = 3 // u = 3 const treeC = (d*b) + u
На этом этапе достаточно просто. 3 * 3
это 9
, 9 + 3
это 12
и, наконец, у нас осталось ...
Ответ!
let b = 3, d= 3, u = 3; const treeA = 144 const treeB = 7 const treeC = 12 const tree = treeA + treeB + treeC
144 + 7 + 12
это 163
. Ответ 163
.
Заключение
Есть масса необычных и замечательных способов, которыми JavaScript может вас подбросить. Но с пониманием того, как устроен язык, вы сможете понять, почему вещи действуют именно так.
В более общем плане путь к решению может быть более поучительным, чем ответ, а мини-решения, которые вы найдете на этом пути, сами по себе могут быть обучающими моментами.
Стоит сказать, что я проверял свою работу, используя консоль браузера, и меня больше интересовал обратный инжиниринг решения, чем решение его из первых принципов.
Даже зная, как решить эту проблему, приходилось решать множество синтаксических неоднозначностей, многие из которых, я уверен, вы заметили, глядя на дерево! Я отметил некоторые из них ниже, но каждый достоин отдельной статьи!
Я также хотел бы поблагодарить https://twitter.com/AnthonyPAlicea, без курса которого я бы никогда не смог разобраться в этом, а также https://twitter.com/ tlakomy за постановку вопроса в первую очередь.
Заметки и странные вещи
Я сохранил это отдельно, чтобы попытаться сохранить общий поток решения проблем в чистоте, но по ходу дела мне приходилось разгадывать множество мини-загадок.
Как влияет порядок изменения и использования переменных.
"Учти это"
let x = 10 console.log(x++ + x)
Здесь есть несколько вопросов. Что будет записываться в консоль и каково значение x
во второй строке?
Если вы ответили числом, извините, но я вас обманул. Хитрость в том, что x++ + x
оценивается как (x++) + x
, а когда механизм JavaScript оценивает левую часть (x++)
, он увеличивает x
, поэтому, когда дело доходит до + x
, значение x равно 11
, а не 10
.
Чтобы еще больше усложнить задачу, какое значение возвращает x++
?
Я довольно сильно намекнул, что на самом деле ответ _74 _. *
В этом разница между x++
и ++x
. Если рассматривать операторы как базовые функции, они выглядят примерно так:
function ++x(x) { const oldValue = x; x = x + 1; return oldValue; } function x++(x) { x = x + 1; return x }
Рассматривая их таким образом, вы можете понять, что
let x = 10 console.log(x++ + x)
Означает, что x++
возвращает 10
, и в момент оценки + x
его значение равно 11
. Таким образом, консоль будет регистрировать 21
, а значение x будет 11
.
Эта относительно простая проблема на самом деле указывает на общий антишаблон для всего кода, заключающийся в том, что выражения и побочные эффекты смешиваются. "Подробнее"
* Я собирался сказать 10
! здесь, но кто-то пошутил бы факториалом, а я уже достаточно сошел с ума по этому поводу)
Можно ли сделать так, чтобы два оператора имели одинаковый приоритет, но разную ассоциативность?
Давайте сделаем это шаг за шагом и не будем обращать внимания на то, является ли ассоциативность реальным словом или нет.
Возьмем операторы +
и =
и сделаем обобщение.
В первом случае a + b + c
оценивается как (a + b) + c
, потому что +
остается ассоциативным.
В последнем случае a = b = c
оценивается как a = (b = c)
, потому что =
правильно ассоциативен. обратите внимание, что =
возвращает значение присвоенной переменной, поэтому a
станет равным значению b
после вычисления этого выражения.
Теперь, если бы мы заменили операнды на их приоритет,
a left b left c
=(a left b) left c
a right b right c = a right (b right c) but what about a left b right c = ? a right b left c = ?
Вы понимаете, насколько логически невозможны вторые примеры? a + b = c
возможно только потому, что +
имеет приоритет над =
, поэтому синтаксический анализатор знает, что делать. Если бы два оператора имели одинаковый приоритет, но разную ассоциативность, синтаксический анализатор не мог бы определить, в каком порядке выполнять действия!
Итак, чтобы подвести итог, нет. Операторы с одинаковым приоритетом не могут иметь разную ассоциативность!
Интересно, что в F # вы можете изменять ассоциативность функции на лету, поэтому я смог говорить об ассоциативности, не сходя с ума! "Подробнее"
Примечание. Я не расстраиваюсь из-за того, что ушел б. С самого начала они были токсичной парой. c, с другой стороны, было сокровищем и никогда не заслуживал такого горя.
Унарные операторы
Это был интересный момент, связанный с попыткой выяснить порядок оценки +n
и ++n
.
Вы не можете сделать -- -i
, потому что —
возвращает число, и числа не могут быть увеличены или уменьшены, и вы не можете сделать ---i
, потому что значение ---
неоднозначно (это -- —
или — --
? Комментарии ниже), но вы можете сделать :
let i = 10 console.log(-+-+-+-+-+--i)
Положительно сбивает с толку
Одной из наиболее проблемных проблем была двусмысленность +
в JavaScript. Этот же символ используется для четырех различных функций, как показано ниже:
let i = 10 console.log(i++ + + ++i)
Каждый операнд имеет собственное значение, приоритет операторов и ассоциативность. Это напоминает мне знаменитую словесную головоломку:
Буйвол бизон Буйвол буйвол буйвол бизон Буйвол буйвол.
Я не буду портить ответ, но скажу, что наш эквивалент в JavaScript на самом деле на порядок более запутанный.
Унарный или переуступка?
+
может означать либо унарный оператор, либо присваивание. что это в случае u
для начальной проблемы? вот так
... + u
Что ж, ответ в конечном итоге зависит от того, что… есть. Если бы мы записали его в одну строку,
... + u
Ответ между x + u
и x — + u
разный. В первом случае это означает сложение, а во втором - унарный +
. Единственный способ выяснить, что это означает, - это выяснить остальную часть дерева оценки, пока не останется только один оператор, который он может означать!