JS versus PHP battle


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

Дано:

var $a, $b;
$a = 1;
$b = 1;
console.log( $a + $a++ );
console.log( $b + $b + $b++ );
console.log( $a, $b );

и такой же код на PHP:

<?php
$a = 1;
$b = 1;
var_dump( $a + $a++ );
var_dump( $b + $b + $b++ );
var_dump( $a, $b );

Собственно простой вопрос: что будет выведено?

В целом задачка очевидна вроде бы. Зная как работают операторы и с какой стороны парсит парсер: слева направо или справа налево можно дать точный ответ. Думай как парсер, будь как парсер 🙂

Ответы

Нанчем с JS кода:

Чтение выражения идет слева направо. Изначально $a = 1. После вычисления выражения $a = 2. Итого получаем:

//            1    1    и после $a = 2
console.log( $a + $a++ ); // = 2
//            1    1    1  и после $b = 2
console.log( $b + $b + $b++ ); // = 3
console.log( $a, $b ); // 2 , 2

Это разные выражения и условие

($a + $a++) !== ($b + $b + $b++)

истинно. А теперь рассммотрим тот же код на PHP:

Прежде чем пойдем дальше, скажу что условие

($a + $a++) === ($b + $b + $b++)

для PHP будет истинно. Внезапно, да?!

(ссылка для проверки во всех актуальных версиях интерпретатора https://3v4l.org/1ss4E)

Ну давайте разбираться почему.

//         2    1  и после $a = 2
var_dump( $a + $a++ ); // = 3

Почему так? Сначала PHP смотрит на правую часть видит что там идет вызов $a и подставляет значение 1, но тут же продолжает вычисление, в результате которого происходит постфиксный инкремент. Теперь $a=2. Когда интерпретатор начинает вычислять левую часть $a уже равен 2 и у нас получается выражение 2 + 1

Что же происходит с $b?

 1    1    1  и после $b = 2
$b + $b + $b++

В данном случае интерпретатор как бы “сгруппирует” выражения:

1 операция      2 операция
/ /
1 1 1 и после $b = 2

($b + $b) + $b++

Все дело в приоритете операторов. Если идете на собеседование, советую подтянуть знания почитав мануал http://php.net/manual/ru/language.operators.precedence.php

Если операторы имеют равный приоритет, то будут ли они выполняться справа налево или слева направо определяется их ассоциативностью. К примеру, “-” является лево-ассоциативным оператором. Следовательно 1–2–3 сгруппируется как (1–2) — 3 и пересчитается в -4. С другой стороны “=” — право-ассоциативный оператор, так что $a = $b = $c сгруппируется как $a = ($b = $c).

Неассоциативные операторы с одинаковым приоритетом не могут использоваться совместно. К примеру 1 < 2 > 1 не будет работать в PHP. Выражение 1 <= 1 == 1, с другой стороны, будет, поскольку == имеет более низкий приоритет чем <=.

И теперь, зная эту особенность, можно играть с группировками и префиксным инкрементом:

//         2     2    1
var_dump( $b + ($b + $b++) ); // 5
// 1 1 2
var_dump( $b + $b + ++$b ); // 4
// 1 1 2
var_dump( ($b + $b) + ++$b ); // 4
// 2 2 2
var_dump( $b + ($b + ++$b) ); // 6

В JS все будет более просто и предсказуемее: сначала подстановка, потом вычисление инкремента. В PHP же надо знать порядок группировки.

UPD

Alexander Karpan в комментарии к статье дал другой способ понять почему так происходит (спасибо за это!). ⇣⇣⇣ Если сдампить опкоды, то можно увидеть как происходят вычисления:

L8 : T7 = ADD CV1($b) CV1($b)
L9 : T8 = POST_INC CV1($b)
L10: T9 = ADD T7 T8

Итог

То что в JS не равно

var a = 1, b = 1;
(a + a++) !== (b + b + b++)

то в PHP еще как равно:

$a = 1; $b = 1;
($a + $a++) === ($b + $b + $b++)