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

Часть 1: первая программа, особенности языка, стандарты
Часть 2: стиль кода и структура программ
Часть 3: переменные, типы данных, выражения, объекты
Часть 4: функции
Часть 5: массивы и циклы
Часть 6: исключения, точка с запятой, шаблонные литералы
Часть 7: строгий режим, ключевое слово this, события, модули, математические вычисления
Часть 8: обзор возможностей станда��та ES6
Часть 9: обзор возможностей стандартов ES7, ES8 и ES9



Переменные


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

Сама по себе переменная в JavaScript не содержит информацию о типе значений, которые будут в ней храниться. Это означает, что записав в переменную, например, строку, позже в неё можно записать число. Такая операция ошибки в программе не вызовет. Именно поэтому JavaScript иногда называют «нетипизированным» языком.

Прежде чем использовать переменную, её нужно объявить с использованием ключевого слова var или let. Если речь идёт о константе, применяется ключевое слово const. Объявить переменную и присвоить ей некое значение можно и не используя эти ключевые слова, но делать так не рекомендуется.

▍Ключевое слово var


До появления стандарта ES2015 использование ключевого слова var было единственным способом объявления переменных.

var a = 0

Если в этой конструкции опустить var, то значение будет назначено необъявленной переменной. Результат этой операции зависит от того, в каком режиме выполняется программа.

Так, если включён так называемый строгий режим (strict mode), подобное вызовет ошибку. Если строгий режим не включён, произойдёт неявное объявление переменной и она будет назначена глобальному объекту. В частности, это означает, что переменная, неявно объявленная таким образом в некоей функции, окажется доступной и после того, как функция завершит работу. Обычно же ожидается, что переменные, объявляемые в функциях, не «выходят» за их пределы. Выглядит это так:

function notVar() {
  bNotVar = 1 //лучше так не делать
}
notVar()
console.log(bNotVar)

В консоль попадёт 1, такого поведения от программы обычно никто не ждёт, выражение bNotVar = 1 выглядит не как попытка объявления и инициализации переменной, а как попытка обратиться к переменной, находящейся во внешней по отношению к функции области видимости (это — вполне нормально). Как результат, неявное объявление переменных сбивает с толку того, кто читает код и может приводить к неожиданному поведению программ. Позже мы поговорим и о функциях, и об областях видимости, пока же постарайтесь всегда, когда смысл некоего выражения заключается в объявлении переменной, пользоваться специализированными ключевыми словами. Если в этом примере тело функции переписать в виде var bNotVar = 1, то попытка запустить вышеприведённый фрагмент кода приведёт к появлению сообщения об ошибке (его можно увидеть в консоли браузера).

Выглядеть оно, например, может так: Uncaught ReferenceError: bNotVar is not defined. Смысл его сводится к тому, что программа не может работать с несуществующей переменной. Гораздо лучше, при первом запуске программы, увидеть такое сообщение об ошибке, чем писать непонятный код, который способен неожиданно себя вести.

Если, при объяв��ении переменной, её не инициализируют, не присваивают ей какого-либо значения, ей автоматически будет присвоено значение undefined.

var a //typeof a === 'undefined'

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

var a = 1
var a = 2

В одном выражении можно объявить несколько переменных:

var a = 1, b = 2

Областью видимости переменной (scope) называют участок программы, в котором доступна (видима) эта переменная.

Переменная, инициализированная с помощью ключевого слова var за пределами какой-либо функции, назначается глобальному объекту. Она имеет глобальную область видимости и доступна из любого места программы. Если переменная объявлена с использованием ключевого слова var внутри функции, то она видна только внутри этой функции, являясь для неё локальной переменной.

Если в функции, с использованием var, объявлена переменная, имя которой совпадает с именем некоей переменной из глобальной области видимости, она «перекроет» глобальную переменную. То есть, при обращении к такой переменной внутри функции будет использоваться именно её локальный вариант.

