Считываем hash: пуленепробиваемый способ

Original author: Lea Verou
  • Translation
  • Tutorial
Это, вероятно, одна из тех задач, о которой все думают, что знают решение, но многие решают её в итоге некорректно. Наткнувшись на ещё один сверхслабый кусок кода, написанный для этой цели, я задумала разъясняющую блогозапись.

Суть проблемы


Вы хотите убрать символ решётки (#) из значения location.hash. Например, когда hash равен "#foo", Вы хотите получить строку, содержащую "foo". Это же просто, правда?

Сложные случаи


Вот что большинство разработчиков, кажется, упускает из виду: в современных, мощно наджаваскриптованных, приложениях переменная hash может содержать любые юникодовые символы. Она не обязательно должна соответствовать значению реального атрибута id с той же страницы. А когда она и соответствует, атрибуты id теперь могут содержать почти любые юникодовые символы. Да ещё часто забывают, что на странице может и не быть никакого хэша. Даже если URL оканчивается символом «#», строка location.hash равняется на самом деле "" (пустой строке), а не "#".

Наивные подходы


Вот наиболее недавний — я нашла его в книге, на которую составляла техническую рецензию:

var hash = location.hash.match(/#(\w+)/)[1];

У него сразу несколько проблем:

  • Даёт неверный результат, когда hash содержит не латинский или не алфавитно-цифровой символ. Например, от хэша «#foo@o#bar$%huh hello» будет получено просто "foo".
     
  • Выбросит TypeError, если строка location.hash пуста — потому что .match() вернёт null.

Я видела другие варианты этого подхода, в том числе с употреблением явно заданных символьных классов вместо \w, с прибавлением начала строки (^) перед символом «#» (превосходная мысль, способствует производительности), и проверявшие, возвращает ли метод .match() что-нибудь, перед употреблением его результата. Однако же все они обычно были жертвою, по меньшей мере, одной из двух вышеупомянутых проблем.

Другой подход, который один мой друг использовал однажды, был вот каков:

var hash = location.hash.split('#')[1];

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

  • На том же тестовом примере будет получен хотя бы кусок "foo@o", что означает ошибку подхода только когда hash содержит символ «#».
     
  • Когда хэша нет, не выбрасывает ошибку, хотя всё же выдаёт undefined вместо пустой строки.

Получаем правильное значение


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

var hash = location.hash.substring(1);

Однако же поглядите попристальнее:

  • От нашего заковыристого тестового хэша он выдаёт правильный результат: "foo@o#bar$%huh hello"
     
  • Когда хэша нет, он правильно возвращает пустую строку.

«Но ведь он предполагает, что в начале строки решётка!» — я прям слышу, как воскликнут некоторые из вас. Ну, это стало бы настоящей заботою, кабы мы обрабатывали произвольную строку. Тогда пришлось бы проверить, символ «#» ли в её начале, да и существует ли эта строка вообще. Однако в случае с location.hash это не так только тогда, когда хэша нет. А этот вариант здесь учтён. ;)

Дополнение: как указывают в комментариях, также можно использовать location.hash.slice(1) вместо substring. Мне это даже больше нравится, потому что на 4 байта короче.

Если, однако же, вы одержимы регэксами и желаете решить задачу с их помощью во что бы то ни стало, то вот столь же пуленепробиваемый и почти столь же краткий способ:

var hash = location.hash.replace(/^#/, '');

Если по какой-то причине (ОКР?) вам хочется решить задачу при помощи .match() во что бы то ни стало, вы можете сделать вот что:

var match = location.hash.match(/^#?(.*)$/)[1];

В этом случае, так как символ «#» необязателен, .match() никогда не возвратит null. И нет, символ «#» никогда ошибочно не окажется частью возвращаемого хэша: так работают движки регэксов.

«Это слишком просто, чего я тут время зря трачу!»


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

«Эй, но и тут не всё в порядке!»


В этом случае хотелось бы знать, что я пропустила — так что, пожалуйста, оставьте комментарий! :)

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 26

    +2
    В старом Firefox (в районе 3.5) был баг. То ли "#", то ли "/", выдавался как обычно или закодированный (как в encodeURIComponent), в зависимости от способа получения: через полный href или hash: location.href.split('#').slice(1).join('#') или location.hash.replace(/^#/, '');

    Но сейчас это неактуально, наверное. Во всяком случае, в FF9 не воспроизводится.
    –28
    «нашла… видела… пропустила...»
    Мицгол, с вами все в порядке?..
      +24
      Вы делаете вид, что Вам не заметно, что это перевод и что автор первоисточника — женщина?
        +3
        перевод же
        +13
        От примеров волосы встали дыбом. Неужели кто-то умудряется такое писать? О_о
          +3
          Если Вы про значение хеша с уникодом и прочим, то да. В бизнес приложениях адресующих страницы через хеш такое, и не только, возможно.
            +4
            Нет, я про примеры способов отрезания диеза
            0
            Расстрою Вас: в этойнашей стране даже приговоры пишутся с грамматическими ошибками, а Вы тут про быдлокод…
            +3
            никогда бы не додумался первый символ регэкспами убирать.

            И помниться в какой-то опере хеш возвращался без решетки. так что приходилось делать так:

            var hash=location.hash;
            if(hash.indexOf('#')==0) hash=hash.substr(1);
              +2
              ах, забыл. та же опера (к сожалению версий не помню, давно это было) выдавала ошибку при работе с переменной hash, аргументируя это тем, что вообще-то это не строка, и неплохо было бы сделать объявление так:
              var hash=location.hash.toString();
              +5
              Читая первую половину статьи, думал что неужели я что-то не понимаю, и почему бы просто не сделать substr().
              Но нет, оказывается всё ок, а проблема просто высосана из пальца.
                0
                С хэшем всё понятно, а что делать с document.title?
                  +4
                  а с ним что не так?
                    0
                    Он тоже может состоять из любых символов, например.
                      +3
                      и что? любая строка может состоять из любых символов. но в нём же нет из браузера в браузер '#' в начале
                        +3
                        Сарказм же.
                          +5
                          до конца не уверен.
                  –4
                  а как по поводу?
                  /^#?([^#]+)/
                    0
                    От тестового хэша «#foo@o#bar$%huh hello» останется кусок «foo@o».

                    Незачёт.
                      0
                      /^#?([^#]+)(.*)/
                      Ну и брать $1 + $2
                        0
                        Ужé лучше.

                        Но слишком много возёхаться придётся.

                        Проще тогда сразу записать /^#?(.*)$/ что и предлагает Lea Verou.
                        +3
                        А вы уверены что такой хеш валиден?
                        Согласно w3c «the value of the hash attribute the value of this attribute MUST be the string concatenation of the hash mark(#) and the fragment identifier».
                        По rfc, на который опирается док (не, как ни странно не более новая версия):
                              fragment = *uric
                              uric          = reserved | unreserved | escaped
                              reserved      = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
                                              "$" | ","
                              unreserved    = alphanum | mark
                              mark          = "-" | "_" | "." | "!" | "~" | "*" | "'" |
                                              "(" | ")"
                        
                              escaped       = "%" hex hex
                              hex           = digit | "A" | "B" | "C" | "D" | "E" | "F" |
                                                      "a" | "b" | "c" | "d" | "e" | "f"
                        
                              alphanum      = alpha | digit
                              alpha         = lowalpha | upalpha
                        
                              lowalpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" |
                                         "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" |
                                         "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
                              upalpha  = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" |
                                         "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" |
                                         "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
                              digit    = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" |
                                         "8" | "9"
                        


                        Во-первых я не вижу здесь '#', а во-вторых — юникода.
                        Да, в w3c это лишь WD (уже 5 с лишним лет, а вы говорите чтоб проприетарщину не тащили без обсуждения!), но всё равно есть же еще rfc, которые являются стандартом.
                        Так что те кто тащит такие символы в хэш — ССЗБ.
                        Вот введёт хром/IE/FF полноценную валидацию урла, и порушится код.
                          +4
                          Этот стандарт говорит не о том, что идентификатор фрагмента не может содержать символ «#», а о том, что идентификатор фрагмента, когда записывается в URLе, обязан содержать этот символ в %-кодированном виде.

                          Что отнюдь не отменяет доступа к нему через DOM в сыром виде.

                          Вот пример:

                          <a href="#%74%65%73%74%31%23%74%65%73%74%32">test</a>

                          Вот итог интерпретации этого примера:

                          test

                          ↑ Нажимаем на эту гиперссылку, затем в консоли Файерфокса (Ctrl+Shift+K) смотрим значение location.hash и видим "#test1#test2".

                          Проверяем location.hash.match(/^#?([^#]+)/)[1] и видим "test1".

                          Проверяем location.hash.match(/^#?(.*)$/)[1] и видим "test1#test2".

                          Всё, эксперимент окончен.
                            +2
                            > Нажимаем на эту гиперссылку, затем в консоли Файерфокса (Ctrl+Shift+K) смотрим значение location.hash и видим "#test1#test2".

                            Нажимаю на эту гиперссылку, затем в консоли Хрома (Ctrl+Shift+I) смотрим значение location.hash и видим "#%74%65%73%74%31%23%74%65%73%74%32".
                            Эксперимент можно и не начинать, ибо сразу видно искажение стандарта FF.

                    Only users with full accounts can post comments. Log in, please.