Недавно столкнулся с одной из "типовых" задач на собеседованиях. Просто увидел такое выражение где-то в интернете и не совсем понял как оно работает. А именно не понимал почему пустой массив дает 0.
![] == []
Такое выражение вернет true.
Решил конкретно рассмотреть данный пример по спецификации, и понять что происходит шаг за шагом.
TL;DR
![]- возвращаетfalse, потому что объект в Boolean всегдаtrueи противоположностьtrue- этоfalseПри нестрогом сравнении где операнды имеют разный тип, операнды приводятся к числу
falseпреобразуется в число0Пустой массив преобразуется в число
0, потому что при приведении объектов к примитиву будет вызван сначалаvalueOf, потомtoString(иногда порядок вызовов будет разный) и возьмется первый примитивный результат вызова. У нас возьмется результат вызоваtoString, потому что он вернет примитив (valueOfна объекте, вернет тот же объект).toStringнам вернет примитив, потому что вызовtoStringна массиве вызывает его методjoin, а методjoinс пустым массивом вернет пустую строку'', а пустая строка''превратится в0.Сравнение
0и0дадутtrue
Подсчет количества выражений
В данном случае у нас имеется 4 выражения (Expressions).
Порядок выполнения можно посмотреть в разделе 13 (https://262.ecma-international.org/14.0/#sec-ecmascript-language-expressions). Только там не будет красивой таблички. Придется идти по экспрешенам и смотреть что в каком порядке выполнится.
![] == []
^^ -> 1
![] == []
^^^ -> 2
![] == []
^^ -> 3
![] == []
^^^^^^^^^^ -> 4Создание массива
Вызов логического НЕ c массивом
Создание массива
Нестрогое равенство
Давайте на примере разберем запись спецификации относительно 4 пункта.
Видим вот такую запись: ([?In, ?Yield, ?Await] - специально удалил, что бы не путалось под глазами, данная запись нас сейчас не интересует)
EqualityExpression :
RelationalExpression
EqualityExpression == RelationalExpression
EqualityExpression != RelationalExpression
EqualityExpression === RelationalExpression
EqualityExpression !== RelationalExpressionДанная запись означает что EqualityExpression будет равна чему то из списка.
Возьмем на пример выражение 0 == 0 == 0
Его мы можем записать как:
EqualityExpression == RelationalExpression
^^^^^^^^^^^^^^^^^^ -> EqualityExpression == RelationalExpression
EqualityExpression == RelationalExpression == RelationalExpression
^^^^^^^^^^^^^^^^^^ -> RelationalExpression
RelationalExpression == RelationalExpression == RelationalExpressionЗдесь мы не стали раскладывать RelationalExpression далее по цепочки, но ниже мы это сделаем.
Все выражения можно опять же представить деревом, в котором главный узел - это выражение с самым низким приоритетом, а его листья - выражения с более высоким приоритетом.
Далее мы спускаемся по цепочки определений.
RelationalExpression :
RelationalExpression :
ShiftExpression
RelationalExpression < ShiftExpression
RelationalExpression > ShiftExpression
RelationalExpression <= ShiftExpression
RelationalExpression >= ShiftExpression
RelationalExpression instanceof ShiftExpression
RelationalExpression in ShiftExpression
PrivateIdentifier in ShiftExpressionТо есть у нас в каждом верхнем определении есть еще набор экспрешенов, который его определяет, и так далее вниз, пока не дойдем до самых приоритетных выражений. В данном случае выражение RelationalExpression может в себе содержать любое из списка, например RelationalExpression < ShiftExpression. Поэтому мы можем сказать, что верхние выражения содержат в себе весь набор нижних выражений. Например, взяв выражение RelationalExpression мы можем этим описать любое выражение, которые входит в него, от UpdateExpression ** ExponentiationExpression до простого Literal, но не выше цепи, то есть оно уже не будет являться EqualityExpression, а наоборот, будет его потомком.
Вся цепочка с нашими операндами будет выглядеть так:
EqualityExpression
RelationalExpression
ShiftExpression
AdditiveExpression
MultiplicativeExpression
ExponentiationExpression
UnaryExpression
UpdateExpression
LeftHandSideExpression
NewExpression
MemberExpression
PrimaryExpression
Literal
Все. Далее уже идет определение литерала:
NumericLiteral
DecimalLiteral
DecimalIntegerLiteral
0
Мы можем заменить каждый наш операнд на его представление ниже по дереву (из каких выражений он может состоять)
В конце концов у нас будет цепочка выглядеть так:
1) EqualityExpression == RelationalExpression
2) (EqualityExpression == RelationalExpression) == RelationalExpression
3) ((RelationalExpression) == RelationalExpression) == RelationalExpression
Далее мы можем заменить RelationalExpression на его представление ниже по дереву, пока не дойдем до нужного выражения.
...
и так далее, до:
Literal == Literal == Literal
что станет:
0 == 0 == 0Что бы закрепить, давайте рассмотрим начало цепочки 1 + 1 == 0 == 0.
Буду заменять только те выражение, которое внутри себя содержит 1 + 1
1) EqualityExpression == RelationalExpression
2) (EqualityExpression == RelationalExpression) == RelationalExpression
3) ((RelationalExpression) == RelationalExpression) == RelationalExpression
4) (((ShiftExpression)) == RelationalExpression) == RelationalExpression
5) ((((AdditiveExpression))) == RelationalExpression) == RelationalExpression
6) (((((AdditiveExpression + MultiplicativeExpression)))) == RelationalExpression) == RelationalExpression
...
n) (((((Literal + Literal)))) == Literal) == Literal
-----
(((((1 + 1)))) == 0) == 0Если хотите сами попробовать опуститься с самого начала, вот вам самый менее приоритетный оператор: (https://262.ecma-international.org/14.0/#sec-comma-operator)
Порядок выполнения выражений
Может показаться, что как будто мы начинаем выполнять выражения с самого низко приоритетного оператора. Так оно и есть! Но как так?
Все просто. Во время разрешения экспрешенов, у вас идет выполнения левых и правых операндов. И пока они не выполнились, наше выражение ждет(если можно так выразиться) их выполнения.
Простыми словами - самый низко приоритетный оператор запускается самым первым, но заканчивает свою работу самым последним. Можете представить это как стек операций, где в самом верху выполнится самая приоритетная операция, а в самом низу - самая низко приоритетная операция.
Вот алгоритм выражения сравнения из спецификации:(https://262.ecma-international.org/14.0/#sec-equality-operators-runtime-semantics-evaluation)
EqualityExpression == RelationalExpression
1. Let lref be ? Evaluation of EqualityExpression.
2. Let lval be ? GetValue(lref).
3. Let rref be ? Evaluation of RelationalExpression.
4. Let rval be ? GetValue(rref).
5. Return ? IsLooselyEqual(rval, lval).Видно что на 1 шаге выполняется Evaluation нашего левого операнда(EqualityExpression), а на 3 шаге правого(RelationalExpression). Это означает, что пока наши операнды не выполнятся, шаг 5 не наступит. Можно заменить, что выполнение идет слева направо. Сначала будут выполняться все левые операнды (экспрешены), потом правые.
Как пример в выражении 1 + 2 * 3 == 4 операнды закончат выполнение по порядку - умножение, потом сложение и потом сравнение, но начнут выполнятся в порядке - сравнение, сложение, умножение. А точнее:
┌── Началось выполнение экспрешена нестрогого равенства ==
│ ├── Выполняется левый операнд
│ │ ├── Началось выполнение экспрешена сложения
│ │ │ ├── Выполняется левый операнд
│ │ │ │ ├── Началось выполнение экспрешена получение числа 1
│ │ │ │ └── Закончилось выполнение экспрешена получения числа, вернулось число 1
│ │ │ └── Выполняется правый операнд
│ │ │ ├── Началось выполнение экспрешена умножения *
│ │ │ │ ├── Выполняется левый операнд
│ │ │ │ │ ├── Началось выполнение экспрешена получения числа 2
│ │ │ │ │ └── Закончилось выполнение экспрешена получения числа, вернулось число 2
│ │ │ │ └── Выполняется правый операнд
│ │ │ │ ├── Началось выполнение экспрешена получения числа 3
│ │ │ │ └── Закончилось выполнение экспрешена получения числа, вернулось число 3
│ │ │ └── Закончилось выполнение экспрешена умножения, вернулось число 6
│ │ └── Закончилось выполнение экспрешена сложения, вернулось число 7
│ └── Выполняется правый операнд
│ ├── Началось выполнение экспрешена получения числа 4
│ └── Закончилось выполнение экспрешена получения числа, вернулось число 4
└── Закончилось выполнение выражение нестрогого равенства, вернулось значение falseОперанды сравнения -
(1 + 2 * 3)и(4)Операнды сложения -
(1)и(2 * 3)Операнды умножения -
(2)и(3)
И это логичное поведение парсера. Вы строите дерево. Где на самой верхушке будет то выражение, что выполниться самым последним с его листьями - операндами, но запустится самым первым(если можно так выразиться).
1 + 2 * 3 == 4
/ \
/ \
/ 4
1 + 2 * 3
/ \
/ \
1 \
2 * 3
/ \
/ \
2 3Правильнее было бы это дерево нарисовать все же так:
==
/ \
/ \
/ 4
+
/ \
/ \
1 \
*
/ \
/ \
2 3Как вы видите, здесь 7 экспрешенов (получение числа это тоже expression). Вообще можно просто прочитать про то, как работают те же абстрактные синтаксические деревья (AST).
Разбор примера
Вернемся к нашему изначальному примеру.
![] == []
/ \
/ \
![] \
\ []
[] Давайте тоже его отобразим в нормальном виде
=
/ \
/ \
! \
\ []
[] Давайте по шагам получи результаты наших операндов.
Получение массива
1)Получаем массив - тут все просто.
Выполнение логического НЕ ( ! )
2)Логическое НЕ к массиву. Давайте смотреть, что там. Понятное дело все знаю что это выражение вернет false, но почему? Ответ как и всегда, да потому что так написано в спецификации. Вот и давайте посмотрим, что там написано:
Logical NOT Operator ( ! ): (https://262.ecma-international.org/14.0/#sec-logical-not-operator)
1. Let expr be ? Evaluation of UnaryExpression.
2. Let oldValue be ToBoolean(? GetValue(expr)).
3. If oldValue is true, return false.
4. Return true.3, 4 пункт просто возвращают противоположный примитивное булеан значение, мы же идем смотреть 2 пункт. (пока что на GetValue(expr) не надо обращать внимание, оно возвращает специальный тип спецификации, в нашем случае мы можем воспринимать это как просто то, что возвращает нам само значение - наш массив). Видим операцию ToBoolean
ToBoolean: (https://262.ecma-international.org/14.0/#sec-toboolean)
1. If argument is a Boolean, return argument.
2. If argument is one of undefined, null, +0 