Движок V8 JavaScript использует Irregexp, один из самых быстрых движков RegExp. Однако, чтобы максимально использовать эту мощь, разработчикам JS все еще необходимо избегать определенных ловушек, которые, к сожалению, не являются ни очевидными, ни хорошо документированными и могут тратить драгоценное время выполнения на встроенные функции RegExp, прежде чем когда-либо достигнут сгенерированный код сопоставления с образцом Irregexp.

Например, недавно я столкнулся с проблемой на трекере UglifyJS2, в которой разработчик обнаруживал ошибку корректности в старой версии Chrome. Сам исходный код казался достаточно невинным:

$ browserify test.js > bundle.js
$ uglifyjs bundle.js -c reduce_vars=true | node
[ 'fooBar' ]  // Wrong!
$ uglifyjs bundle.js -c reduce_vars=false | node
[ 'foo', 'Bar' ]  // This is what we expect.

Расследование привело к простой ошибке в медленном пути RegExp.prototype[@@split], которая была исправлена ​​несколько месяцев назад - проблема решена.

Либо это? Почему мы вообще идем медленным путем в этом тривиальном примере?

Возможно, вы не знаете об этом, но медленный путь может иметь огромное влияние на производительность. В частности, в случае @@split медленный путь может быть до 40 раз медленнее, чем быстрый.

Встроенные функции RegExp предъявляют несколько требований, которые необходимо выполнить для быстрого перехода. Рассмотрим regexp[@@split](string) (вызываемый внутри string.split(regexp)). Чтобы пойти по быстрому пути:

  • regexp не должно быть изменено (не должно быть добавленных, удаленных или измененных свойств)
  • regexp.prototype не должно быть изменено
  • regexp.lastIndex должен быть простым неотрицательным целым числом - не объектом, не получателем.

В нашем примере выше RegExp.prototype фактически изменяется, когда uglifyjs запускается с reduce_vars=true. Сама модификация выполняется с помощью es6.regexp.constructor polyfill Babel, что является плохой новостью, поскольку это может означать, что могут быть затронуты более крупные части Интернета.

Так почему вообще существует различие между медленным и быстрым путями?

Ответ - производительность. Чтобы быть эффективным, быстрый путь должен иметь возможность делать определенные предположения о форме объекта RegExp. Например, ему необходимо знать, что к lastIndex можно получить доступ по определенному смещению в области памяти объекта, и что доступ к lastIndex не имеет побочных эффектов (т. Е. Нет установленных геттеров, нет преобразований объекта в целое число) .

Используя эти предположения, быстрый путь может генерировать очень компактный код, превращая lastIndex доступ в простые загрузки / сохранения при определенных смещениях. Он также может пропускать ToLength(lastIndex) операций, поскольку lastIndex уже известно как неотрицательное целое число.

С другой стороны, медленный путь должен иметь возможность обрабатывать более общие формы:

Обратите внимание, что преобразование ToLength выше вызывает указанную пользователем функцию, которая может выполнять произвольные действия, включая изменение экземпляра RegExp во время работы встроенных функций (например, RegExp.prototype[@@split]).

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