Статьи

Google Closure: как не писать JavaScript

На прошлой неделе на конференции Edge of the Web в Перте я встретился с Дмитрием Барановским , создателем библиотек JavaScript Raphaël и gRaphaël . Возможно, самая важная вещь, которую делают эти библиотеки, — это создание сложной векторной графики в Internet Explorer, где производительность JavaScript относительно низкая. Поэтому у Дмитрия мало терпения к плохо написанному JavaScript, как к коду, который он обнаружил в только что выпущенной Google библиотеке закрытия .

Прочитав на конференции доклад о том, как написать свою собственную библиотеку JavaScript ( подробные заметки ), Дмитрий поделился своими мыслями о новой библиотеке за завтраком на следующее утро. «Как раз то, что нужно миру — еще одна отстойная библиотека JavaScript», — сказал он. Когда я спросил его, что делает его «отстойным», он уточнил. «Это библиотека JavaScript, написанная разработчиками Java, которые явно не получают JavaScript».

В течение оставшейся части дня всем, кто слушал, Дмитрий приводил пример за примером ужасного кода, который он обнаружил, когда копался в Closure. Его самый большой страх, сказал он мне, заключается в том, что люди будут переходить от действительно превосходных библиотек JavaScript, таких как jQuery, к Closure, основываясь на имени Google.

«Я заключу с тобой сделку», — сказал я ему. «Пришлите мне несколько примеров этого ужасного кода, и я опубликую его на SitePoint».

Медленная петля

Из array.js , строка 63:

