JavaScript — это страшный зверь. Знать где разбросаны грабли, на которые кто-то уже наступал, не менее важно чем знать как правильно пользоваться языком. Как говорил Сунь-Цзы: "знай своего врага", и это именно то, о чем пойдет речь в этой главе. Мы рассмотрим темную сторону JavaScript и обратим внимание на все грабли, разложенные для ничего не подозревающих разработчиков.
Как уже было написано во введении, изюминка CoffeeScript не только в его лаконичном синтаксисе, но и в его способности обойти некоторые шероховатости JavaScript. Однако, в связи с тем, что CoffeeScript лишь транслируется в JavaScript, а не запускается в виртуальной машине или интерпретаторе, язык не является панацеей от всех опасностей и неожиданностей JavaScript, и есть еще ряд вещей, о которых Вы должны знать.
####Подмножество JavaScript
Синтаксис Coffescript покрывает только подмножество Javascript'а, известное как "Лучшие практики", ??so already there's less to fix??. Для примера возьмем выражение with
. Долгое время его использование считалось вредным, и звучали советы избегать его. Подразумевалось что with
предоставит короткую запись для рекуррентного доступа к свойствам объекта. Например, вместо написания:
dataObj.users.alex.email = "info@eribium.org";
Вы могли бы написать:
with(dataObj.users.alex) {
email = "info@eribium.org";
}
Если оставить в стороне тот факт, что нам не следовало бы создавать объекты с такой сильной вложенностью, синтаксис довольно понятен. Но есть одно НО
. Эта запись является чертовски запутанной для интерпретатора Javascript - он не понимает, что именно вы собираетесь сделать в этом контексте и проделывает лишнюю работу.
Такой процесс понижает производительность, он означает, что интерпретатору придется отключить любые JIT оптимизации. Кроме того выражения, использующие with
, не могут быть модифицированы различными инструментами, такими как uglify-js
. Кроме того, 'with' будет удален из будущих версий Javascript. Учитывая все обстоятельства, правильным шагом будет не использовать это выражение, и Coffeescript уже предпринял шаги для этого, исключая его из своего синтаксиса. Другими словами, использование with
в Coffeescript вызовет синтаксическую ошибку.
####Глобальные переменные
По умолчанию, Javascript код выполняется в глобальной области видимости. Там же находятся и все созданные переменные. Если вы хотите создать переменную в локальной области видимости, нужно явно указывать это с помощью ключевого слова var
.
usersCount = 1; // Global
var groupsCount = 2; // Global
(function(){
pagesCount = 3; // Global
var postsCount = 4; // Local
})()
Это решение кажется немного странным, так как в подавляющем большинстве случаев вы будете создавать локальные, а не глобальные переменные. Так почему бы не сделать это по умолчанию? В нынешнем виде разработчики должны все время помнить об использовании ключевого слова var
перед объявлением любой переменной, или они столкнуться со странными багами, из-за того, что переменные будут случайно конфликтовать и перезаписывать друг друга.
??
К счастью, Coffeescript приходит на помощь, полностью уничтожая присваивание неявных глобальных переменных. Другими словами, ключевое слово var
является зарезервированным в CoffeeScript, и его использование вызовет синтаксическую ошибку. Локальные переменные создаются неявными по умолчанию, и вам придется попотеть, чтобы создать глобальную переменную без явного указания ее, как свойства window
.
??
Взгляните на пример присваивания значения переменной в Coffeescript:
outerScope = true
do ->
innerScope = true
Компилируется в:
var outerScope;
outerScope = true;
(function() {
var innerScope;
return innerScope = true;
})();
Обратите внимание, что в Coffeescript переменные автоматически инициализируются в том контексте, в котором они используются впервые. Несмотря на то, что ??shadow?? внешнюю переменную, вы все еще имеете доступ к ней и можете на нее ссылаться. Однако, будьте осторожны. Вам придется следить за тем, чтобы случайно не переиспользовать имена внешних переменных, особенно когда вы пишите функции или классы с высокой вложенностью. В примере ниже мы случайно перезаписываем переменную package
в методе класса:
package = require('./package')
class Hem
build: ->
# Overwrites outer variable!
package = @hemPackage.compile()
hemPackage: ->
package.create()
Иногда возникает необходимость использовать глобальные переменные. Вы можете создать их, задавая свойства объекту window
:
class window.Asset
constructor: ->
Гарантируя, что глобальные переменные создаются только явно, Coffescript избавляет нас от одной из самых распространенных ошибок в программах, написанных на Javascript.
####Точка с запятой Использование точки с запятой не является обязательным в Javascript, поэтому мы можем опускать их. Тем не менее, Javascript компилятору они, на самом деле, нужны, поэтому парсер автоматически вставляет их там, где встречаются синтаксические ошибки из-за их отсутствия. Другими словами, он пытается выполнить выражение без точки с запятой и, если получает ошибку, он пытается сделать тоже самое уже с ней.
К сожалению, это чрезвычайно плохая идея, которая может привести к изменению поведения в вашем коде. Взгляните на следующий пример с, казалось бы, валидным Javascript кодом:
function() {}
(window.options || {}).property
Но для парсера это не так; мы получим сообщение о синтаксической ошибке. Если мы будем использовать ??ВЕДУЩИЕ СКОБКИ??, парсер не вставит точку с запятой. Код преобразуется в одну единственную строку:
function() {}(window.options || {}).property
Теперь вы видите проблему и почему ругался парсер. В процессе написания Javascript, вы должны всегда ставить точку с запятой в конце выражения. К счастью, CoffeeScript обходит эту проблема, избавившись от точки с запятой в своем синтаксисе. Вместо этого точки с запятой автоматически вставляются (в нужные места) при компиляции Coffeescript в Javascript.
####Зарезервированные слова
Некоторые ключевые слова в JavaScript зарезервированы для будущих версий языка, например, const
, enum
и class
. Используя их в качестве имен переменных в вашем коде, вы можете получить непредсказуемые результаты; некоторые браузеры справятся с ними на отлично
, а другие могут сдохнуть
. CoffeeScript аккуратно обходит эту проблему путем обнаружения использования ключевых слов, экранируя их, если это требуется.
For example, let's say you were to use the reserved keyword class as a property on an object, your CoffeeScript might look like this:
Для примера, представим, что вам пришлось использовать ключевое слово class
в качестве свойства объекта. Ваш Coffeescript код будет выглядеть вот так:
myObj = {
delete: "I am a keyword!"
}
myObj.class = ->
Парсер CoffeeScript заметит, что вы используете зарезервированное ключевое слово и заэкранирует его:
var myObj;
myObj = {
"delete": "I am a keyword!"
};
myObj["class"] = function() {};
####Равенства
####Определение функции
####Свойства чисел
Одним из недостатков парсера Javascript является то, что, если мы ставим точку после числа, запись интерпретируется как литерал числа с плавающей точкой, а не запрос значения свойства объекта. Например, следующий код вызовет ошибку:
5.toString();
Javascript парсер ожидает еще одно число после точки, и мы получим Unexpected token error
, когда он дойдет до toString()
. Существует два решения это проблемы: заключать число в скобки или добавлять еще одну точку.
(5).toString();
5..toString();
К счастью, парсер CoffeeScript достаточно умен, чтобы справиться с этой проблемой, используя нотацию с двумя точками автоматически (как в примере выше) всякий раз, когда вы хотите получить доступ к свойству числа.
В то время как CoffeeScript проделал большой путь, чтобы исправить недостатки JavaScript, этот путь далек от завершения. Как я ранее упоминал, CoffeeScript жестко ограничен статическим анализом по своей сути, и не выполняет никаких проверок при запуске из соображений производительности. CoffeeScript это прямой компилятор исходного кода в исходный код, идея в том, что каждая конструкция CoffeeScript преобразуется в аналогичную конструкцию JavaScript. CoffeeScript не создает абстракций над какими либо ключевыми словами JavaScript, такими как typeof, и таким образом некоторые недостатки JavaScript применимы и к CoffeeScript.
В предыдущих разделах мы рассмотрели некоторые недостатки JavaScript, которые исправлены в CoffeeScript. Теперь давайте поговорим о тех недостатках JavaScript, которые CoffeeScript исправить не может.
В то время как CoffeeScript избавился от некоторых слабостей JavaScript, другие остались, как необходимое зло. Вам просто нужно осознавать последствия использования некоторых функций. Один из примеров — eval(). Она, несомненно, может быть полезна, но следует помнить о её недостатках и избегать использования, если это возможно. Функция eval() исполнит строку, содержащую код JavaScript, в локальной области видимости, кроме того, такие функции как setTimeout() и setInterval() принимают и исполняют строки, содержащие код, в качестве первого аргумента.
Однако, они, как и eval(), усложняют работу компилятора и являются причиной серьезного проседания быстродействия. Так как компилятор понятия не имеет, что внутри строки кода до её интерпретации он не может выполнить никакие операции по оптимизации, такие как сведение в одну строку, например. Ещё одна проблема возникает с безопасностью. Если передать в eval непроверенные данные, это запросто откроет скрипт для инъекций кода. В 99% случаях, когда вы используете eval, есть лучшие и более безопасные альтернативы (такие, как квадратные скобки).
# Не стоит так делать
model = eval(modelName)
# Используйте квадратные скобки
model = window[modelName]
Оператор typeof это, пожалуй, самая большая проблема JavaScript, просто потому, что он ущербный по своей сути. На практике он пригоден только для одного, проверять определено ли значение.
typeof undefinedVar is "undefined"
Все остальные проверки типов typeof довольно жалко проваливает, возвращая непостоянный результат, который зависит от браузера и того, как были созданы экземпляры. Это не то, с чем может помочь CoffeeScript, так как он использует статический анализ и не выполняет проверки типов при компиляции. Так что это на вашей совести.
Что бы проиллюстрировать проблему я привожу таблицу из JavaScript Garden, которая показывает некоторые из основных несоответствий при проверке типов.
Значение Класс Тип
-------------------------------------
"фуу" String string
new String("фуу") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object
new RegExp("мяу") RegExp object
{} Object object
new Object() Object object
Как вы можете видеть, то, объявляете ли вы строку с помощью кавычек или с помощью класса String влияет на результат, который возвращает typeof. Логически typeof должно возвращать "string" при обоих проверках, но в последнем случае он возвращает "object". К несчастью непоследовательность в остальных случаях только хуже.
Так как нам выполнять проверку типов в JavaScript? Ну, к счастью Object.prototype.toString() приходит на помощь. Если мы вызовем эту функцию в контексте конкретного объекта, она вернет корректный тип. Все, что нам надо — подкорректировать строку, которую получаем, так что бы мы получили ту строку, которую должен был бы вернуть typeof. Вот пример имплементации, портированный из $.type jQuery:
type = do ->
classToType = {}
for name in "Boolean Number String Function Array Date RegExp Undefined Null".split(" ")
classToType["[object " + name + "]"] = name.toLowerCase()
(obj) ->
strType = Object::toString.call(obj)
classToType[strType] or "object"
# Возвращает тип, который мы ожидаем:
type("") # "string"
type(new String) # "string"
type([]) # "array"
type(/\d/) # "regexp"
type(new Date) # "date"
type(true) # "boolean"
type(null) # "null"
type({}) # "object"
Если вы хотите проверить была ли определена переменная, вам все же придется использовать typeof, иначе получите ReferenceError.
if typeof aVar isnt "undefined"
objectType = type(aVar)
Или, более коротко, с оператором существования:
objectType = type(aVar?)
Как альтернатива проверки типов, часто можно использовать слабую типизацию и оператор существования CoffeeScript вместе, что бы устранить необходимость определять тип объекта. Например, скажем, мы помещаем значение в массив с помощью push. Мы можем так сказать, что пока массивоподобный объект, имплементирует метод push(), мы можем сказать, что это массив:
anArray?.push? aValue
Если anArray это объект, не являющийся массивом, то оператор существования гарантирует, что push() никогда не будет вызван.
Ключевое слово instanceof JavaScript практически так же бесполезно, как и typeof. В идеале instanceof сравнивает конструкторы двух объектов и возвращает булево значение, показывающее является ли один из объектов экземпляром другого. Однако, в реальности, instanceof работает как надо только тогда, когда сравниваются кастомные объекты. Когда дело доходит до встроенных типов, то instanceof так же бесполезен, как и typeof.
new String("фуу") instanceof String # true
"фуу" instanceof String # false
В дополнение, instanceof не позволит сравнить объекты объявленные в различных фреймах в браузерах. На практике, instanceof возвращает корректные значения только для кастомных объектов, таких как классы CoffeeScript.
class Parent
class Child extends Parent
child = new Child
child instanceof Child # true
child instanceof Parent # true
Удостоверьтесь, что используете это только для ваших собственных объектов, или, еще лучше, вообще от неё избавьтесь.
Ключевое слово delete можно безопасно использовать только для удаления свойств объекта.
anObject = {one: 1, two: 2}
delete anObject.one
anObject.hasOwnProperty("one") # false
Любое другое его использование, такое как удаление переменных или функций, не сработает.
aVar = 1
delete aVar
typeof Var # "integer"
Это достаточно своеобразное поведение, но уж какое есть. Если хотите удалить ссылку на переменную, просто присвойте ей null.
aVar = 1
aVar = null
Функция JavaScript parseInt() может вернуть неожиданный результат, если не передать в качестве аргумента строку не указав систему счисления. Например:
# Возвращает 8, а не 10!
parseInt('010') is 8
Всегда указывайте систему счисления, что бы функция работала корректно:
# Укажите десятичную систему счисления, что бы получить корректный результат
parseInt('010', 10) is 10
CoffeeScript не может сделать это за вас; просто запомните, что надо всегда указывать систему исчисления, когда используете parseInt().
Строгий режим — новая возможность ECMAScript 5, которая позволяет вам выполнять программу или функцию в строгом режиме. Он выбрасывает больше исключений и предупреждений, чем в нормальном, показывая разработчикам, где они отклоняется от наилучших подходов к разработке, пишут не поддающийся оптимизации код или делают распространенные ошибки. Другими словами, строгий режим уменьшает количество ошибок, увеличивает безопасность, улучшает производительность и устраняет некоторые сложные для использования возможности языка. Что тут может быть плохого?
Строгий режим на сегодняшний день поддерживается в следующих браузерах:
- Chrome >= 13.0
- Safari >= 5.0
- Opera >= 12.0
- Firefox >= 4.0
- IE >= 10.0
Надо отметить, что строгий режим обладает полной обратной совместимости со старыми браузерами. Программы, которые его используют, будут выполняться и в строгом и нормальном режиме.
Большинство изменений в строгом режиме относятся к синтаксису JavaScript:
- Ошибки при появлении дублирующихся свойств и имен аргументов функций
- Ошибки при некорректном использовании оператора delete
- Попытка доступа к arguments.caller и arguments.callee вызовет ошибку (по причинам связанным с производительностью)
- Использование оператора with вызовет синтаксическую ошибку
- Некоторые переменные, такие как
undefined
больше недоступны для записи - Вводятся дополнительные зарезервированные ключевые слова, такие как implements, interface, let, package, private, protected, public, static, и yield
Однако, строгий режим так же несколько изменяет поведение скрипта:
- Глобальные переменные задаются явно (всегда используется var). Глобальное this не определено
- eval не может создавать новые переменные в локальном пространстве имен
- Функция должна быть определена перед использованием (ранее функции можно было определять где угодно)
- аргументы неизменны
CoffeeScript сам по себе вводит многие ограничения строгого режима, как, например, всегда использовать var при определении переменных, но все равно очень полезно использовать строгий режим в скриптах. Действительно, CoffeeScript развивает эту идею и в будущих версиях будет проверять программы на совместимость со строгим режимом во время компиляции.
Все, что нужно для того, что бы включить строгий режим — начать ваш скрипт или функцию следующей строкой:
-> "use strict"
Вот и все, просто строка use strict
. Что может быть проще и он обладает обратной совместимостью. Давайте посмотрим как он работает. Следующая функция вернет синтаксическую ошибку в строгом режиме, но выполнится в обычном:
do -> "use strict" console.log(arguments.callee)
Строгий режим устраняет доступ к arguments.caller и arguments.callee так как они вызывают серьезное проседание производительности и выбрасывает синтаксическую ошибку, если они используются.
Есть один конкретный подвох, который стоит иметь в виду в строгом режиме, а именно — создание глобальных переменных. Следующий пример выбросит в строгом режиме TypeError, но запускается в нормальном режиме, создавая глобальную переменную:
do -> "use strict" class @Spine
Причина этого несоответствия в том, что в строгом режиме this не определено, в то время, как в нормальном оно ссылается на объект window. Решение этой проблемы — явно задать глобальную переменную указав объект window.
do -> "use strict" class window.Spine
Я рекомендую использовать строгий режим, и не имеет значения, что он не добавляет никаких новых возможностей, которые и так не были доступны в JavaScript, и немного замедлит код из за того, что виртуальная машина выполняет больше проверок во время компиляции. Можете вести разработку в строгом режиме, а на продакшен деплоить без него.
JavaScript Lint это инструмент контроля качества кода, его использование это отличный способ его улучшить и ввести в рабочий процесс лучшие подходы к разработке. Проект построен на основе аналогичного инструмента — JSLint. Посмотрите сайт JSLint, что бы получить список, по которому стоит проверять свой код, включая использование глобальных переменных, потерянные точки с запятой и сравнение переменных разных типов.
Хорошая новость: CoffeeScript уже пропускает через линтер весь код, который генерирует, так что JavaScript сгенерированный из CoffeeScript и так проходит проверки JavaScript Lint. Кроме того, команда coffee поддерживает ключ --lint:
coffee --lint index.coffee
index.coffee: 0 error(s), 0 warning(s)