Повторное введение в JavaScript (JS учебник)
Почему повторное введение? Потому что JavaScript известен тем, что является самым неправильно понятым языком программирования в мире. Его часто называют игрушкой, но под слоем обманчивой простоты ожидают мощные языковые возможности. В настоящее время JavaScript используется невероятным количеством высококлассных приложений, показывая, что углублённое знание этой технологии является важным навыком для любого веб или мобильного разработчика.
Было бы полезно начать с истории языка. JavaScript был создан в 1995 Бренданом Айком, инженером в компании Netscape. Первый релиз состоялся вместе с выходом браузера Netscape 2 в начале 1996 года. Сначала язык назывался LiveScript, но затем был переименован в связи с маркетинговыми целями, чтобы сыграть на популярности языка Java компании Sun Microsystem — несмотря на это языки практически не имеют ничего общего друг с другом. Так было положено начало путаницы между этими языками.
Чуть позже Microsoft выпустила очень похожий и практически совместимый язык JScript, который шёл вместе с IE3. Через пару месяцев Netscape отправил язык в Ecma International, Европейскую организацию занимающуюся стандартами, которая выпустила первую версию стандарта ECMAScript в 1997. Стандарт получил значимое обновление в ECMAScript edition 3 в 1999, и остаётся самым стабильным до сегодняшнего дня. Четвёртая версия была отклонена, из-за проблем с усложнениями в языке. Многие вещи из четвёртого издания послужили основой для стандарта ECMAScript 5 (декабрь 2009) и ECMAScript 6 (июнь 2015).
На заметку: Далее по тексту мы будем называть язык ECMAScript как "JavaScript".
В отличие от большинства языков, JavaScript не следует концепции ввода (input) и вывода (output). Он спроектирован таким образом, чтобы запускаться как язык сценариев, встроенный в среду исполнения. Самая популярная среда исполнения это браузер, однако интерпретаторы JavaScript присутствуют и в Adobe Acrobat, Photoshop, Yahoo!'s Widget engine, и даже в серверном окружении, например node.js.
Описание
JavaScript является объектно-ориентированным языком, имеющий типы и операторы, встроенные объекты и методы. Его синтаксис происходит от языков Java и C, поэтому много конструкций из этих языков применимы и к JavaScript. Одним из ключевых отличий JavaScript является отсутствие классов, вместо этого функциональность классов осуществляется прототипами объектов (смотрите ES6 Classes ) . Другое главное отличие в том, что функции это объекты, в которых содержится исполняемый код и которые могут быть переданы куда-либо, как и любой другой объект.
Начнём с основы любого языка: с типов данных. Программы на JavaScript оперируют значениями, и все эти значения принадлежат к определённому типу. Типы данных в JavaScript:
Да, ещё Undefined и Null, которые немного обособлены. И Массивы, которые являются особым видом объектов. А также Даты и Регулярные выражения, тоже являющиеся объектами. И, если быть технически точным, функции это тоже особый вид объекта. Поэтому схема типов выглядит скорее так:
Также есть несколько встроенных типов Ошибок. Чтобы было проще, рассмотрим подробнее первую схему.
Числа
Числа в JavaScript — это "64-битные значения двойной точности формата IEEE 754", согласно спецификации. Это имеет интересные последствия. В JavaScript нет такой вещи, как целое число, поэтому с арифметикой нужно быть начеку, если вы привыкли к вычислениям в языках C или Java. Взгляните на пример:
На практике целые значения это 32-битные целые (и хранятся таким образом в некоторых браузерных реализациях), что может быть важно для побитовых операций.
Поддерживаются стандартные арифметические операторы, включая сложение, вычитание, остаток от деления и т.д. Есть ещё встроенный объект, который я забыл упомянуть, называемый Math, который содержит более продвинутые математические функции и константы:
Вы можете преобразовать строку в целое число, используя встроенную функцию parseInt() . Её необязательный второй параметр — основание системы счисления, которое следует всегда явно указывать:
Если вы не предоставите основание, то можете получить неожиданные результаты:
Это случилось потому, что функция parseInt() расценила строку как восьмеричную из-за начального 0, а шестнадцатеричную - из-за начального "0x".
Если хотите преобразовать двоичное число в десятичное целое, просто смените основание:
Вы можете аналогично парсить дробные числа, используя встроенную функцию parseFloat() , которая использует всегда основание 10 в отличие от родственной parseInt() .
Также можно использовать унарный оператор + для преобразования значения в число:
Специальное значение NaN (сокращение от "Not a Number") возвращается, если строка не является числом:
NaN "заразителен": любая математическая операция над NaN возвращает NaN :
Проверить значение на NaN можно встроенной функцией isNaN() :
JavaScript также имеет специальные значения Infinity (бесконечность) и -Infinity :
Проверить значение на Infinity , -Infinity и NaN можно с помощью встроенной функции isFinite() :
Строки
Строки в JavaScript - это последовательности символов Unicode (в кодировке UTF-16). Для тех, кто имеет дело с интернационализацией, это должно стать хорошей новостью. Если быть более точным, то строка - это последовательность кодовых единиц, каждая из которых представлена 16-битовым числом, а каждый символ Unicode состоит из 1 или 2 кодовых единиц.
Чтобы представить единственный символ, используйте строку, содержащую только этот символ.
Чтобы выяснить длину строки (в кодовых единицах), используйте свойство length :
Это уже первый шаг для работы с объектами! Мы уже говорили, что и строки можно использовать как объекты? У них тоже есть методы:
Другие типы
JavaScript дополнительно различает такие типы, как null , который указывает на преднамеренное отсутствующее значение, и undefined , указывающий на неинициализированное значение — то есть, значение, которое даже не было назначено. Мы поговорим о переменных позже, но в JavaScript можно объявить переменную без присвоения ей значения. В этом случае тип переменной будет "undefined" .
Ещё в JavaScript есть логический (булевый) тип данных, который может принимать два возможных значения true или false (оба являются ключевыми словами). Любое значение может быть преобразовано в логическое значение в соответствии со следующими правилами:
- false , 0 , пустая строка ( "" ), NaN , null и undefined преобразуются в false.
- Все остальные значения преобразуются в true.
Преобразование значений можно осуществить явно, используя функцию Boolean() :
Этот метод используется редко, так как JavaScript может автоматически преобразовывать типы в тех случаях, когда ожидается булевое значение, например в операторе if . Из-за того, что любой тип данных может быть преобразован в булевое значение, иногда говорят, что данные "истинные" или "ложные".
Для операций с логическими данными используются логические операторы: && (логическое И), || (логическое ИЛИ), ! (логическое НЕ).
Переменные
Для объявления новых переменных в JavaScript используются ключевые слова let , const или var .
let позволяет объявлять переменные, которые доступны только в блоке, в котором они объявлены:
const позволяет создавать переменные, чьи значения не предполагают изменений. Переменная доступна из блока, в котором она объявлена.
var наиболее общее средство объявления переменной. Оно не имеет ограничений, которые имеют два вышеописанных способа. Это потому, что это был изначально единственный способ объявления переменной в JavaScript. Переменная, объявленная с помощью var , доступна в пределах функции, в которой она объявлена.
Пример кода с переменной, объявленной с помощью var :
Если вы объявляете переменную без присвоения ей какого-либо значения, то её тип будет определён как undefined .
Важной особенностью языка JavaScript является то, что блоки данных не имеют своей области видимости, она есть только у функций. Поэтому, если объявить переменную через var в блоке данных (например, внутри контролирующей структуры if ), то она будет доступна всей функции. Следует отметить, что в новом стандарте ECMAScript Edition 6 появились инструкции let и const , позволяющие объявлять переменные с областью видимости, ограниченной пределами блока.
Операторы
JavaScript поддерживает такие операторы, как + , - , * , / и % , который возвращает остаток от деления (не путать с модулем (en-US)). Значения присваиваются с помощью оператора = , или с помощью составных операторов += и -= . Это сокращённая запись выражения x = x оператор y .
Так же используются операторы инкремента ( ++ ) и декремента ( -- ). Которые имеют префиксную и постфиксную форму записи.
Оператор + (en-US) так же выполняет конкатенацию (объединение) строк:
При сложении строкового и числового значений происходит автоматическое преобразование в строку. Поначалу такое может запутать:
Для приведения значения к строке просто прибавьте к нему пустую строку.
Для сравнения (en-US) в JavaScript используются следующие операторы: < , > , <= и >= . Сравнивать можно не только числа, но и строки. Проверка на равенство немного сложнее. Для проверки используют двойной ( == ) или тройной ( === ) оператор присваивания. Двойной оператор == осуществляет автоматическое преобразование типов, что может приводить к интересным результатам:
Если преобразование нежелательно, то используют оператор строгого равенства:
Для проверки на неравенство используют операторы != и !== .
Отдельного внимания стоят побитовые операторы (en-US), с которыми вы можете ознакомиться в соответствующем разделе.
Управляющие структуры
Управляющие структуры в JavaScript очень похожи на таковые в языках семейства C. Условные операторы выражены ключевыми словами if и else , которые можно составлять в цепочки:
В JavaScript есть три типа циклов: while , do-while и for . While используется для задания обычного цикла, а do-while целесообразно применить в том случае, если вы хотите, чтобы цикл был выполнен хотя бы один раз:
Цикл for похож на такой же в языках C и Java: он позволяет задавать данные для контроля за выполнением цикла:
JavaScript также содержит две других известных конструкции: for . of
Логические операторы && и || используют "короткий цикл вычисления", это значит, что вычисление каждого последующего оператора зависит от предыдущего. Например, полезно проверить существует ли объект или нет, прежде чем пытаться получить доступ к его свойствам:
Таким способом удобно задавать значения по умолчанию:
К условным операторам в JavaScript принадлежит также тернарный оператор " ? " :
Оператор switch используется при необходимости множественного сравнения:
Если в конце инструкции case не добавить останавливающую инструкцию break , то выполнение перейдёт к следующей инструкции case . Как правило, такое поведение нежелательно, но если вдруг вы решили его использовать, настоятельно рекомендуем писать соответствующий комментарий для облегчения поиска ошибок:
Вариант default опциональный. Допускается использование выражений как в условии switch , так и в cases . При проверке на равенство используется оператор строгого равенства === :
Объекты
Объекты в JavaScript представляют собой коллекции пар имя-значение (ключ-значение). Они похожи на:
- Словари в Python.
- Хеши в Perl и Ruby.
- Таблицы хешей в C и C++.
- HashMaps в Java.
- Ассоциативные массивы в PHP.
Именем свойства объекта в JavaScript выступает строка, а значением может быть любой тип данных JavaScript, даже другие объекты. Это позволяет создавать структуры данных любой сложности.
Существует два основных способа создать объект:
Обе эти записи делают одно и то же. Вторая запись называется литералом объекта и более удобная. Такой способ является основой формата JSON, и при написании кода лучше использовать именно его.
С помощью литерала объекта можно создавать не только пустые объекты, но и объекты с данными:
Доступ к свойствам объекта можно получить следующими способами:
Эти два метода равнозначны. Первый метод используется, если мы точно знаем к какому методу нам нужно обратиться. Второй метод принимает в качестве имени свойства строку, и позволяет вычислять имя в процессе вычислений. Следует отметить, что последний метод мешает некоторым движкам и минимизаторам оптимизировать код. Если появится необходимость назначить в качестве имён свойств объекта зарезервированные слова, то данный метод тоже может пригодиться:
На заметку: Стандарт EcmaScript 5 позволяет использовать зарезервированные слова в качестве имён свойств объекта без "оборачивания" их в кавычки. Подробнее в спецификации ES5.
Больше информации об объектах и прототипах: Object.prototype (en-US).
Для получения информации по прототипам объектов и цепям прототипов объектов смотрите Inheritance and the prototype chain.
Начиная с ECMAScript 2015, ключи объектов могут быть определены переменными с использованием квадратных скобок при создании: допустимо вместо var userPhone = ; userPhone[phoneType] = 12345 .
Массивы
Массивы в JavaScript всего лишь частный случай объектов. Работают они практически одинаково (если именем свойства является число, то доступ к нему можно получить только через вызов в скобках []), только у массивов есть одно удивительное свойство ' length ' (длина). Оно возвращает число, равное самому большому индексу массива + 1.
Создать массив можно по старинке:
Но гораздо удобнее использовать литерал массива:
Запомните, свойство array.length не обязательно будет показывать количество элементов в массиве. Посмотрите пример:
Запомните — длина массива это его самый большой индекс плюс один.
Если попытаться получить доступ к несуществующему элементу массива, то получите undefined :
Для перебора элементов массива используйте такой способ:
ES2015 представляет более краткий for . of способ обхода по итерируемым объектам, в т.ч. массивам:
Перебрать элементы массива также можно с помощью цикла for. in . Но, если вдруг будет изменено какое-либо свойство Array.prototype , то оно тоже будет участвовать в выборке. Не используйте данный метод.
И самый новый способ перебора свойств массива был добавлен в ECMAScript 5 — это метод forEach():
Для добавления данных в массив используйте метод push() :
У массивов есть ещё множество полезных методов. С их полным списком вы можете ознакомиться по ссылке.
Метод Описание a.toString() Возвращает строковое представление массива, где все элементы разделены запятыми. a.toLocaleString() Возвращает строковое представление массива в соответствии с выбранной локалью. a.concat(item1[, item2[, . [, itemN]]]) Возвращает новый массив с добавлением указанных элементов. a.join(sep) Преобразует массив в строку, где в качестве разделителя используется параметр sep a.pop() Удаляет последний элемент массива и возвращает его. a.push(item1, . itemN) Добавляет один или более элементов в конец массива. a.reverse() Меняет порядок элементов массива на обратный. a.shift() Удаляет первый элемент массива и возвращает его. a.slice(start[, end]) Возвращает новый массив. a.sort([cmpfn]) Сортирует данные в массиве. a.splice(start, delcount[, item1[, . [, itemN]]]) Позволяет вырезать из массива его часть и добавлять на это место новые элементы. a.unshift(item1[, item2[, . [, itemN]]]) Добавляет элементы в начало массива.
Функции
Наряду с объектами функции также являются ключевыми компонентами языка JavaScript. Базовые функции очень просты:
В этом примере показано практически всё, что нужно знать о функциях. Функции в JavaScript могут принимать ноль или более параметров. Тело функции может содержать любые выражения и определять свои собственные переменные, которые будут для этой функции локальными. Инструкция return используется для возврата значения и остановки выполнения функции. Если инструкции return в функции нет (или есть, но не указано возвращаемое значение), то JavaScript возвратит undefined .
Можно вызвать функцию, вообще не передавая ей параметры. В таком случае будет считаться, что их значения равны undefined :
Можно передать больше аргументов, чем ожидает функция:
Это может показаться бессмысленным, но на самом деле функции могут получить доступ к "лишним" аргументам с помощью псевдомассива arguments , в нём содержатся значения всех аргументов, переданных функции. Давайте напишем функцию, которая принимает неограниченное количество аргументов:
Или создадим функцию для вычисления среднего значения:
Это довольно полезно, но при этом кажется излишне подробным. Для уменьшения количества кода взглянем на замену использования массива аргументов синтаксисом остаточных параметров. В этом случае мы можем передавать в функцию любое количество аргументов, сохраняя код минималистичным. Оператор остаточных параметров используется в списке параметров функции в формате: . variable и включает в себя целый список аргументов, с которыми функция будет вызвана. Мы будем также использовать замену цикла for циклом for. of для получения значений, которые будет содержать наша переменная.
В вышенаписанном коде переменная args содержит все значения, которые были переданы в функцию.
Важно отметить, что где бы ни был размещён rest parameter operator в объявлении функции, он будет содержать все аргументы после его объявления, не раньше. например: function avg(firstValue, . args) будет хранить первое переданное значение в переменной firstValue и оставшиеся в args. Это ещё одно полезное свойство языка, однако оно ведёт нас к новой проблеме. avg() функция принимает список аргументов, разделённый запятыми. Но что если вы хотите найти среднее значение в массиве? Вы можете переписать функцию следующим образом:
На тот случай, если вы хотите использовать первый вариант функции, а не переписывать её заново, то в JavaScript есть возможность вызывать функцию с произвольным массивом аргументов. Для этого используется метод apply():
Вторым аргументом метода apply() передаётся массив, который будет передан функции в качестве аргументов. О первом аргументе мы поговорим позже. Наличие у функций методов также говорит о том, что на самом деле они являются объектами.
Этот же результат можно получить, используя spread operator в вызове функции.
For instance: avg(. numbers)
В JavaScript можно создавать анонимные функции:
Данная запись семантически равнозначна записи function avg() . Это даёт возможность использовать разные интересные трюки. Вот посмотрите, как можно "спрятать" локальные переменные в функции:
В JavaScript есть возможность рекурсивного вызова функции. Это может оказаться полезным при работе с иерархическими (древовидными) структурами данных (например такие, которые встречаются при работе с DOM).
Тут мы сталкиваемся с проблемой: как вызвать функцию рекурсивно, если у неё нет имени? Для этого в JavaScript есть именованные функциональные выражения IIFEs (Immediately Invoked Function Expressions). Вот пример использования именованной самовызывающейся функции:
Имя функции в примере доступно только внутри самой функции. Это улучшает оптимизацию и читаемость кода.
Собственные объекты
В классическом Объектно-Ориентированном Программировании (ООП) объекты — это коллекции данных и методов, которые этими данными оперируют. JavaScript - это язык, основанный на прототипах, и в его определении нет понятия классов, таких, как в языках C++ или Java. (Иногда это может запутать программистов, знакомых с языками, в которых есть классы.) Вместо классов JavaScript использует функции. Давайте представим объект с личными данными, содержащий поля с именем и фамилией. Есть два типа отображения имён: "Имя Фамилия" или "Фамилия, Имя". С помощью объектов и функций можно сделать следующее:
Работает, но сам код никуда не годится. С таким подходом у вас будут десятки функций, засоряющих глобальный объект. Это можно исправить, прикрепив функцию к объекту. Это просто, ведь все функции и есть объекты:
А вот кое-что новенькое: ключевое слово ' this '. Когда ' this ' используется внутри функции, оно ссылается на текущий объект. Значение ключевого слова зависит от способа вызова функции. Если вызвать функцию с обращением к объекту через точку или квадратные скобки, то ' this ' получится равным данному объекту. В ином случае ' this ' будет ссылаться на глобальный объект. Это часто приводит к ошибкам. Например:
При вызове fullName() , ' this ' получает ссылку на глобальный объект. А так как в глобальном объекте не определены переменные first и last , то имеем два undefined .
Используя особенность ключевого слова ' this ', можно улучшить код функции makePerson :
В примере мы использовали новое ключевое слово: ' new '. Оно тесно связано с 'this'. Данное ключевое слово создаёт новый пустой объект, а потом вызывает указанную функцию, а this получает ссылку на этот новый объект. Функции, которые предназначены для вызова с ' new ' называются конструкторами. Существует соглашение, согласно которому все функции-конструкторы записываются с заглавной буквы.
Мы доработали наш код в предыдущем примере, но всё равно остался один неприятный момент с самостоятельным вызовом fullName() .
Каждый раз, когда с помощью конструктора создаётся новый объект, мы заново создаём и две новые функции. Гораздо удобнее создать эти функции отдельно и дать доступ к ним конструктору:
Уже лучше: мы создали функции-методы только один раз, а при новом вызове функции-конструктора просто ссылаемся на них. Можно сделать ещё лучше? Конечно:
Person.prototype это объект, доступ к которому есть у всех экземпляров класса Person . Он создаёт особую цепочку прототипов. Каждый раз, когда вы пытаетесь получить доступ к несуществующему свойству объекта Person , JavaScript проверяет, существует ли свойство в Person.prototype . В результате все, что передано в Person.prototype , становится доступным и всем экземплярам этого конструктора через this объект.
Это очень мощный инструмент. JavaScript позволяет изменять прототипы в любое время, это значит, что можно добавлять новые методы к существующим объектам во время выполнения программы:
Занимательно то, что добавлять свойства в прототип можно и для встроенных объектов JavaScript. Давайте добавим новый метод reversed классу String , этот метод будет возвращать строку задом наперёд:
Данный метод будет работать даже на литералах строки!
Как уже упоминалось, prototype формирует часть цепочки. Конечным объектом этой цепочки прототипов является Object.prototype , методы которого включают и toString() — тот метод, который вызывается тогда, когда надо получить строковое отображение объекта. Вот что можно сделать с нашими объектами Person :
Помните, мы вызывали avg.apply() с первым аргументом равным null? Теперь мы можем сделать так: первым аргументом, переданным методу apply() будет объект, который примет значение ' this '. Вот к примеру упрощённая реализация ' new ':
Это не точная копия new , так как она не устанавливает цепочку прототипов (это сложно ). Метод apply() применяется не очень часто, но знать его важно. В примере выше, синтаксис . args (включая многоточие) называется остаточными параметрами, потому что они включают в себя все оставшиеся аргументы.
практически полностью эквивалентен этому:
В JavaScript метод apply() имеет похожий метод call() , который тоже позволяет устанавливать ' this ', но принимает список, а не массив аргументов.
Вложенные функции
Объявлять новые функции можно и внутри других функций. Мы использовали этот приём чуть выше, создавая функцию makePerson() . Главная особенность вложенных функций в том, что они получают доступ к переменным, объявленным в их функции-родителе:
Это очень полезное свойство, которое делает сопровождение кода более удобным. Если ваша функция в своей работе использует другие функции, которые больше нигде не используются, то можно просто вложить вспомогательные функции в основную. Это сократит количество функций в глобальном объекте, что довольно неплохо.
Ещё это отличный способ сократить количество глобальных переменных. Так при написании кода у нас часто будет возникать искушение понасоздавать глобальных переменных, которые будут доступны разным функциям. Всё это усложняет код, делает его менее читаемым. Вложенные функции имеют доступ к переменным своей функции-родителя, и мы можем использовать это для группировки множества функций вместе (естественно в разумных пределах), что позволит держать наш глобальный объект в чистоте и порядке.
Замыкания (Closures)
Мы подошли к одному из самых мощных и непонятных инструментов JavaScript. Давайте разберёмся.
Функция makeAdder создаёт новую функцию, которая прибавляет полученное значение к значению, которые было получено при создании функции.
Такой же фокус мы наблюдали в предыдущем примере, когда внутренние функции получали доступ к переменным той функции, в которой были объявлены. Только в нашем примере основная функция возвращает вложенную. Поначалу может показаться, что локальные переменные при этом перестанут существовать. Но они продолжают существовать — иначе код попросту не сработал бы. Вдобавок ко всему у нас есть две разные "копии" функции makeAdder , присвоенные разным переменным (одна копия, в которой а - это 5, а во второй а - это 20). Вот что имеем в результате вызова:
И вот что произошло: когда JavaScript выполняет функцию, создаётся объект 'scope', который содержит в себе все локальные переменные, объявленные внутри этой функции. Он инициализируется любым значением, переданным функции в качестве параметра. 'Scope' подобен глобальному объекту, который содержит все глобальные переменные и функции, кроме нескольких важных отличий: при каждом вызове функции создаётся новый объект 'scope' и, в отличие от глобального, к объекту 'scope' нельзя получить прямой доступ из вашего кода. И нет способа пройтись по свойствам данного объекта.
Так что при вызове функции makeAdder создаётся новый объект 'scope' с единственным свойством: a , которому присваивается значение, переданное функции в качестве аргумента. Потом makeAdder возвращает новую анонимную функцию. В любом другом случае 'сборщик мусора' удалил бы объект scope, но возвращаемая функция ссылается на этот объект. В итоге объект scope не удаляется до тех пор, пока существует хотя бы одна ссылка на него.
Все объекты scope соединяются в цепочку областей видимости, которая похожа на цепочку прототипов в объектной системе JavaScript.
Замыкание это связка из функции и объекта scope, созданного при её вызове. Подробнее о замыканиях здесь.
Found a problem with this page?Last modified: Feb 22, 2022 , by MDN contributors
Mozilla© 2005- 2022 Mozilla and individual contributors. Content is available under these licenses.