Семинар 2: JavaScript

Язык программирования JavaScript (или попросту JS) - это язык, к которому невозможно относиться равнодушно. Как правило, люди или влюбляются в него с первого взгляда, или ненавидят всей душой. И для того, и для другого найдется немало причин, ведь этот язык в своем развитии прошел множество этапов и сделал кучу верных и неверных выборов.

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

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

Современное состояние JavaScript

Прежде чем мы приступим к изучению JavaScript, выясним, что же можно делать на этом языке сейчас. А возможности этого языка действительно впечатляют. Так, на современном JS (естественно, с использованием HTML и CSS) можно:

  • писать полноценные приложения, работающие в браузере пользователя, т.н. Single-Page Applications (SPA); примеры таких приложений - Яндекс.Почта, Google Docs, ВКонтакте и многие другие современные веб-приложения, которыми мы пользуемся каждый день;
  • создавать что-то среднее между SPA и мобильным приложением - т.н. Progressive Web Applications (PWA); фактически это SPA, которое можно добавить на рабочий стол Android-смартфона;
  • сделать полноценное мобильное приложение;
  • писать приложения для рабочего стола; к примеру, новое приложение Skype написано на JS.

Более того, даже если словосочетание "пользовательский интерфейс" внушает первобытный ужас и нет никакого желания что-то верстать, путь в JavaScript-разработку не закрыт - на JS можно писать бекенд (а что такое бекенд - а вот что).

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

История JavaScript

Сложно поверить, но в начале своего развития JavaScript задумывался как язык для добавления небольшой интерактивности к веб-странице - изменения свойств элементов при наступлении событий типа наведения мышки, простых анимаций (то есть всего того, что мы сейчас делаем на CSS - тогда CSS был не так сильно развит) и реализации простой - в несколько строк кода - логики на стороне клиента. Это, собственно, и сыграло с языком злую шутку - он не был рассчитан не то, что на современные требования, даже не скромный по современным меркам интерактив веб-сайтов начала 2010-х гг. JS проектировался как максимально простой язык программирования, который не должен следить за программистом, как это делает, к примеру, С++, не должен препятствовать его потенциально опасным действиям и вообще должен позволять писать как угодно. Действительно, зачем нужно заморачиваться языку, на котором пишут максимально 10-20 строк кода...

Следует также отметить, что в истории Всемирной Паутины был период, называемый войной браузеров. Этот процесс имел куда более далеко идущие последствия, чем может показаться на первый взгляд - фактически каждый браузер 2000-х гг. имели свою собственную версию JavaScript. Из-за этого, во-первых, сложное программирование на JS не имело особого смысла, ведь оно превращалось во многом в разработку под конкреный браузер. Во-вторых, JS как технология не развивался - разработчики браузеров не особо шли на диалог друг с другом и не пытались свести JS к единому стандарту.

В довершение ко всему в Интернет пришел бизнес и тут же затребовал организации интерактивности на стороне клиента, ведь это большое конкурентное преимущество веб-сайта (и, соответственно, бизнеса). Таким образом, на языке, не предназначенном для написания более чем двузначного количества строк кода, стали писать несоизмеримо более сложные вещи.

Все это вкупе с тем, что в 2000-х программирование на JavaScript не являлось профессией, а скорее дополнением к какому-либо "основному" языку программирования (как правило, PHP), доставляло невообразимое удовольствие веб-разработчикам и питало их ненависть к JS.

Примерно в 2012 году произошел резкий скачок в развитии JS - появился ECMAScript 5, после чего каждые несколько лет начинает выходить обновленная версия языка, в него добавляются новые полезные и удобные возможности. Конечно, далеко не от всех старых проблем JS удалось избавиться, но при этом современный JavaScript ведет себя гораздо более предсказуемо, чем это было 10 лет назад.

JS в целом достаточно простой язык программирования, но для того, чтобы начать на нем разрабатывать, нужно изучить не только синтаксис, но и основные "подводные камни" языка, что мы сейчас и постараемся сдеать.

Поддержка браузерами

