Почему WAT

http://blog.caplin.com/2012/01/27/the-why-of-wat/
  • Перевод
Недавний доклад Гарри Бернхардта на CodeMash оказался довольно популярен (прим. пер.: в том числе и на хабре). В докладе он шутит по поводу некоторых особенностей поведения ruby и javascript.
Вряд ли я смогу вас убедить, что есть какой-то смысл в том, на что жалуется Гарри. Тем не менее, надеюсь, я смогу вам объяснить, почему javascript ведёт себя именно так.

Символ +


Символ + в javascript может обозначать три вещи.

Это может быть бинарный оператор сложения, применяющийся к двум числам.
Это может быть бинарный оператор сложения строк, применяющийся к двум строкам.
Это может быть унарный оператор «это число положительное», применяющийся к одному числу.

Всё немного усложняет автоматическое приведение типов в javascript. Например, если перед или после символа + находятся не строки или числа, javascript автоматически преобразует операнды либо к строкам, либо к числам.

boolean, null, undefined преобразуются к числам (true = 1, false = 0, null = 0, undefined = NaN)
объекты, массивы, функции преобразуются к строкам.

Так что теперь, когда вы видите
[] + []

вы понимаете, что javascript преобразует оба пустых массива к строкам и сложит получившиеся две строки.
Когда массив преобразуется в строку, вы получаете строку, содержащую список его элементов через запятую. Поэтому для пустого массива мы получаем пустую строку.

[] + {}

В этом случае пустой объект справа также будет преобразован в строку. Так как у него не реализован метод toString, в соответствии с наследованием, будет использован метод toString Object-а, который возвращает строку "[object Object]" для объектов. Поэтому в данном примере мы складываем пустую строку со строкой "[object Object]".

Пустые блоки кода


Теперь рассмотрим странности парсера javascript. Вы, возможно, заметили, что мы используем фигурные скобки в javascript для двух различных целей. Чтобы обозначить блоки кода (например, после if или как часть определения функции) и чтобы записывать объекты в объектной нотации (её ещё называют JSON). В некоторых языках программирования блоки кода могут использоваться во многих местах внутри существующего кода, чтобы определить новую область видимости. Javascript (пока что) не использует область видимости на уровне блоков кода, тем не менее, он позволяет использовать блоки кода внутри кода. А значит, иногда, когда вы пишете {}, парсер javascript воспринимает это как пустой блок кода, а иногда — как пустой объект.
Вообще говоря, если фигурная скобка находится в начале выражения, она будет интерпретирована как начало блока кода. Вы можете заставить парсер javascript обработать её как объект, обрамив в обычные скобки.

{} + []

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

Дополнительная уловка заключается в том, что javascript приводит объект, функцию или массив к числу, предварительно приводя их к строке. Пустой массив приводится к нулю. Вот, например:

+ [] === 0 /* потому что пустой массив становится пустой строкой "", которая становится 0 */
+ [1] === 1 /* потому что массив с единицей становится строкой "1", которая становится  1 */
isNaN(+[1, 2] ) /* потому что массив, содержащий 1, 2, становится строкой "1,2", которая не является числом */
+({toString: function() {return "3"}}) === 3

Итак, пустой объект приводится к строке "[object Object]", которая приводится к NaN, когда ей из-за префиксного оператора + приходится преобразовываться в число.
{} + {}


Символ −


В отличие от +, символ − довольно прост. У него всего два значения: бинарный оператор, применяющийся к двум числам, и префиксный оператор, применяющийся к одному числу. Как вы уже знаете, строки, которые не представляют собой числа, приводятся к NaN, если они должны быть использованы как числа. Поэтому вас не должно удивлять, что
isNaN("wat" - 1) // true


Почему это важно?


Лишь некоторые из обозначенных моментов могут встретиться и встречаются на практике. Давным-давно, когда люди использовали eval, чтобы парсить JSON, им приходилось оборачивать свой JSON в скобки, чтобы парсер случайно не воспринял его как блок кода. Часто важно помнить о типе переменных и преобразовывать ваши строки в числа, потому что иначе люди будут удивлены, увидев 3 + 1 = 31. Знание случаев, когда происходит приведение типов, сделает большое количество правил по поводу == false гораздо менее запутанными. И, самое главное, это позволит вам находиться среди слушателей доклада «javascript сошёл с ума» и вести себя так, как будто всё происходящее для вас совершенно логично (а таких, похоже, было мало).
Поделиться публикацией

Похожие публикации