Важно понимать, что блоки (области кода, заключённые в фигурные скобки) не создают новых областей видимости. Новая область видимости создаётся при вызове функции. Ключевое слово var имеет так называемую функциональную область видимости, а не блочную.

Если в коде функции объявлена некая переменная, она видна всему коду функции. Даже если переменная объявлена с помощью var в конце кода функции, обратиться к ней можно и в начале кода, так как в JavaScript работает механизм поднятия переменных (hoisting). Этот механизм «поднимает» объявления переменных, но не операции их инициализации. Это может стать источником путаницы, поэтому возьмите себе за правило объявлять переменные в начале функции.

▍Ключевое слово let


Ключевое слово let появилось в ES2015, его, упрощённо, можно назвать «блочной» версией var. Область видимости переменных, объявленных с помощью ключевого слова let, ограничивается блоком, оператором или выражением, в котором оно объявлено, а также вложенными блоками.

Если само слово «let» кажется не очень понятным, можно представить, что вместо него используется слово «пусть». Тогда выражение let color = 'red' можно перевести на английский так: «let the color be red», а на русский — так: «пусть цвет будет красным».

При использовании ключевого слова let можно избавиться от неоднозначностей, сопутствующих ключевому слову var (например, не удастся два раза, используя let, объявить одну и ту же переменную). Использование let за пределами функции, скажем, при инициализации циклов, не приводит к созданию глобальных переменных.

Например, такой код вызовет ошибку:

for (let i = 0; i < 5; i++) {
    console.log(i)
}
console.log(i)

Если же, при инициализации цикла, счётчик i будет объявлен с использованием ключевого слова var, то i будет доступно и за пределами цикла, после того, как он завершит работу.

В наши дни, при разработке JS-программ на основе современных стандартов, вполне можно полностью отказаться от var и использовать только ключевые слова let и const.

▍Ключевое слово const


Значения переменных, объявленных с использованием ключевых слов var или let, могут быть перезаписаны. Если же вместо этих ключевых слов используется const, то объявленной и инициализированной с его помощью константе новое значение присвоить нельзя.

const a = 'test'

В данном примере константе a нельзя присвоить новое значение. Но надо отметить, что если a — это не примитивное значение, наподобие числа, а объект, использование ключевого слова const не защищает этот объект от изменений.

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

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

const obj = {}
console.log(obj.a)
obj.a = 1 //работает
console.log(obj.a)
//obj = 5 //вызывает ошибку

В константу obj, при инициализации, записывается новый пустой объект. Попытка обращения к его свойству a, несуществующему, ошибки не вызывает. В консоль попадает undefined. После этого мы добавляем в объект новое свойство и снова пытаемся обратиться к нему. В этот раз в консоль попадает значение этого свойства — 1. Если раскомментировать последнюю строку примера, то попытка выполнения этого кода приведёт к ошибке.

Ключевое слово const очень похоже на let, в частности, оно обладает блочной областью видимости.

В современных условиях вполне допустимо использовать для объявления всех сущностей, значения которых менять не планируется, ключевое слово const, прибегая к let только в особых случаях. Почему? Всё дело в том, что лучше всего стремиться к использованию как можно более простых из доступных конструкций для того, чтобы не усложнять программы и избегать ошибок.

Типы данных


JavaScript иногда называют «нетипизированным» языком, но это не соответствует реальному положению дел. В переменные, и правда, можно записывать значения разных типов, но типы данных в JavaScript, всё-таки, есть. В частности, речь идёт о примитивных и об объектных типах данных.

Для того чтобы определить тип данных некоего значения, можно воспользоваться оператором typeof. Он возвращает строку, указывающую тип операнда.

▍Примитивные типы данных


Вот список примитивных типов данных JavaScript:

  • number (число)
  • string (строка)
  • boolean (логическое значение)
  • null (специальное значение null)
  • undefined (специальное значение undefined)
  • symbol (символ, используется в особых случаях, появился в ES6)

Здесь названия типов данных приведены в том виде, в котором их возвращает оператор typeof.

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

Тип number