Существует проблема поддержки нового JS старыми браузерами. Например, Internet Explorer 9 был выпущен тогда, когда про новые фишки ECMAScript никто и не помышлял, но при этом браузер до сих пор используется. Естественно, новые фишки JS в этом браузере просто не будут работать и это проблема - у какой-то части ваших клиентов не будет работать ваше приложение! В нашем курсе мы совершенно не будем касаться этой проблемы и рассматривать пути ее решения, отметим лишь, что настоятельно рекомендуется пользоваться самыми последними версиями браузеров Chorme, Firefox или Opera. В них наш код точно будет работать.

'use strict'

Не забывайте писать в начале своих скриптов 'use strict'! Что это значит, мы разберем позже, пока отметим лишь то, что данная строка добавляет +100 к предсказуемости вашей программы.

Что такое JavaScript

JavaScript - это интерпретируемый динамически типизированный язык программирования.

Интерпретируемый язык программирования

Мы не будем вдаваться в детали классификации языков программирования. Напротив, мы попытаемся грубо, "на пальцах" рассмотреть два основных типа языков программирования - компилируемые и интерпретируемые.

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

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

  1. Компилировать программу можно только целиком.
  2. После компиляции вы получаете некий исполняемый файл, который может работать непосредственно на вашем железе, ему для этого больше не нужен компилятор.
  3. Компилятор в свою очередь никак не может получить доступ к программе, когда она уже выполняется, потому что работает до запуска программы (он ее, в общем-то, и создает).

С компилируемыми языками программирования вы сталкивались на первом курсе и, возможно, в школе: это Delphi, Pascal, C, C++.

Интерпретаторы принимают на вход текст программы и сами же ее выполняют. Представьте себе, что вы пишете программу-калькулятор - на вход примиаете строку, разбиваете ее на два числа и знак опреации, выполняете эту операцию и печатаете результат. Вы, конечно же, можете внутри своей программы организовать хранение введенных данных. Точно так же можно организовать запоминание результатов предыдущих вычислений (переменные) и вообще усложнить программу до того, что она будет рещать дифференциальные уравнения. Примерно по такому принципу построены интерпретаторы - это программы, которые читают исходный код, если нужно каким-то образом учитывают введенные данные (например, запоминают в какой-то структуре данных переменные), делают вычисления и возвращают результат. Главное, что важно понимать:

  1. Интерпретатору можно давать программу как целиком, так и по кусочкам (но, конечно, вы не должны обращаться к необъявленным переменным и т.п., этого вам не разрешит делать ни один язык программирования, неважно, компилируемый он или интерпретируемый).
  2. В конце вы не получаете исполняемый файл, интерпретатор - это и есть то, что исполняет вашу программу как бы внутри себя.
  3. Интерпретатор имеет полный доступ к состоянию программы в любой момент ее исполнения, он знает абсолютно обо всем, что происходит с ней.

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

Примеры интерпретируемых языков программирования - JavaScript, PHP, Ruby, Python.

И компилируемые, и интерпретируемые языки программирования существуют и используются по сей день. На стороне компилируемых языков программирования скорость работы, на стороне интерпретируемых - гибкость.

Динамически типизированный язык программирования

Помимо разделения языков программирования на компилируемые и интерпретируемые, существует разделение на статически и динамически типизированные.

Статически типизированные языки программирования предполагают, что у переменной есть явно указанный при объявлении тип. Переменная не может менять этот тип. Исторически так сложилось, что компилируемые языки программирования статически типизированны, ведь практически невозможно написать такой компилятор, который бы смог до того, как программа начнет исполняться, распределить реальную память компьютера так, чтобы можно было менять тип (а значит и размер) переменной. Гораздо легче жестко задать типы и так же жестко распределить память.

Вспомним С:

int a = 0;

Теперь a занимает 4 байта и может принимать только целочисленные значения. Здесь a - это конкретная ячейка в памяти компьютера: известно, где она находится и сколько байт занимает.

