Про JavaScript (не для JavaScript программистов).


Последние полтора месяца пишу для одного своего заказчика графический движок на 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
Любое использование либо копирование материалов или подборки материалов сайта, элементов дизайна и оформления допускается лишь с разрешения правообладателя и только со ссылкой на источник: programador.ru

Телеграм канал: @prgrmdr
Почта для связи: vit [at] programmisty.com