Значения типа number в JavaScript представлены в виде 64-битных чисел двойной точности с плавающей запятой.

В коде числовые литералы представлены в виде целых и дробных чисел в десятичной системе счисления. Для записи чисел можно использовать и другие способы. Например, если в начале числового литерала имеется префикс 0x — он воспринимается как число, записанное в шестнадцатеричной системе счисления. Числа можно записывать и в экспоненциальном представлении (в таких числах можно найти букву e).

Вот примеры записи целых чисел:

10
5354576767321
0xCC // шестнадцатеричное число

Вот дробные числа.

3.14
.1234
5.2e4 //5.2 * 10^4

Числовые литералы (такое поведение характерно и для некоторых других примитивных типов), при попытке обращения к ним как к объектам, автоматически, на время выполнения операции, преобразуются в соответствующие объекты, которые называют «объектными обёртками». В данном случае речь идёт об объектной обёртке Number.

Вот, например, как выглядит попытка обратиться к переменной a, в которую записан числовой литерал, как к объекту, в консоли Google Chrome.


Подсказка по объектной обёртке Number

Если, например, воспользоваться методом toString() объекта типа Number, он возвратит строковое представление числа. Выглядит соответствующая команда, которую можно выполнить в консоли браузера (да и в обычном коде) так:

a.toString()

Обратите внимание на двойные скобки после имени метода. Если их не поставить, система не выдаст ошибку, но, вместо ожидаемого вывода, в консоли окажется нечто, совсем не похожее на строковое представление числа 5.

Глобальный объект Number можно использовать в виде конструктора, создавая с его помощью новые числа (правда, в таком виде его практически никогда не используют), им можно пользоваться и как самостоятельной сущностью, не создавая его экземпляры (то есть — некие числа, представляемые с его помощью). Например, его свойство Number.MAX_VALUE содержит максимальное числовое значение, представимое в JavaScript.

Тип string


Значения типа string представляют собой последовательности символов. Такие значения задают в виде строковых литералов, заключённых в одинарные или двойные кавычки.

'A string'
"Another string"

Строковые значения можно разбивать на несколько частей, используя символ обратной косой черты (backslash).

"A \
string"

Строка может содержать так называемые escape-последовательности, интерпретируемые при выводе строки в консоль. Например, последовательность \n означает символ перевода строки. Символ обратной косой черты можно использовать и для того, чтобы добавлять кавычки в строки, заключённые в такие же кавычки. Экранирование символа кавычки с помощью \ приводит к тому, что система не воспринимает его как специальный символ.

'I\'m a developer'

Строки можно конкатенировать с использованием оператора +.

"A " + "string"

Шаблонные литералы