Динамически типизированные языки программирования умеют определять тип переменной во время присваивания. На любом динамически типизированном языке программирования вы можете написать что-то вроде (это пока псевдокод):

a = 10
a = 'kdfnkndf'

С точки зрения С++ это невозможно: если он выделил память под int, он не сможет уместить туда строку. А динамически типизированные языки программирования почему-то могут. Как же это возможно?

Дело в том, что в динамически типизированном языке программирования переменная - это ссылка. Динамически типизированные языки программирования интерпретируемые, поэтому в них инструкция a = 10 выполняется так:

  1. Интерпретатор создает (для простоты скажем где-то внутри себя) ячейку типа int и помещает туда значение 10.
  2. Интерпретатор создает идентификатор a и делает так, что а ссылается на ячейку типа int со значением 10.

Таким образом, a как таковая не имеет типа и может ссылаться на ячейку любого типа. Если переприсвоить a значение другого типа, то интерпретатор просто изменит ссылку - теперь a ссылается на другую ячейку.

Особенности JavaScript

Теперь, когда мы знаем, с чем нам придется иметь дело, познакомимся с JS поближе.

Типы данных

В JavaScript существует 6 типов данных:

  1. Число (number) - это и целые числа, и числа с плавающей точкой (для JS все - число с плавающей точкой). Числовые литералы выглядят так же, как и в C и C++: 1 - это целое число, 1.0 (или просто 1.) - это число с плавающей точкой.
  2. Булев тип (boolean) - это значения true или false.
  3. Строка (string) - последовательность байт в кодировке utf-8(то есть туда можно писать не только латинские символы); строка может выглядеть так 'this is string' (в одинарных кавычках - наиболее предпочтительный вариант) или так "this is string" (ничем не отличается от первого варианта кроме того, что так не принято); также есть возможность поместить в строку значение переменной: если в переменной a записано значение 1, то строка `${a}` (в обратных кавычках) будет эквивалентна строке '1' (здесь ${} - операция подстановки в строку).
  4. Ничего (null).
  5. Не инициализировано (undefined).
  6. Объект (object).

Вы могли заметить, что про числа, булевы значения и строки есть комментарий, а про null, undefined и object - нет.

Для того, чтобы понять, зачем нам нужны null и undefined, нужно научиться объявлять переменные, что мы и сделаем в следующем разделе.

В случае с типом object все еще сложнее. Первые пять типов называются примитивными. Это значит, что они ведут себя так, как вы привыкли видеть в С++ или Delphi: например, при операции присваивания из одной переменной в другую значение копируется. Переменные типа object ведут себя совсем не так, но до этого мы дойдем еще позже.

Переменные

Многие динамически типизированные языки программирования позволяют объявлять переменные без каких-либо ключевых слов, например, в языке Ruby, который мы изучим в дальнейшем, это делается так:

a = 10

В противовес этому в JS необходимо использовать ключевые слова. В современной версии языка их целых три: var, let и const.

Ключевое слово var нужно для того, чтобы объявить глобальную переменную. Использовать его можно так:

var a = 10;

Настоятельно не рекомендуется использовать это ключевое слово в современном JS. Оно ведет себя не очень предсказуемо и может сильно запутать начинающих разработчиков на JS. Примеры подводных камней приведены в дополнительных материалах, а здесь мы рассмотрим только один простой пример. Итак, что будет выведено в результате выполнения этого кода (console.log - это инструкция "напечатать в консоль браузера", F12 в Chrome и Firefox)?

console.log(myVar);
var myVar = 10;
console.log(myVar);

Любой нормальный человек (и любой нормальный язык программирования) ответит вам, что программа поломается на первой строке, ведь myVar не объявлена, НО в JS все будет работать по-другому. Вы увидите в консоли

undefined
10

Дело в том, что интерпретатор JS проходит код дважды: в первый раз он читает его и ищет все глобальные переменные и функции, во второй - выполняет. Поэтому к моменту исполнения интерпретатор уже знает о существовании переменной myVar. Однако на момент первого вывода в консоль она еще не инициализирована - на этот случай и существует специальный тип undefined, который присваивается любой объявленной, но неинициализированной переменной.

