Строковая интерполяция. Сказка-быль

    Постановка задачи


    Совершенно случайно я превратился из питониста в JS-разработчика, и на мою хрупкую детскую психику обрушился непосильный груз вещей, которых в JS нет. Например, нет удобного форматирования строк. На питоне можно написать:
    'hello, %(thing)s' % {'thing': 'world'}
    

    Или вот так:
    'hello, {thing}'.format(**{'thing': 'world'})
    



    Ближайший аналог в JS — конкатенация (operator +), которая очень плохо масштабируется с увеличением длины строки, да еще и выглядит безобразно до предела:
    '<div class="input-append"><input type="text" name="username" '+
    'id="signup_username" placeholder="'+placeholder+'"><input '+
    'type="hidden" name="password" value="'+generated+'"><button '+
    ...
    

    По возможности хотелось бы этого избежать.

    Jeremy Ashkenas, когда разрабатывал CoffeeScript, также обратил на эту особенность JS внимание, и случайно диалект PHP:
    "hello, #{document.cookie}"
    

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

    Мне в этой штуке нравится похожий на Ruby синтаксис, но не нравится все остальное, особенно выполнение произвольного кода внутри строки. Таким образом постановка задачи:
    – написать функцию
    – которая подставляет переменные в строку
    – загруженную из файла
    – не PHP

    Поиск решения


    Обычно в таких случаях используют готовые библиотеки, более того, в NPM по слову template находится более двух тысяч пакетов.

    В самом деле, mustache или lodash (underscore.js) работают превосходно, но… очень медленно: 10-20 мкс на одну подстановку. Не предел мечтаний ни в коем случае, особенно когда «продвинутый» функционал вроде циклов и фильтров совершенно не нужен.

    А конкатенация, хоть и выглядит страшно, как звериный оскал коллективизма, работает все-таки в 10-30 раз быстрее. Таким образом, мы добавляем к постановке задачи:
    – транслируется в конкатенацию
    – и работает очень быстро

    Вот теперь по этой спецификации можно изобретать велосипед. Because why not.

    Что получилось


    У меня получилась вот такая штука: Ruby-like simple string interpolation (GitHub)

    В ней 9 строк кода, и она выполняет миллион триста тысяч подстановок в секунду (около 0,77 мкс на подстановку) на той же машине, где mustache делает 130 тысяч, а lodash/underscore 45 тысяч подстановок в секунду.
    var hello = fmt('hello, #{thing}')
    hello({thing: 'world'})
    // -> hello, world
    

    Вывод: за счет отказа от сложных функций шаблонизатора (циклы, условные выражения) было достигнуто ускорение в 10-30 раз по сравнению с популярными библиотеками, не прибегая к выполнению произвольного кода в шаблоне.

    Rssi.js можно установить из npm очевидной командой npm install rssi, поддерживается также Bower (bower install rssi); на стороне клиента можно использовать AMD (RequireJS), а можно не использовать.



    Спасибо за прочитывание этого не очень связного текста! Пишите патчи, господа хорошие, и до новых встреч.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 48
    • +7
      Я добавлю свой вариант:

      var thing = 'world!';
      ['Hello ', thing].join("");
      
      • +11
        Тем, кому не понравился комментарий добавлю:

        String.prototype.format = function() {
        	var i = -1, args = arguments;
        	
        	return this.replace(/#\{(.*?)\}/g, function(one, two) {
        		return (typeof args[0] == 'object')?args[0][two]:args[++i];
        	});
        }
        
        var thing = 'world!';
        
        console.log(
        	'hello #{1} - #{2}'.format(thing, 'two!'),
        	'hello #{wrd} - #{2}'.format({'wrd': thing, '2': 'two!'})
        );
        
          • –1
            Конечно круто, но использовать я бы не стал из-за обилия регулярок, в основном они — самая медленная операция в функции.
            Кстати, в своём бы посте я бы вообще убрал регулярку. И сделал так:

            String.prototype.format = function() {
            	var i = 0, args = arguments, regular = /#\{(.*?)\}/g, func = function(one, two) {
            		return (typeof args[0] == 'object')?args[0][two]:args[i++];
            	}
            	
            	return this.replace(regular, func);
            }
            
            String.prototype.format2 = function() {
            	var i = 0
            		, k = 0
            		, last_k = 0 
            		, type = typeof arguments[0] == 'object'
            		, len = this.length
            		, open = false
            		, ret = '';
            	
            	for(;k < len;k++) {
            		if(this[k] == '#' && this[k+1] != undefined && this[k+1] == '{') {
            			last_k = open = k += 2;
            		}
            		else if(open && this[k] == '}') {
            			ret += type ? arguments[0][this.substring(last_k, k)] : arguments[i++];
            			open = false;
            		}
            		else if(!open) {
            			ret += this[k];
            		}
            	}		
            	return ret;
            }
            
            // https://github.com/lampaa/JSTF
            function RunTest(a,b,c){var d=console;d.time(c);for(var i=0;i<b;i++){a(i)}d.timeEnd(c)}
            	
            var thing = 'world!';
            	
            RunTest(function(cycleNum) {
            	'hello #{1} - #{2}'.format(thing, 'two!');
            	'hello #{wrd} - #{2}'.format({'wrd': thing, '2': 'two!'});
            }, 400000, '1');
            
            RunTest(function(cycleNum) {
            	'hello #{1} - #{2}'.format2(thing, 'two!');
            	'hello #{wrd} - #{2}'.format2({'wrd': thing, '2': 'two!'});
            }, 400000, '2');
            


            Хром:
            1: 4237.000ms
            2: 3662.000ms

            В лисе всё с точностью наборот.
            • +2
              А начиналось с 9 строк…
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Ну там много чего есть, циклы, функции.
          Для полноценных шаблонов (сайтов, например) я обычно mustache как раз и использую.
        • +19
          Совершенно случайно я превратился из питониста в JS-разработчика

          Как же это Вас угораздило? )
          • –1
            на одних бэкендах далеко не уедешь…
          • +3
            В CoffeeScript подстановка переменных в строку сделана аналогично Ruby, PHP тут не причем.
            • 0
              Имелся в виду доступ к глобальным объектам из шаблона, у меня почему-то первая ассоциация с php.
            • –2
              «hello, #{document.cookie}»

              Очень печально, что такая же хрень попадет и в ES6:(
              • +1
                И почему это печально?
                • +1
                  Потому что идеальный синтаксис для этого — как в python, а не как в уродском php.
              • +1
                Ещё в underscore.string есть sprintf:
                _.sprintf("%.1f", 1.17)
                "1.2"
                
                • 0
                  А вот это я бенчмаркнуть, конечно, забыл. Спасибо, добавлю.

                  Эх, была бы sprintf() нативной — все проблемы решились бы сами собой.
                • 0
                  Для любителей стрелять межконтинентальными баллистическими ракетами по воробьям: Vash.js

                  <p>How are you @model.name? Today is a sunny day on the planet Gunsmoke.</p>
                  
                  <ul class="@(model.active ? 'highlight' : '')">
                      @model.forEach(function(m){
                          <li>m.name</li>
                      })
                  </ul>
                  


                  У этой библиотеки, конечно, несколько иная область применения, но как синтаксический сахар все же интересно.

                  <offtop>
                  Думаю попробовать использовать в самопальной CMS для создания страниц с привязанными данными
                  </offtop>
                  • +2
                    Вам pull request.

                    Немного перемудрили с «JSON.stringify» — это лишнее; кеширование регекспа даст +5% к производительности; и не предавайте забвению точку с запятой, что может сыграть злую шутку в джаваскрипте;.
                    • 0
                      Спасибо за патч. Тесты запускаются (GNU) make. В данном случае они даже не запускаются.

                      Никакой злой шутки правильная расстановка точек с запятой в JS сыграть не может; в коде стоят все нужные точки с запятой.

                      Кеширование регулярки завтра попробую измерить, спасибо.
                      • +1
                        Ваш код не содержал ни одной ;, поэтому позволил себе напомнить) Все же, их использование является таким себе правилом хорошего тона.
                        Как злая шутка:
                        var b = 1, c = 1, a = b + c
                        (1+2).toString()

                        Тесты, каюсь, не запускал, правил онлайн. Падают скорее всего потому, что я изменил названия моих любимых переменных foo и bar. Попробую залить пачсет позже.

                        Кстати, вчера на заметил, но предпочтительнее использовать obj.hasOwnProperty("' + bar + '") для проверки наличия проперти.
                        • –2
                          «Правилом хорошего тона» является при работе с заимствованным кодом сохранять стиль кода. Про расстановку никчемных знаков препинания ничего не слышал, хотя, безусловно, можно писать и так:
                          ;;var a = 9;;;

                          Как я и писал выше, в коде расставлены все нужные точки с запятой. Ненужные, в полном соответствии со стандартом языка, не расставлены.
                          • +2
                            Это все Phyton виноват.

                            Хозяин — барин, но порекомендую habrahabr.ru/post/136860/

                            Тесты поправил, JSON.stringify вернул (увидел, что обрабатываете "\n" в строке. В этом случае — да, использование JSON.stringify оправдано, но не забывайте о старых ИЕ — им ничего неизвестно о JSON)
                            • –1
                              В статье по ссылке хорошо описаны несколько «злых шуткок», но, в тоже время, она оправдывает ";" в начале строк. На мой взгляд, это конечная стадия пути «расставляем везде -> расставляем нужные -> расставляем на всякий случай в начале». Читать такой код невозможно, надеюсь, и на ваш взгляд тоже.

                              Чтобы расставить точки с запятой над i. Я ставлю знаки везде в конце строк — это исключает вероятность синтаксических ошибок при минификации скрипта + считаю, что такой код удобнее читать + это удобная привычка для С-like языков, коих много.
                              • 0
                                Позволю себе процитировать статью, о которой мы говорим:
                                Пишите код, как хотите. Мне нет ни малейшего дела.
                                • 0
                                  Пожалуй, вряд ли мы сможем обсудить «вероятность синтаксических ошибок при минификации скрипта». Ошибочно считать, что кому-то есть дело до чужого кода.
                                  • 0
                                    А зачем обсуждать нулевую вероятность чего-либо? При минификации этого скрипта не происходит синтаксических ошибок, если не использовать сломанный минификатор. Google Closure, UglifyJS и другие распространенные программы превосходно справляются с валидным JS, как и все графические браузеры (даже, не к ночи будь помянут, MSIE6), Node.js и другие JS-окружения. Проверьте и убедитесь сами.

                                    Именно для этого существует стандарт языка.
                                    • 0
                                      если не использовать сломанный минификатор

                                      Неизвестно кто какой минификатор будет использовать для моего кода, поэтому мне не сложно обезопасить себя от недоразумений, даже если они и случались с нулевой вероятностью два раза.
                              • 0
                                Да, в старых браузерах JSON3.js или аналог нужно подключать отдельно.

                                Спасибо, я читал этот пост в оригинале пару лет назад. Более того, он полностью подтверждает мою позицию. Возможно, вы хотели запостить ссылку на какой-то другой, опровергающий пост.
                                • –1
                                  Статьей хотел пояснить, что такая позиция может привести к затрудненной читаемости кода того же npm. Потом уже понял, что это и был ваш style-guide. Nevermind)
                      • 0
                        Я просто оставлю это здесь: habrahabr.ru/post/99005/
                        • –1
                          К слову, говорить о том, что запись вида:

                          'hello, %(thing)s' % {'thing': 'world'}

                          лучше, чем:

                          'hello, ' + thing + 's'

                          можно лишь в случае загрузки строки из файла (или иного внешнего источника). В остальных случаях подобная запись — на любителя (например, кому-то она может не нравиться из-за необходимости выполнения дополнительных манипуляций глазами и мозгом для поиска соответствия между макросом и переменной при чтении кода).
                          • 0
                            … или в случае локализации строк.
                            • 0
                              Рискну не согласиться: когда переменных много, такая запись явно предпочтительнее.

                              Например, кошмарные конструкции вида ..."'+str+'"
                              Мне сложно представить, что кому-то активно нравится набирать эти хитросплетения кавычек (а это еще очень простой случай):

                              '<a href="'+url+'" class="btn btn-'+button_type+'" rel="goto-'+target+'">'+link_text+'</a>'


                              И то же самое по версии CoffeeScript, ошибиться просто негде:

                              "<a href='#{url}' class='btn btn-#{button_type}' rel='goto-#{target}'>#{link_text}</a>"
                              • 0
                                Мой комментарий был о том, что записывать переменную непосредственно в требуемое место строки проще, чем записывать в требуемое место строки сначала макрос, а потом, в конце строки, записывать уже переменную, которая будет заменять этот макрос (если только такое поведение не требуется специально). А в случае, когда переменных много, — особенно. Вы же сейчас привели просто два различных способа записи именно первого случая.

                                > «Мне сложно представить, что кому-то активно нравится набирать эти хитросплетения кавычек»
                                Мне, например, нравится. И мне не очень нравится наличие в языке целого зоопарка из различных способов записи выражения внутри строки (на мой взгляд, это лишь порождает дополнительный бесполезный выбор при написании кода).

                                > «И то же самое по версии CoffeeScript, ошибиться просто негде:»
                                Ошибиться можно, например, вот так (в коде две ошибки):
                                "<a href='#{url}' class='btn btn-# {button_type}' rel='goto-#{target}'>#{link_text)</a>"
                                • 0
                                  В редакторе с подсветкой кода ошибки достаточно заметны:
                                  "<a href='#{url}' class='btn btn-# {button_type}' rel='goto-#{target}'>#{link_text)</a>"
                                  

                                  Но да, безусловно, ошибиться можно.

                                  Про нравится / не нравится не стану спорить, мне просто не встречалась такая точка зрения.
                            • +1
                              Зацените кто-нибудь и мой шаблонизатор, пожалуйста:
                              github.com/StreetStrider/Beardy
                              • 0
                                Самая близкая по краткости запись этого выражения, наверное, такая:
                                'test #{foo} and #{bar}'.replace(/#{(\w+)}/g, function(s,m) {return {foo:42, bar:43}[m]});
                                
                                • +1
                                  Я думал, может сделать это так, как сделано в QString (из библиотеки Qt, C++). Тогда шаблонизация будет вида:

                                  // Plain one
                                  return "Hello, %1".arg("world!");
                                  
                                  // Complicated one
                                  return "Hello, #{world}".arg({'world' : 'world!'});
                                  


                                  Я не большой мастак JS, но насколько я знаю, можно добавить эту функцию в прототип String. А вообще, стоит ли запилить такую штуку?
                                  • 0
                                    '<div class="input-append"><input type="text" name="username" '+
                                    'id="signup_username" placeholder="'+placeholder+'"><input '+
                                    'type="hidden" name="password" value="'+generated+'"><button '+
                                    ...

                                    Хотелось бы услышать наилучшее решение этой проблемы. Решения, такие как загрузка внешних файлов (шаблонов), или, подключения библиотек, с весом в over100kb — не рассматриваю (не потому что это плохо, просто мне так не подходит).
                                    • 0
                                      Комментарий выше, например. 1 функция в String.prototype и вуаля! Если хотите, наклепаю
                                      • 0
                                        Имеется ввиду именно многострочность строки
                                        • 0
                                          Многострочность можно делать как в Си, это не очень хороший способ, но работает в современных браузерах и Node.js:
                                          str = "FFFFFFFFFFFF\
                                          FFFFFFFFFFFFUUUU\
                                          UUUUUUUUuuuuuuuu"
                                          

                                          Или использовать CoffeeScript, например.

                                          А почему загрузка файлов не подходит? Я обычно именно это делаю, когда есть куча html или другой фигни, которой в коде быть совершенно необязательно.
                                          • 0
                                            Даже не знаю, побаиваюсь почему-то. Фобия так скажем. Про \ знаю — не устраивает.
                                    • 0
                                      А всякие родные штуки вроде document.createElement разве не позволяют выставлять элементу атрибуты вроде id, class и data-что-нибудь?
                                      Я совсем не мастак в JS, но интересуюсь.
                                      • 0
                                        Тогда колбаса вроде
                                        '<input type="text" name="username" id="signup_username" placeholder="'+placeholder+'">'
                                        

                                        быстро превратится в понятный набор функций вроде
                                        document.createElement("input").attr("name", "username").attr("id", "signup_username").data("placeholder", placeholder);
                                        

                                        Тут я, похоже, вспомнил, что когда-то точил костыли на jQuery.
                                        Впрочем, если уж речь зашла о шаблонизаторах, я довольно давно пишу на Jade и радуюсь:
                                        input#signup_username(name="username", data-placeholder=placeholder)
                                           input(type="hidden", name="password", value=generated)
                                        
                                        • +1
                                          Шаблонизация это зачастую DOM, но не всегда. Шаблонизатор должен решать обобщённую задачу генерации строки по шаблону. Можно генерить SVG и прочие XMLи, можно генерить таблицы стилей, можно генерить вообще отчёты в текстовом файле. И всё это не конструируя DOM.
                                          • 0
                                            Ага, понял. Ну тогда свои функции писать только, костыли-костылики.
                                      • –1
                                        olado.github.io/doT/index.html я тоже люблю велосипеды, но поиском все же пытаюсь пльзоваться

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