Последние полтора месяца пишу для одного своего заказчика графический движок на JavaScript (HTML5/Canvas), который будет рисовать в браузере некоторые их инженерные схемы. Параллельно консультирую штатных программистов, у которых не очень большой опыт работы с JavaScript-ом.
При кажущейся простоте JavaScript не создан для легкого написания надежного кода. Для самопроверки пользуюсь jsLint-ом и периодически запускаю google closure compiler. Тем не менее, это не решает всех проблем. Даже если вы пишете правильный и аккуратный код, указываете комментарии в исходником коде для проверки типов и т.д., все равно приходится время от времени сталкиваться с работами других программистов.
Из своих “полевых наблюдений” за программистами я понял, что у тех несчастных, которые начинают писать на JavaScript-е после других языков, возникает путанное понимание некоторых фундаментальных понятий этого языка.
Не многопоточный, но асинхронный.
Если не брать в рассмотрение Worker-ы, то JavaScript – однопоточный (single-threaded). Из этого факта следует два примечательных момента:
Плохой – если где-то бесконечный цикл всё зависает.
Хороший – вы уверенны в том, что никто внезапно втихую не поменяет вашу самую главную переменную, в тот момент когда вы еще не завершили выполнение своей функции. Нет нужды (как в Java) запоминать про volatile и synchronized .
При этом вполне возможна асинхронная модель работы. На самом деле я постоянно удивляюсь, что некоторые путают асинхронность и многопоточность, т.к. это в принципе разные понятия.
Асинхронность, если очень упрощенно, это когда мы вызвали функцию и НЕ ждем пока она вернет свой результат. Когда ответ придет (или произойдет какое-то событие), тогда мы и будет предпринимать какие-либо действия (например вызовем функцию для обработки ответа/события).
Наглядный пример – AJAX запрос на сервер. В случае если ваш запрос долго обрабатывается на сервере, текущий поток не блокируются (кнопочки нажимаются, мышь двигается и т.д.). Другими словами, мы дали поручение и не “стоим над душей” у исполнителя, а спокойно продолжаем заниматься своими делами.
Когда ответ придет, то тогда мы его обработаем.
Программисты, которые имеют богатый опыт написания программ НЕ на JavaScript, часто представляют сложную картинку в которой происходит порождение нового Thread-а, в котором мы выполняем отправку запроса, ждем получения ответа и выполняем его обработку. При этом заранее начинают переживать о синхронизации и конкурентном доступе к различным объектам. Старое поколение java-программистов – те, кто писал на Swing-е, вспоминают как они использовали SwingUtilities.invokeLater(Runnable), более молодые, попробовавшие свои силы в Android-e, думают об аналогиях с Activity.runOnUiThread(Runnable).
JavaScript-программисты как правило не сильно задумываются о таких сложностях. Нет многопоточности – нет проблемы.
С другой стороны, есть асинхронность и далеко не всегда можно понять заранее в каком порядке будут приходить ответы и вызываться функции-обработчики. В итоге это приводит к появлению сложно отлавливаемых и наивных ошибок, связанных уже с асинхронной природой написанных программы.
Кто ты, мистер this?
Другая проблема, это отличие в this. Например в Java в теле не статик метода через this можно получить ссылку на текущий объект.
Другое дело JavaScript. В нем функции – как объекты.
Например, вы сделали свой объект. Затем написали в нем функцию в качестве метода.
Так вот, после этого, кто угодно может оторвать написанную вами функцию от вашего объекта и будет использовать ее как ему захочется. Например запихать ее в качестве параметра в другую функцию (это не извращение, это норма для JavaScript программ).
О каком таком текущем объекте может идти речь в таком случае?
В JavaScript “this” – это не текущий объект, а некоторая абстракция – текущий контекст исполнения (Execution Context).
На что он указывает на самом деле зависит от обстоятельств.
Например:
var a = {
moo : "moo",
foo : function () {
alert ("moo:" + this.moo);
}
}
Если вызвать a.foo(), то this – будет объект а. Если вызвать setTimeout(a.foo, 1), то this – НЕ будет указывать на а.
Самое главное – надо помнить, что если вы пишите функцию в которой используете this, то с this – может быть произойти подмена и this-ом станет нечто совсем другое.
Напоследок немного про Canvas.
Про пиксели. Если вы нарисуете горизонтальную линию:
context.lineWidth = 1;
context.moveTo(10,10);
context.lineTo(200,10);
context.stroke();
Нужно внимательно присмотреться и можно легко увидеть, что она будет толщиной не 1 пиксель, а 2 пикселя.
Канва пытается нарисовать линию так, чтобы середина линии была строго в точке (10,10).
Поскольку это не возможно, она пытается нарисовать “приблизительно”. В данном случае – два раза толще и более светлым цветом.
Если сдвинуть на 0.5 пикселя, то получится четкая черная линия.
context.lineWidth = 1;
context.moveTo(10,20.5);
context.lineTo(200,20.5);
context.stroke();
Пример:
Полезные ссылки:
- Спецификацию языка ECMA-262 (JavaScript). Раздел: 10.3 Execution Contexts (контекст исполнения) Ecma-262.pdf.
- Проверялка JavaScript-a. jsLint
- Сжималка и проверялка JavaScript-a от Google. Closure Compiler
- Хороший русскоязычный сайт по JavaScript-у: javascript.ru