Как видите, var работает так себе. Давайте посмотрим, как обстоят дела с let и const.

Ключевое слово let - это var на стероидах. Оно ведет себя именно так, как вы ожидаете:

console.log(myVar);
let myVar = 10;
console.log(myVar);

выведет ошибку вида

VM72:1 Uncaught ReferenceError: myVar is not defined
    at <anonymous>:1:13

и правильно - ибо нефиг.

Ключевое слово const в общем-то ничем не отличается от let за исключением того, что ему нельзя переприсваивать значение, то есть

let a = 10;
a = 11;

делать можно, а

const a = 10;
a = 11;

То есть ключевое слово const означает, что переменная ссылается на одну конретную ячейку памяти и эту ссылку нельзя передвинуть. При этом const - это не константа. Вы можете модифицировать переменную:

const str = 'aaa';
str.replace('a', 'b'); // Меняем первую a на b.

Это возможно потому, что переменная все еще ссылается на ту же самую ячейку, просто значение в этой ячейке изменилось.

Итак, когда использовать let, а когда const? Используйте const так часто, как можете, а когда вам нужно менять объект, на который ссылается переменная - используйте let (например, цикл for (let i = 0; i < 10; i++) с const работать не будет).

null, undefined... WTF?

Как мы уже поняли, специальное значение undefined используется интерпретатором JS если переменная объявлена, но не инициализирована, например:

let a;
console.log(a); // undefined

Есть один нюанс...

Конечно, в случае с const так сделать нельзя - это просто не имеет смысла. Тогда переменная бы всегда имела значение undefined и вы не смогли бы его изменить. Поэтому интерпретатор JS выкинет ошибку, если вы попытаетесь так сделать.

В принципе никто не запрещает использовать значение undefined - вы можете его присваивать, сравнивать с ним и т.д., однако с точки зрения семантики это противоречит здравому смысл - вроде бы определяется переменная, но со специальным значением "переменная не определена". Специально для того, чтобы можно было использовать что-то в смысле "пока тут ничего нет" или "значение неизвестно" используется null.

Что является false кроме false?

Для того, чтобы показать, что где-то чего-то нет или что-то неизвестно, можно было бы обойтись старым добрым false, но создателям языка этого показалось недостаточно. Как минимум, без undefined в JS объективно никуда, но ведь есть еще и null (да и это на самом деле не все)... Получается, что как минимум false, null и undefined имеют смысловое значение "ложь" и чтобы проверить, что что-то "ложь", нужно проверять кучу условий?

Ответ - нет. JS умеет неявно приводить типы. Поэтому с точки зрения JS значение "ложь" имеют:

  1. false.
  2. null.
  3. undefined.
  4. '' (пустая строка).
  5. 0.
  6. NaN (это не тип данных, это результат арифметической операции, который называется "не число" (Not a Numner, в частности, бесконечность), например, такое получится, если вычислить 1 / 0).

Все остальное - true!

Объекты

JS - это необычный язык. С одной стороны он объектно-ориентированный, но с другой стороны в нем есть только объекты, а классов нет. В JS нет понятия "наследование" в привычном смысле, а каждый объект уникален, то есть не является экземпляром класса, а является как бы экземпляром самого себя.

В этом языке не существует инкапсуляции данных. Объект - это просто набор пар ключ-значение, которые кто угодно может не только прочитать, но и в любой момент изменить то, что уже есть, добавить что-то новое и удалить существующее. Самый простой способ объявить объект - воспользоваться объектным литералом:

const myObject = {
    a: 10, // ключ - название свойства, значение - то, что мы получим при
           // обращении по этому ключу.
    b: '100',
    c: {
        key: 'value', // объекты могут быть вложенными.
    },
    // любой ключ - это строка, то есть вот это будет приведено к строке:
    1: 1,
    // а еще, если строка не может быть валидным идентификатором, можно писать так:
    'This is object attr': 1228,
};