for (var i = fromIndex; i < arr.length; i++) { 

Этот цикл for ищет свойство .length массива ( arr ) каждый раз в цикле. Просто установив переменную для хранения этого числа в начале цикла, вы можете сделать цикл намного быстрее:

 for (var i = fromIndex, ii = arr.length; i < ii; i++) { 

Разработчики Google, похоже, выяснили этот трюк позже в том же файле. Из array.js , строка 153:

 var l = arr.length;  // must be fixed during loop... see docs ⋮ for (var i = l - 1; i >= 0; --i) { 

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

 var i = arr.length; ⋮ while (i--) { 

Но не все проблемы производительности Closure Library связаны с плохо оптимизированными циклами. Из dom.js , строка 797:

 switch (node.tagName) { case goog.dom.TagName.APPLET: case goog.dom.TagName.AREA: case goog.dom.TagName.BR: case goog.dom.TagName.COL: case goog.dom.TagName.FRAME: case goog.dom.TagName.HR: case goog.dom.TagName.IMG: case goog.dom.TagName.INPUT: case goog.dom.TagName.IFRAME: case goog.dom.TagName.ISINDEX: case goog.dom.TagName.LINK: case goog.dom.TagName.NOFRAMES: case goog.dom.TagName.NOSCRIPT: case goog.dom.TagName.META: case goog.dom.TagName.OBJECT: case goog.dom.TagName.PARAM: case goog.dom.TagName.SCRIPT: case goog.dom.TagName.STYLE: return false; } return true; 

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

Опытные разработчики JavaScript знают, что гораздо быстрее создать объект для инкапсуляции этой логики:

 var takesChildren = {} takesChildren[goog.dom.TagName.APPLET] = 1; takesChildren[goog.dom.TagName.AREA] = 1; ⋮ 

Когда этот объект настроен, функция проверки, принимает ли тег дочерние элементы, может выполняться намного быстрее:

 return !takesChildren[node.tagName]; 

Этот код может быть дополнительно hasOwnProperty от внешних помех с помощью hasOwnProperty (полное объяснение этого см. Ниже).

 return !takesChildren.hasOwnProperty(node.tagName); 

Если есть одна вещь, которую мы ожидаем от Google, это фокус на производительности. Черт, Google выпустил свой собственный браузер Google Chrome, прежде всего, чтобы поднять производительность JavaScript на новый уровень!

Видя такой код, нужно задаться вопросом, могла ли Google достичь того же, научив своих инженеров писать лучший код JavaScript.

Шесть месяцев в протекающей лодке

Было бы несправедливо предположить, что Google проигнорировал производительность в создании Closure. Фактически, библиотека предоставляет общий метод для кэширования результатов функций, которые работают медленно, но которые всегда будут возвращать один и тот же результат для данного набора аргументов. Из memoize.js , строка 39:

 goog.memoize = function(f, opt_serializer) { var functionHash = goog.getHashCode(f); var serializer = opt_serializer || goog.memoize.simpleSerializer; return function() { // Maps the serialized list of args to the corresponding return value. var cache = this[goog.memoize.CACHE_PROPERTY_]; if (!cache) { cache = this[goog.memoize.CACHE_PROPERTY_] = {}; } var key = serializer(functionHash, arguments); if (!(key in cache)) { cache[key] = f.apply(this, arguments); } return cache[key]; }; }; 

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

Используемый для кэширования результатов функции, например, на основе координат указателя мыши, объем памяти этого кода будет быстро выходить из-под контроля и замедлять браузер для сканирования.

По словам Дмитрия, «я не уверен, как этот шаблон называется в Java, но в JavaScript он называется« утечка памяти »».

Код в вакууме

В своем выступлении по созданию библиотек JavaScript Дмитрий сравнил глобальный охват JavaScript с общественным туалетом. «Вы не можете не попасть туда», — сказал он. «Но старайтесь ограничить свой контакт с поверхностями, когда вы делаете».

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

Из object.js , строка 31:

 goog.object.forEach = function(obj, f, opt_obj) { for (var key in obj) { f.call(opt_obj, obj[key], key, obj); } }; 

Такие циклы for in по своей природе опасны в библиотеках JavaScript, потому что вы никогда не знаете, какой другой код JavaScript может выполняться на странице, и что он мог бы добавить к стандартному Object.prototype JavaScript.

Object.prototype — это объект JavaScript, который содержит свойства, общие для всех объектов JavaScript. Добавьте новую функцию в Object.prototype , и к каждому объекту JavaScript, запущенному на странице, будет добавлена ​​эта функция — даже если она была создана заранее! Ранние библиотеки JavaScript, такие как Prototype, делали большие добавления удобных функций в Object.prototype .

К сожалению, в отличие от встроенных свойств, предоставляемых Object.prototype , пользовательские свойства, добавленные в Object.prototype будут отображаться как свойство объекта в любом цикле for-in на странице.

Короче говоря, библиотека Closure не может сосуществовать с любым кодом JavaScript, который добавляет функции в Object.prototype .

Google мог бы сделать свой код более надежным, используя hasOwnProperty для проверки каждого элемента in цикле for in чтобы убедиться, что он принадлежит самому объекту:

 goog.object.forEach = function(obj, f, opt_obj) { for (var key in obj) { if (obj.hasOwnProperty(key)) { f.call(opt_obj, obj[key], key, obj); } } }; 

Вот еще один особенно хрупкий кусочек библиотеки закрытия. Из base.js , строка 677:

 goog.isDef = function(val) { return val !== undefined; }; 

Эта функция проверяет, имеет ли определенная переменная определенное значение. Или так, если только сторонний скрипт не устанавливает глобальную undefined переменную в другое значение. Эта единственная строка кода в любом месте страницы приведет к сбою библиотеки Closure:

 var undefined = 5; 

Опора на глобальную undefined переменную является еще одной ошибкой новичка для авторов библиотеки JavaScript.

Вы можете подумать, что любой, кто присваивает значение undefined заслуживает того, что получает, но исправление в этом случае тривиально: просто объявите локальную undefined переменную для использования внутри функции!

 goog.isDef = function(val) { var undefined; return val !== undefined; }; 

Типичная путаница

Одним из наиболее запутанных аспектов JavaScript для разработчиков, работающих на других языках, является его система типов данных. Библиотека Closure содержит множество блоков, которые еще больше показывают, что ее авторам не хватает обширного опыта работы с тонкостями JavaScript.

Из string.js , строка 97:

 // We cast to String in case an argument is a Function. … var replacement = String(arguments[i]).replace(…); 

Этот код преобразует arguments[i] в строковый объект, используя функцию преобразования String . Это, возможно, самый медленный способ выполнить такое преобразование, хотя он будет наиболее очевидным для многих разработчиков из других языков.

Намного быстрее добавить пустую строку ( "" ) к значению, которое вы хотите преобразовать:

 var replacement = (arguments[i] + "").replace(…); 

Вот еще одна путаница с типом строк. Из base.js , строка 742:

 goog.isString = function(val) { return typeof val == 'string'; }; 

JavaScript на самом деле представляет текстовые строки двумя различными способами — в качестве примитивных строковых значений и в виде строковых объектов:

 var a = "I am a string!"; alert(typeof a); // Will output "string" var b = new String("I am also a string!"); alert(typeof b); // Will output "object" 

Большинство строк времени эффективно представлены в виде примитивных значений ( a выше), но для вызова любого из встроенных методов в строке (например, toLowerCase ) его сначала необходимо преобразовать в строковый объект ( b выше). JavaScript конвертирует строки назад и вперед между этими двумя представлениями автоматически по мере необходимости. Эта функция называется «автобокс» и появляется на многих других языках.

К сожалению, для опытных разработчиков Google, Java только когда-либо представляет строки как объекты. Это мое лучшее предположение, почему Closure Library пропускает второй тип строки в JavaScript:

 var b = new String("I am also a string!"); alert(goog.isString(b)); // Will output FALSE 

Вот еще один пример смешения типов в стиле Java. Из color.js , строка 633:

 return [ Math.round(factor * rgb1[0] + (1.0 - factor) * rgb2[0]), Math.round(factor * rgb1[1] + (1.0 - factor) * rgb2[1]), Math.round(factor * rgb1[2] + (1.0 - factor) * rgb2[2]) ]; 

Эти 1.0 с говорят. Такие языки, как Java, представляют целые числа ( 1 ) в отличие от чисел с плавающей запятой ( 1.0 ). В JavaScript, однако, числа являются числами. (1 - factor) работал бы так же хорошо.

Еще один пример кода JavaScript с дуновением Java об этом можно увидеть в fx.js , строка 465:

 goog.fx.Animation.prototype.updateCoords_ = function(t) { this.coords = new Array(this.startPoint.length); for (var i = 0; i 

Видите, как они создают массив во второй строке?

 this.coords = new Array(this.startPoint.length); 

Хотя это необходимо в Java, совершенно бессмысленно заранее указывать длину массива в JavaScript. Не менее важно создать новую переменную для хранения чисел с var i = new Number(0); вместо var i = 0; ,

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

 this.coords = []; 

О, а вы заметили еще один неэффективный цикл for в этой функции?

API дизайн

Если все вышеперечисленные низкоуровневые качества кода не убедят вас, я не согласен с тем, чтобы вы попробовали использовать некоторые API, встроенные в Closure Library.

Графические классы Closure, например, смоделированы на основе HTML5 canvas API , который соответствует тому, что вы ожидаете от JavaScript API, разработанного телом стандартов HTML. Короче говоря, это повторяющееся, неэффективное и совершенно неприятное кодирование.

Как автор Raphaël и gRaphaël , Дмитрий имеет большой опыт разработки пригодных для использования API-интерфейсов JavaScript. Если вы хотите понять весь ужас API Canvas (и, соответственно, графического API Closure), ознакомьтесь с аудио и слайдами из выступления Дмитрия на Web Directions South 2009 по этому вопросу.

Ответственность Google за качество кода

К этому моменту я надеюсь, что вы уверены, что Closure Library не является ярким примером лучшего кода JavaScript, который может предложить Интернет. Если вы ищете это, могу ли я порекомендовать более известные игроки, такие как jQuery ?

Но вы можете подумать: «И что? Google может выпустить дрянной код, если захочет — никто не заставляет вас его использовать ». И если бы это был личный проект, выпущенный каким-то гуглером на стороне под его или ее собственным именем, я бы с вами согласился, но Google одобрил Закройте библиотеку, отметив ее маркой Google.

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