Комментарии 68

    +13
    Когда человек наконец-то узнает все тайны Вселенной, он перестанет смеяться.
      +2
      не смешно
        +2
        Это и не должно вызывать смех, это глубокая мысль, ну или просто мысль.
          +1
          > он перестанет смеяться.
          > не смешно

          Улавливаете иронию?
          • НЛО прилетело и опубликовало эту надпись здесь
              +2
              Ваш ответ 42?
                +2
                Сложные же у вас шутки…
          +14
          WATMAN!
            0
            так что же такое WAT? — не нашла нигде расшифровки ;-)
              0
              what?
            +19
            Доклад не звучал, как недоумение «как такое может быть?!». Автор доклада понимал каждое действие, которое он делал и понимал, что должно произойти. Суть доклада — показать, что эти моменты — это баг. А вы их объяснили, и сделали из них фичу.
            Но, это не то поведение, которое ожидает увидеть программист в нормальном стандартном языке программирования. По этому, это все же баг.
            Не ради холивара: сами гуру джаваскрипта понимают, что язык не очень выразителен и логичен, но альтернатив пока нету (CoffeeScript не считается).
              +9
              Сорри. Не доглядел, что это перевод, а не ваша статья.
                +1
                Не ради холивара, но что значит «не очень выразителен и логичен»?
                  +6
                  > эти моменты — это баг
                  > это не то поведение, которое ожидает увидеть программист в нормальном стандартном языке программирования

                  А какого поведения ожидали вы? Что язык с мягкой типизацией и перебитым оператором "+" (с приведением к строке, как типу по умолчанию) выкинет исключение? Свалится с ошибкой? Остановит компиляцию (_ирония_)?

                  Серьёзно, лично вы какого поведения бы ожидали?
                    +5
                    Лично я ожидал бы как минимум того, что если typeof(a) == typeof(b) == Array, то a + b == a.append(b). И вообще, на мой взгляд, невозможность оверлоадить операторы в JavaScript — полнейшее недоразумение.
                      0
                      Какую-то глупость сказал. Я имел в виду a + b == [a[0], a[1], ..., a[a.length], b[0], b[1], ..., b[b.length]]
                        +2
                        Было бы правильней, по-моему,
                        a + b == [a[0], a[1], ..., a[a.length-1], b[0], b[1], ..., b[b.length-1]]

                          +1
                          Ааааргх! Простите, я сегодня невнимательный :(
                      • НЛО прилетело и опубликовало эту надпись здесь
                          +3
                          > И вообще, на мой взгляд, невозможность оверлоадить операторы в JavaScript — полнейшее недоразумение.

                          А на мой взгляд нормально. Не хватало ещё чтобы те кучи кода, которые Вы подключаете к проекту, могли перегружать такие стандартные операторы. Хватает изменения глобальных объектов и прототипов. «Счастливой отладки» будет в лучшем виде.
                          • НЛО прилетело и опубликовало эту надпись здесь
                              +1
                              Суперссылка! Но ниндзюцу — это не путь самурая.
                          +2
                          Мои ожидания:
                          [] + [] = []
                          [] + {} = undefined (или какой-то еще объект, который укажет, что произошла нестандартная операци; или поднять исключение)
                          {} + [] = [] + {}
                          a + b = b + a (!!!!!!!!!)
                          {} + {} = {}
                            0
                            Да, согласен {} + [] != [] + {} — совсем не очевидно
                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                конкатенация чего? Есть конкатенация строк: строка + строка, а тут конкатенация `яблоко` + `волнистый попугайчик`. Что ты ожидаешь получить в результате? `яблоко` + `волнистый попугайчик` =?
                                +3
                                (a + b) даже в C не всегда равно (b + a)
                                  0
                                  a + b = b + a (!!!!!!!!!)

                                  странные ожидания.

                                  a = 'foo';
                                  b = 'bar';
                                  
                                  a+b == 'foobar';
                                  b+a == 'barfoo';
                                  


                                  И с какого хрена они должны быть равными?
                                    0
                                    > {} + []
                                    0
                                    > [] + {}
                                    "[object Object]"
                                    


                                    Да, все очевидно и ожидаемо, не правда ли? С какого хрена яблоко + шум дождя = демократия? Как можно проводить операции над разными типами?
                                    Где еще кроме строк должна не работать коммутативность?
                                0
                                Описанные курьезы нельзя считать багами, хотя бы потому, что они все известны и задокументированы. Это именно «особенности» джаваскрипта, которые (понятное дело) смущают всех тех, кто не изучал документации. А таких большинство, я в том числе.
                                +9
                                Не очень понял смысл поста (вижу, что перевод).
                                Не думаю, что кто-то из программистов сомневался, что поведение jsc-интерпретатора, продемонстрированное на видео, противоречит спецификации языка. Естественно, результат логичен в том плане, что его можно без сложностей объяснить, последовательно рассмотрев каждую операцию.
                                Дело в другом, в человеческой логике.

                                Сложение массивов через оператор (+) — занятие сомнительное, но если язык это позволяет, то логично, что программист ожидает получить на выходе массив, элементами которого будут суммы элементов массивов (в соот. последовательности). Ну или null, если так делать нельзя. Или пустой массив, на худой конец.
                                Пустая строка в результате — это нелогично ни разу. Хотя и по спецификации.

                                И так далее.

                                Понятно, что специалиста, который уже привык пользоваться своим инструментом, привык к подобному «нормальному поведению», такими фокусами не удивить. Более того, он может использовать эти фокусы в реальном проекте, чтобы делать что-то быстрее и проще. Никто не спорит. Но продуманности и логичности языку это не прибавляет.
                                • НЛО прилетело и опубликовало эту надпись здесь
                                  –4
                                  «Человеческую логику», в той форме, в которой Вы ее описали, принято называть «интуицией». Программист должен «не надеяться и верить», а «знать». И из этого знания получать логически стройную программу.
                                    +5
                                    Не согласен с Вами — язык должен ориентироваться на обычную логику человека, и дела не втом, что должен программист, а что не должен.
                                    Программист — прежде всего — человек.
                                    Можно и define true false сделать — и описать это в стандарте языка — это не протеворечит ничему.
                                    НО! Это противоречит ожидаемому поведению — отсюда — буду возникать ошибки.

                                    Хороший язык с моей точки зрения:
                                    1) ведет ожидаемо с точки зрения человека, ибо написан он прежде всего для него
                                    2) ограничивает возможность совершить ошибку

                                    Отрекшись от знаний JS подумайте просто чисто из ощущений — если вы складываете два массива что вы должны получить? cтроку?

                                    Язык должен помогать человеку, ведь он для него и написан.
                                    Мне очень нравится JS и он все больше стремится к идеальному языку, и надеюсь эти грабли в конце концов уберут как было с with
                                      +3
                                      Даже естественные языки, несмотря на свою многовековую историю, не очень-то логичны с точки зрения человека. Попробуйте учить китайский или чеченский. Вам родной язык кажется логичным потому, что Вы его зубрили с пеленок. К примеру, почему «стол» — это он, а «парта» — это она? Вам это кажется логичным. А вот очень многим людям чуждо даже само понятие грамматического рода.
                                      Вы можете возразить, что искусственные языки изобретались целенаправленно и лишены логических изъянов естественных языков. И так должно быть и с языками программирования. Но почему тогда почти никто на искуственных языках не говорит? Да потому, что они «кажутся» людям не логичными.

                                      Пример из жизни. Если я складываю два сырых яйца, что я должен получить? Ожидается, что два яйца. А если с силой? Да друг об друга?

                                      Кстати, в опытных руках грабли являются не инструментом самоубийства, а полезным сельхозорудием: с помощью with можно было эмулировать let.
                                        +3
                                        Ну во-первых складывая два яйца независмо от способа вы получите всетеже два яйца, просто складывая их с силой вы получите два БИТЫХ яйца ) Сущность у Вас не поменялась, поменялось состояние.

                                        Во вторых, логика в программистких языках математическая и никак уж не ассоциируется с обычными языками. В математике говорится — если Вы складываете два слона на выходе вы должны получить ту же сущность. И это одинаково что для математика из Южной Гвинеи, что для Эскимоса, не находите?

                                        По поводу with — его убрали из-за неочивидности поведения, да им можно было кое-что и полезное делать, но ИМХО — правильно что убрали. Хотите скоп — вводите скоп, а не странную функцию с неоднозначным поведением. Ошибок можно было наплодить с ним, даже зная все его особенности.
                                          –2
                                          Вы не находите, что я говорю именно об этом? Если «логика в программистких языках математическая», то нечего к ней подходить с житейской точки зрения. Если у Вас изначально неверная посылка «во всех языках массив плюс массив дает массив», то нечего удивляться, что и заключение «в JavaScript массив плюс массив должен давать массив» получается неверное.
                                            +2
                                            Математическая логика про массивы ничего не знает, она лишь говорит что при сложении сущностей результатом будет такая же сущность. Это вполне интуитивно, не так ли?

                                            Тут не причем житейская логика.

                                            И да, язык ИМЕЕТ право на то, что массив плюс массив дает строку, но это поведение отличается от ожидаемого.

                                            Он ИМЕЕТ на это право, но если бы он делал это ОЖИДАЕМО — количество бы ошибок УМЕНЬШИЛОСЬ — вот что я имел ввиду.
                                            • НЛО прилетело и опубликовало эту надпись здесь
                                      +1
                                      Вы должны знать, что отвечать надо с помощью ссылки «ответить» под комментарием. А я вот проинтуичил, что ваш комментарий — это ответ на мой, хотя тред другой. Интуиция — это иногда даже удобно, правда?:)

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

                                      Курс «алгоритмы и структуры данных» независим от языка, на котором будут написаны примеры, потому что абстракция массива проста и понятна, и едина (должна быть) для всех инструментом, использующих эту абстракцию. Если математик складывает массивы, он удивится строке в результате, это WAT. Не больше и не меньше. А спецы пусть мучаются или радуются — их дело. :) Если уж говорить о JS, то мне нравится, что все — строка, типизации нет и т.д., с этим проще работать (до определенного уровня сложности систем, конечно), но над докладом я посмеялся.
                                      –1
                                      После этого поста у меня сложилось впечатление о полном отсутствии юмора у JS программистов. Всякие вещи на подобии []+[]='' и {}+[] != []+{} ведь ни разу не логичны с точки зрения математики и могут свернуть мозг набок. А для тех, у кого wat-пост вызвал бурю негодования — Ну пошутили и будет вам, чего так напрягаться то?
                                        0
                                        Рискую быть сильно заминусованным, но все же спрошу:
                                        В топике упомянут eval для парса JSON…
                                        А какой метод парса JSON, полученного через AJAX, является расово верным, и почему?
                                        Точнее, что плохого в eval?
                                        Нет, eval конечно небезопасен, но если данные приходят от моего же сервера, в котором я абсолютно уверен, в чем может быть проблема?
                                        Ладно бы в JS существовал кроссбраузерный вариант нативного парса JSON, так нет же, для некоторых браузеров нужны костыли. Не совсем понимаю, в чем целесообразность отказа от eval?
                                          +3
                                          эм.
                                          var jsonObject = JSON.parse("json string");
                                          http://www.json.org/js.html
                                            –1
                                            Но это кастомный парсер, а не нативная функция. В чем смысл использования ее вместо нативного eval?
                                              +3
                                              1. Это нативная функция, начиная с 5-ой версии стандарта.
                                              2. Смысл в безопасности.
                                                +1
                                                Ну eval плох только тем что не безопасен.
                                                JSON parser жертвуя скоростью, дает уверенность в безопасности — т.е. он только преобразует валидный json в объект, и не может выполнить произвольный код, в отличает от eval.
                                                  +1
                                                  JSON.parse() — это «нативная функция» (часть языка) по стандарту ECMAScript 5 и в более новых. То есть начиная с декабря 2009 года.
                                                    –1
                                                    Да, я знаю об этом. Но как всегда, есть особенный браузер… :D Который не в курсе.

                                                    А все же, как может проявиться «небезопасность» eval'а при условии парса таким образом ответа от своего же сервера?
                                                      +1
                                                      Почему вы уверены, что получаете ответ именно от своего сервера?
                                                      Почему вы уверены, что ответ от вашего сервера не может содержать атакующего кода?
                                                      Код исполняется на клиенте и подвержен разным видам атаки.
                                                        0
                                                        Я полагал, что кроссдоменные запросы по умолчанию не работают.
                                                        А если есть возможность подменить произвольный ответ от сервера, то можно сразу исходную страницу и подменить, зачем мучиться с подменой AJAX?
                                                        Аналогично с вариантом атакующего кода от сервера.
                                                        Впрочем, я думаю, что продолжать дискуссию смысла нет. Простое любопытство тут карается :(
                                                          0
                                                          Это нервные понабежали. )

                                                          По поводу атак недавно на хабре проскакивало несколько статей про XSS. Очень показательно. :)
                                                            0
                                                            Видимо да :) Даже кое-куда нагадить успели )
                                                            Я ж для самообразования спрашиваю, разобраться хочу. Для себя. Гуру JS себя не считаю, и eval не хвалю. Но понять хочу, а не слепо принять на веру «eval плохо».

                                                            Вообще да, есть в этом резон. Использование eval потенциально дает новые XSS. Однако, если формирование JSON на сервере автоматизировано (json_encode, например), то все спецсимволы будут экранированы, поэтому я не понимаю, где тут может быть проблема.
                                                            За исключением варианта скомпрометированного сервера или канала общения с сервером, но в этом случае, как я писал выше, разницы нет — JSON это будет или исходная страница.
                                                              +1
                                                              Если не делать ошибок, все будет хорошо, угу. Но это редко случается с девелоперами. :)
                                                              Используя JSON.parse(), можно сделать свое приложение не менее безопасным, чем с eval(). Как правило, более безопасным. А цена — небольшая.
                                                                +2
                                                                Да, пожалуй вы правы. парс ответа сервера — не то место, где следует экономить. Не настолько часто это происходит.
                                                                Даже если разница в безопасности минимальна, оно того стоит.

                                                                Спасибо за ответы :)
                                              +5
                                              Пост следовало бы назвать «Оправдания интерпретатора».

                                                0
                                                Как говорил Джоэл, когда вы видите i=a+b, вы можете рассчитывать только на себя, парни )
                                                Конечно, динамическая типизация способна привести к неочевидным результатам. Но, с другой стороны, более-менее опытный человек знает тонкости языка с которым он работает, а неопытный довольно быстро их поймет. Если захочет.
                                                Ну и именования типа sI = sA + sB жизнь немного облегчают.
                                                  +1
                                                  А я рубист, но ролик про WAT посмотрел со смехом и радостью. И мне ни разу не захотелось бежать строчить посты о том, что Руби тут не виноват и как–то оправдывать его в глазах общественности. Видео — просто юмор, к которому нужно относиться как к юмору.

                                                  Вот еще один WAT из Руби, который есть даже в самой последней версии:

                                                  1.9.3p0 :001 > a = []
                                                  => []
                                                  1.9.3p0 :002 > a[0] = a
                                                  => [[...]]
                                                  1.9.3p0 :003 > a
                                                  => [[...]]
                                                  1.9.3p0 :004 > a[0]
                                                  => [[...]]


                                                  И ничего, живем же как–то.
                                                    0
                                                    А что в этом примере не логично?
                                                      0
                                                      Как минимум многоточие и факт того, что так можно делать. Вон в Питоне в примере ниже подобное, но лишь отчасти.
                                                      0
                                                      Python

                                                      >>> a = [1,2]
                                                      >>> a
                                                      [1, 2]
                                                      >>> a[0] = a
                                                      >>> a
                                                      [[...], 2]
                                                      >>> a[1] = a
                                                      >>> a
                                                      [[...], [...]]
                                                      >>> a[0]
                                                      [[...], [...]]
                                                      >>> a[0][1]
                                                      [[...], [...]]
                                                        0
                                                        Ну и… что не оффтоп:
                                                        >>> a + a
                                                        [[[...], [...]], [[...], [...]], [[...], [...]], [[...], [...]]]
                                                      +1
                                                      Я обожаю JS.
                                                      • НЛО прилетело и опубликовало эту надпись здесь
                                                        • НЛО прилетело и опубликовало эту надпись здесь
                                                          +1
                                                          boolean, null, undefined преобразуются к числам (true = 1, false = 0, null = 0, undefined = NaN)
                                                          объекты, массивы, функции преобразуются к строкам.

                                                          А объект Date с унарными плюсом возвращает число (через valueOf), но при сложении с числами — всё-таки строку.

                                                          Вот эта таблица неплохо всё иллюстрирует:
                                                          docstore.mik.ua/orelly/webprog/jscript/ch11_01.htm#jscript4-CHP-11-SECT-1
                                                            0
                                                            Ребята, подскажите, пожалуйста. Вот {} + {} будет равно NaN.
                                                            Но вот при таком коде:
                                                            var a = {};
                                                            var b = {};
                                                            выражение a + b будет равно [object Object][object Object]
                                                            Почему так?
                                                              0
                                                              если фигурная скобка находится в начале выражения, она будет интерпретирована как начало блока кода

                                                              Очевидно, в случае с переменными такая порблема не возникает.
                                                              Сравните ({}) + {} и {} + {}.
                                                                0
                                                                Большое спасибо. Надо было внимательнее читать, конечно. Но гениально вообще!

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

                                                            Самое читаемое