// обращение к свойствам - через точку
myObject.a; //10
// или через квадратные скобки
myObject['This is object attr']; // 1228

// если такого свойства у объекта нет, вы получите undefined
myObject.someAttr; // undefined

// к существующему объекту всегда можно добавить свойство
myObject.newAttr = 9;
myObject.newAttr; // 9

// помимо этого можно удалить свойство из объекта
delete myObject.a; // true
myObject.a; // undefined

Строгое и нестрогое равенства, приведения типов

В JS есть два вида равенства - строгое (===) и нестрогое (`=='). В случае строгого равенства JS не выполняет неявные приведения типов, в случае нестрогого - выполняет, то есть:

1 === 0 // false
1 == 0 // false

0 === false // false
0 == false // true !!!

То же самое распространяется на неравенство - строгое (!==) и нестрогое (`!=')

Практика показывает, что нестрогое равенство в 98% процентах случаев - это не то, что вам нужно, т.к. оно может приводить к непредсказуемым ошибкам вашей программы. Всегда используйте строгие версии операторов сравнения!

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

let x = // ...

// ...

+x // преобразует x к Number.
!!x // преобразует x к Boolean.

Функции

Функции в JS являются объектами первого рода. Это значит, что идентификатор функции - это переменная. Его можно присваивать в другие переменные, куда-то передавать (например, в другие функции), в общем, работать с ним как с обычной переменной. Существует несколько способов объявления функции:

// "Обычная" функция.
function f() {

}

// Функциональный литерал.
const f = function() {

};

Функциональный литерал придуман для удобства: его можно записать в переменную, передать куда-то (например, в другую функцию), но самое интересное, что с его помощью можно объявить методы в объекте:

const obj = {
    doSomeStuff: function() {
        console.log('Some stuff');
    },
};

obj.doSomeStuff(); // Some stuff

Также в современном JS существуют стрелочные функции:

const arrow = (x, y, z) => {
    return x + y + z;
};

О том, что в них хорошего, написано здесь.

Глобальный объект

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

Разные языки программирования подходят к решению этой задачи по-разному, в JavaScript существует понятие "Глобальный объект". Это обычный с точки зрения языка объект, который хранит все глобальные переменные (объявленные с помощью var) и все функции (объявленные с помощью function)

Мы рассматриваем применение JS исключительно в браузере, в браузере глобальный объект называется window. Попробуйте в консоли браузера (F12) ввести window - вы увидите объектный литерал с кучей свойств.

Ключевое слово this

Последним штрихом в портрете JavaScript является знакомство с ключевым словом this. Из С++ мы знаем, что this - это неявный указатель на текущий объект. Во многих языках это так, возможно, где-то он называется по-другому, но общий смысл от этого не меняется.

В JS все СОВСЕМ не так. Ключевое слово this в JS:

  1. Доступно везде - в глобальном контексте, в функции, в объекте - исключений нет.
  2. Всегда ссылается на объект, который является "главным" в данный момент.

Итак, если открыть консоль браузера и ввести this, мы увидим глобальный объект - ровно то же, что мы могли бы видеть, если бы ввели window.

Вроде просто. Что будет, если посмотреть на this внутри функции:

function fUsual() {
    console.log(this);
}

function fStrict() {
    'use strict';
    console.log(this);
}

fUsual(); // window
fStrict(); // undefined

Помните, высоко наверху мы говорили про строгий режим и то, что его надо использовать? Вот и причина. Всегда используйте строгий режим (по умолчанию он отключен) - тогда браузер будет вести себя более-менее адекватно, а не выдавать window внутри функции за this. Давайте разберемся, чем поведение без строгого режима плохо.

Допустим, есть объект с методом

const objWithMethod = {
    x: 10,
    print: function() {
        console.log(this.x);
    },
};

Если написать objWithMethod.print(), то мы увидим в консоли 10 - JS понимает, что функция print вызывается в контексте объекта objWithMethod и this будет именно этим объектом, он тут главный.

Вспомним, что функции являются объектами первого рода и их можно передавать как переменные. Попробуем сделать так:

function f(fToCall) {
    fToCall();
}

f(objWithMethod.print);

Вроде все ок, передали функцию, она должна вызваться, НО this определяется динамически и теперь это не objWithMethod, а this из f. И вот тут уже важно, в каком режиме вы работаете - строгом или нет.

Если вы работаете не в строгом режиме, то JS будет искать свойство x в глобальном объекте. И вполне может быть, что найдет. Такое поведение вам 100% не нужно, к тому же отловить подобный баг в куче кода ну очень сложно.

Если вы работаете в строгом режиме, то все проще, вы увидите что-то вроде VM242:4 Uncaught TypeError: Cannot read property 'x' of undefined ....

Пользуйтесь строгим режимом, он убережет вас от ошибок!

BOM, DOM, бам-бам-бам

бам-бам-бам

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

Главный инструмент - это BOM или Browser Object Model - браузерный хост-объект, который содержит кучу полезных JS-объектов, помогающих взаимодействовать с контентом на странице.

BOM

В наибольшей степени нас интересует DOM - Document Object Model. Она помогает нам взаимодействовать с HTML-разметкой из JS: изменять существующие элементы, добавлять новые, удалять старые. Давайте разберемся, как этого можно добиться.

Достучаться до DOM можно через объект document (document, window.document или this.document - это одно и то же, но принято использовать самый короткий вариант). Дальше нам нужно получить какой-либо элемент и что-то с ним сделать.

Как получить элемент

Во-первых, существуют прекрасные атрибуты id и class. Чтобы получить элемент по id, существует метод document.getElementById, чтобы получить коллекцию элементов по названию класса - document.getElementsByClassName, по названию тега - document.getElementsByTagName.

"Живые" коллекции

Все коллекции "живые" (и называются NodeList), то есть если сначала получить коллекцию элементов по названию класса, а потом добавить на страницу еще один, он автоматически появится и в коллекции.

Также можно воспользоваться методами document.querySelector (возвращает первый (самый верхний) элемент из всех удовлетворяющих критерию поска) и document.querySelectorAll (возвращает "живую" коллекцию элементов). Отличие от рассмотренных выше методов заключается в том, что методам с префиксом queryselector в качестве аргумента передается CSS-селектор, то есть искать можно и по id, и по классу, и по названию элемента, и по сложным CSS-выражениям.

Элемент HTML-разметки представлен JS-"классом" Element. У такого объекта можно вызывать все рассмотренные выше для document методы, отличие будет лишь в том, что поиск будет вестись не во всем документе, а только внутри конкретного элемента.

Что можно делать с элементом

У Element есть свойства innerHTML и innerText. Первое вставляет HTML-разметку внутрь элемента (по соображениям безопасности не рекомендуется использовать), второе вставляет текст. Например:

<body>
    <div id="my-div">
    </div>

    <script>
        const div = document.getElementById('my-div');
        div.innerHTML = '<p id="my-p"></p>';

        const p = document.getElementById('my-p');
        p.innerText = 'Привет!';
    </script>
</body>

Внутрь элемента div с помощью JS добавляется пустой параграф, в который впоследствии вставляется текст. Свойство innerHTML не рекомендуется использовать потому, что все, что передается в него, рассматривается браузером как разметка, то есть можно случайно "вставить", например, вредоносный скрипт. В случае с innerText все более радужно - что бы туда ни было передано, оно будет обработано браузером как текст.

Существуют и другие способы добавления элементов:

<body>
    <div id="my-div">
    </div>

    <script>
        const div = document.getElementById('my-div');

        const p = document.createElement('p');
        p.innerText = 'Привет!';

        div.insertAdjacentElement('beforeend', p);
    </script>
</body>

Подробные ссылки на документацию по используемым методам даны в дополнительных материалах.

Помимо создания элемента можно менять его стиль: или изменять свойство style напрямую (чего лучше не делать, в больших проектах запутаетесь), или написать CSS-классы и присваивать/удалять их через свойство classList.

События в браузере

Работа с пользовательским интерфейсом по своей природе асинхронна. Невозможно предугадать, когда именно пользователь нажмет ту или иную кнопку, равно как необходимо, чтобы после того, как это все же произойдет, интерфейс не зависал. JS - это асинхронный язык и в нем с самого начала были инструменты для асинхронного программирования.

В браузерном программировании асинхронность достигается благодаря механизму событий. Это специальная функция браузера, которая позволяет информировать о наступлении того или иного события - клика мышкой, отправки формы и т.д. - чем-то напоминает псевдоклассы в CSS, неправда ли?

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

Модное слово

Асинхронный обработчик называется коллбек (callback) или коллбек-функция - функция обратного вызова. Это слово появилось потому, что обработчик, вызывающийся при наступлении события как бы отбрасывает программу назад, в то место, где объявлено его тело. Конечно, на самом деле это неправда, но если выполнять отладку программы по шагам, вы именно это и увидите.

Существует несколько способов добавить обработчик. Допустим, в элементе <script> объявлена функция myCallback и мы хотим сделать ее обработчиком события "клик мышью по кнопке".

<script>
    function myCallback() {
        console.log(1);
    }
</script>

Первый вариант - добавить обработчик прямо в HTML:

<button onclick="myCallback()">Нажми меня</button>

Помимо этого, тело функции можно заинлайнить:

<button onclick="console.log(1);">Нажми меня</button>

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

Второй вариант - установить атрибут прямо из JS

<button>Нажми меня</button>
<script>
    function myCallback() {
        console.log(1);
    }

    const btn = document.querySelector('button');
    btn.onclick = myCallback;
</script>

Или проще:

<button>Нажми меня</button>
<script>
    const btn = document.querySelector('button');
    btn.onclick = function () {
        console.log(1);
    };
</script>

Этот вариант тоже не очень, ведь обработчик в таком случае может быть только один. Вам может быть необходимо повесить несколько обработчиков на одно и то же событие, так еще нужно учитывать, что какая-то библиотека, которую вы используете, может вешать свои обработчики на это событие (и затирать ваш).

Самый предпочтительный вариант - использовать addEventListener для добавления нового обработчика и removeEventListener для удаления уже сущестующего обработчика.

<button>Нажми меня</button>
<script>
    const btn = document.querySelector('button');
    btn.addEventListener('click', function () {
        console.log(1);
    });
</script>

preventDefault

Часто бывает нужно, чтобы браузер не делал то, что обычно, например, не пытался перейти по ссылке или не отправлял форму. Для этого у объекта Event есть метод preventDefault, который предотвращает поведение браузера по умолчанию. Например, чтобы браузер не переходил по ссылкам на странице, достаточно добавить следующий обработчик:

document.addEventListener('click', function (evt) {
    // evt.target - это ссылка на элемент, на котором
    // сработало событие. Этот if проверяет, что мы
    // кликнули по элементу <a> - ведь мы навесили
    // обработчик на всю страницу, поэтому он
    // сработает при любом клике по ней.
    if (evt.target instanceof HTMLAnchorElement) {
        evt.preventDefault();
        return;
    }
});

Работа с формами

При работе с формами мы хотим получить пользовательский ввод и тут нам уже не поможет старый-добрый urlencoding. Рассмотрим пример, который выводит содержимое формы в консоль:

<form id="my-form">
    <input name="input1" />
    <input name="input2" />
    <input type="submit" />
</form>

<script>
    const myForm = document.querySelector('#my-form');
    myForm.addEventListener('submit', function (evt) {
        // Запрещаем форме отправляться
        // способом по умолчанию.
        evt.preventDefault();

        const form = evt.target;

        // Значение поля input1.
        console.log(form.elements['input1'].value);
        // Значение поля input2.
        console.log(form.elements['input2'].value);

        // Очищает пользовательский ввод.
        form.reset();
    });
</script>

Заключение

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

Полезные ссылки

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

Если вам не хватает деталей - прекрасным источником могут стать традиционно StackOverflow и MDN.

Больше полезных ссылок доступно в дополнительных материалах.