В ES2015 появились так называемые шаблонные литералы, или шаблонные строки. Они представляют собой строки, заключённые в обратные кавычки (`) и обладают некоторыми интересными свойствами.

`a string`

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

`a string with ${something}`
`a string with ${something+somethingElse}`
`a string with ${obj.something()}`

Использование обратных кавычек упрощает многострочную запись строковых литералов:

`a string
with
${something}`

Тип boolean


В JavaScript есть пара зарезервированных слов, использующихся при работе с логическими значениями — это true (истина), и false (ложь). Операции сравнения, например, такие, как ==, ===, <, >, возвращают true или false.

Логические выражения используются в конструкциях наподобие if и while, помогая управлять ходом выполнения программы.

При этом надо отметить, что там, где ожидается значение true или false, можно использовать и другие значения, которые автоматически расцениваются языком как истинные (truthy) или ложные (falsy).

В частности, ложными значениями являются следующие:

0
-0
NaN
undefined
null
'' //пустая строка

Остальные значения являются истинными.

Тип null


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

Тип undefined


Значение undefined, записанное в некую переменную, указывает на то, что эта переменная не инициализирована и значение для неё отсутствует.

Это значение автоматически возвращается из функций, результат работы которых не возвращается явно, с использованием ключевого слова return. Если функция принимает некий параметр, который, при её вызове, не указан, он также устанавливается в undefined.

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

typeof variable === 'undefined'

▍Объекты


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

Выражения


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

Арифметические выражения


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

1 / 2
i++
i -= 2
i * 2

Строковые выражения


Результатом вычисления таких выражений являются строки.

'A ' + 'string'
'A ' += 'string'

Первичные выражения


В эту категорию попадают литералы, константы, ссылки на идентификаторы.

2
0.02
'something'
true
false
this //контекст выполнения, ссылка на текущий объект
undefined
i //где i является переменной или константой

Сюда же можно отнести и некоторые ключевые слова и конструкции JavaScript.

function
class
function* //генератор
yield //команда приостановки/возобновления работы генератора
yield* //делегирование другому итератору или генератору
async function* //асинхронное функциональное выражение 
await //организация ожидания выполнения асинхронной функции
/pattern/i //регулярное выражение
() //группировка

Выражения инициализации массивов и объектов


[] //литерал массива
{} //объектный литерал
[1,2,3]
{a: 1, b: 2}
{a: {b: 1}}

Логические выражения


В логических выражениях используются логические операторы, результатом их вычисления оказываются логические значения.

a && b
a || b
!a

Выражения доступа к свойствам


Эти выражения позволяют обращаться к свойствам и методам объектов.

object.property //обращение к свойству (или методу) объекта
object[property]
object['property']

Выражения создания объектов


new object()
new a(1)
new MyRectangle('name', 2, {a: 4})

Выражения объявления функций


function() {}
function(a, b) { return a * b }
(a, b) => a * b
a => a * 2
() => { return 2 }

Выражения вызова


Такие выражения используются для вызова функций или методов объектов.

a.x(2)
window.resize()

Работа с объектами


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

▍Прототипное наследование


JavaScript выделяется среди современных языков программирования тем, что поддерживает прототипное наследование. Большинство же объектно-ориентированных языков используют модель наследования, основанную на классах.

У каждого JavaScript-объекта есть особое свойство (__proto__), которое указывает на другой объект, являющий��я его прототипом. Объект наследует свойства и методы прототипа.

Предположим, у нас имеется объект, созданный с помощью объектного литерала.

const car = {}

Или мы создали объект, воспользовавшись конструктором Object.

const car = new Object()

В любом из этих случаев прототипом объекта car будет Object.prototype.

Если создать массив, который тоже является объектом, его прототипом будет объект Array.prototype.

const list = []
//или так
const list = new Array()

Проверить это можно следующим образом.

car.__proto__ == Object.prototype //true
car.__proto__ == new Object().__proto__ //true
list.__proto__ == Object.prototype //false
list.__proto__ == Array.prototype //true
list.__proto__ == new Array().__proto__ //true

Здесь мы пользовались свойством __proto__, оно не обязательно должно быть доступно разработчику, но обычно обращаться к нему можно. Надо отметить, что более надёжным способом получить прототип объекта является использование метода getPrototypeOf() глобального объекта Object.

Object.getPrototypeOf(new Object())

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


Подсказка по массиву

Базовым прототипом для всех объектов является Object.prototype.

Array.prototype.__proto__ == Object.prototype

У Object.prototype прототипа нет.

То, что мы видели выше, является примером цепочки прототипов.

При попытке обращения к свойству или методу объекта, если такого свойства или метода у самого объекта нет, их поиск выполняется в его прототипе, потом — в прототипе прототипа, и так — до тех пор, пока искомое будет найдено, или до тех пор, пока цепочка прототипов не кончится.

Помимо создания объектов с использованием оператора new и применения объектных литералов или литералов массивов, создать экземпляр объекта можно с помощью метода Object.create(). Первый аргумент, передаваемый этому методу, представляет собой объект, который станет прототипом создаваемого с его помощью объекта.

const car = Object.create(Object.prototype)

Проверить, входит ли некий объект в цепочку прототипов другого объекта, можно с использованием метода isPrototypeOf().

const list = []
Array.prototype.isPrototypeOf(list)

Функции-конструкторы


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

function Person(name) {
  this.name = name
}

Person.prototype.hello = function() {
  console.log(this.name)
}

let person = new Person('Flavio')
person.hello()

console.log(Person.prototype.isPrototypeOf(person))

Здесь мы создаём функцию-конструктор. При её вызове создаётся новый объект, на который указывает ключевое слово this в теле конструктора. Мы добавляем в этот объект свойство name и записываем в него то, что передано конструктору. Этот объект возвращается из конструктора автоматически. С помощью функции-конструктора можно создать множество объектов, свойства name которых будут содержать то, что передано при их создании конструктору.

После создания конструктора мы добавляем в его прототип функцию, которая будет выводить в консоль значение свойства name объекта, созданного с помощью этой функции. Все объекты, созданные с помощью этого конструктора, будут иметь один и тот же прототип, а значит и пользоваться одной и той же функцией hello(). Это несложно проверить, создав ещё один объект типа Person и сравнив его функцию hello() с функцией уже имеющегося в примере объекта (имя функции в таком случае записывают без скобок).

▍Классы


В стандарте ES6 в JavaScript пришло такое понятие как «класс».

До этого в JavaScript можно было пользоваться лишь вышеописанным механизмом прототипного наследования. Этот механизм непривычно выглядел для программистов, пришедших в JS из других языков. Поэтому в языке и появились классы, которые, по сути, являются «синтаксическим сахаром» для прототипного механизма наследования. То есть, и объекты, созданные традиционным способом, и объекты, созданные с использованием классов, имеют прототипы.

Объявление класса


Вот как выглядит объявление класса.

class Person {
  constructor(name) {
    this.name = name
  }
  hello() {
    return 'Hello, I am ' + this.name + '.'
  }
}

У класса есть идентификатор, который можно использовать для создания новых объектов с применением конструкции new ClassIdentifier().

При создании нового объекта вызывается метод constructor, ему передаются параметры.

В классе можно объявлять методы. В нашем случае hello() — это метод, который могут вызывать все объекты, созданные на основе класса. Вот как выглядит создание нового объекта с использованием класса Person.

const flavio = new Person('Flavio')
flavio.hello()

Наследование, основанное на классах


Классы могут расширять другие классы. Объекты, созданные на основе таких классов, будут наследовать и методы исходного класса, и методы, заданные в расширенном классе.

Если класс, расширяющий другой класс (наследник этого класса) имеет метод, имя которого совпадает с тем, который есть у класса-родителя, этот метод имеет преимущество перед исходным.

class Programmer extends Person {
  hello() {
    return super.hello() + ' I am a programmer.'
  }
}
const flavio = new Programmer('Flavio')
flavio.hello()

При вызове метода hello() в вышеприведённом примере будет возвращена строка Hello, I am Flavio. I am a programmer.

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

Внутри класса можно обращаться к родительскому классу с использованием ключевого слова super.

Статические методы


Методы, описываемые в классе, можно вызывать, обращаясь к объектам, созданным на основе этого класса, но не к самому классу. Статические (static) методы можно вызывать, обращаясь непосредственно к классу.

Приватные методы


В JavaScript нет встроенного механизма, который позволяет объявлять приватные (частные, закрытые) методы. Это ограничение можно обойти, например, с использованием замыканий.

Геттеры и сеттеры


В классе можно описывать методы, предваряя их ключевыми словами get или set. Это позволяет создавать так называемые геттеры и сеттеры — функции, которые используются для управления доступом к свойствам объектов, созданных на основе класса. Геттер вызывается при попытке чтения значения псевдо-свойства, а сеттер — при попытке записи в него нового значения.

class Person {
    constructor(name) {
      this.userName = name
    }
    set name(value) {
      this.userName = value
    }
    get name() {
      return this.userName
    }
  }

Итоги


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

Уважаемые читатели! Если вы уже давно пишете на JS, просим рассказать о том, как вы относитесь к появлению в языке ключевого слова class.