Переменные: let и const

В ES-2015 предусмотрены новые способы объявления переменных: через let и const вместо var.

У объявлений переменной через let есть три основных отличия от var:

  • Область видимости переменной let – блок {...}

    Как мы помним, переменная, объявленная через var, видна везде в функции. Переменная, объявленная через let, видна только в рамках блока {...}, в котором объявлена. Это, в частности, влияет на объявления внутри if, while или for.

    Например, переменная через var:
    var apples = 5;
    
    if (true) {
        var apples = 10;
    
        alert(apples); // 10 (внутри блока)
    }
    
    alert(apples); // 10 (снаружи блока то же самое)

    В примере выше apples – одна переменная на весь код, которая модифицируется в if.

    То же самое с let будет работать по-другому:

    let apples = 5; // (*)
    
    if (true) {
        let apples = 10;
    
        alert(apples); // 10 (внутри блока)
    }
    
    alert(apples); // 5 (снаружи блока значение не изменилось)

    Здесь, фактически, две независимые переменные apples, одна – глобальная, вторая – в блоке if. Заметим, что если объявление let apples в первой строке (*) удалить, то в последнем alert будет ошибка: переменная не определена. Это потому что переменная let всегда видна именно в том блоке, где объявлена, и не более.

  • Переменная let видна только после объявления

    Как мы помним, переменные var существуют и до объявления. Они равны undefined. С переменными let всё проще. До объявления их вообще нет. Заметим также, что переменные let нельзя повторно объявлять.

  • При использовании в цикле, для каждой итерации создаётся своя переменная

    Переменная var – одна на все итерации цикла и видна даже после цикла:
    for(var i=0; i<10; i++) { /* … */ }
    
    alert(i); // 10

    С переменной let – всё по-другому.

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

const

Объявление const задаёт константу, то есть переменную, которую нельзя менять.

const apple = 5;
apple = 10; // ошибка

В остальном объявление const полностью аналогично let. Заметим, что если в константу присвоен объект, то от изменения защищена сама константа, но не свойства внутри неё.


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

function sum(a, b, c){
    let d = a + b + c;
    console.log(d);
}

sum(1, 2, 3);  // 6

let nums = [4, 5, 6];
sum(...nums);  // 15

Во втором случае в функцию передается числа из массива nums. Но чтобы передавался не просто массив, как одно значение, а именно числа из этого массива, применяется spread-оператор (многоточие ...).


Декомпозиция

Декомпозиция (destructuring) позволяет извлечь из объекта отдельные значения в переменные:

let user = {
    name: "Tom",
    age: 24,
    phone: "+367438787",
    email: "tom@gmail.com"
};

let {name, email} = user;
console.log(name);      // Tom
console.log(email);     // tom@gmail.com

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

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

let user = {
    name: "Tom",
    age: 24,
    phone: "+367438787",
    email: "tom@gmail.com"
};

let {name: userName, email: mailAddress} = user;
console.log(userName);      // Tom
console.log(mailAddress);   // tom@gmail.com

В данном случае свойство name сопоставляется с переменной userName, а поле email - с переменной mailAddress.

Декомпозиция массивов

Также можно декомпозировать массивы:

let users = ["Tom", "Sam", "Bob"];
let [a, b, c] = users;

console.log(a);     // Tom
console.log(b);     // Sam
console.log(c);     // Bob

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

let users = ["Tom", "Sam", "Bob", "Ann", "Alice", "Kate"];
let [first,,,,fifth] = users;

console.log(first);     // Tom
console.log(fifth);     // Alice

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

Подобным образом можно получить, например, второй и четвертый элементы:

let users = ["Tom", "Sam", "Bob", "Ann", "Alice", "Kate"];
let [,second,,forth] = users;

console.log(second);        // Sam
console.log(forth);         // Ann

Можно совместить получение данных из массива и объекта:

let people = [
    {name: "Tom", age: 34},
    {name: "Bob", age: 23},
    {name: "Sam", age: 32}
];
let [,{name}] = people;

console.log(name);      // Bob

В данном случае получаем значение свойства name второго объекта в массиве.

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


Стрелочные функции

Стрелочные функции (arrow functions) представляют сокращенную версию обычных функций. Стрелочные функции образуются с помощью знака стрелки (=>), перед которым в скобках идут параметры функции, а после - собственно тело функции. Например:

let sum = (x, y) => x + y;
let a = sum(4, 5);
let b = sum(10, 5);

console.log(a);  // 9
console.log(b);  // 15

В данном случае функция (x, y) => x + y осуществляет сложение двух чисел и присваивается переменной sum. Функция принимает два параметра - x и y. Ее тело составляет сложение значений этих параметров. И поскольку после стрелки фактически идет конкретное значение, которое представляет сумму чисел, то функция возвращает это значение. И мы можем через переменную sum вызвать данную функцию и получить ее результат в переменные a и b.

Если функция принимает один параметр, то скобки вокруг него можно опустить:

let square = n => n * n;

console.log(square(5));     // 25
console.log(square(6));     // 36
console.log(square(-7));    // 49

Если тело функции представляет набор выражений, то они облекаются в фигурные скобки:

let square = n => {
    let result = n * n;
    return result;
};

console.log(square(5));     // 25

Для возвращения результата из функции в таком случае применяется стандартный оператор return.

Если стрелочная функция не принимает никаких параметров, то ставятся пустые скобки:

let hello = ()=> console.log("Hello World");
hello();  // Hello World

ООП в JS

ООП - парадигма программирования, в котором основными концепциями являются понятия классы и объекты.

Нужно понимать, что, в отличие от процедурного программирования, ООП следует следующим архитектурным идеям:

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

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

Класс (class) - это тип, описывающий устройство объектов. Все экземпляры одного класса (объекты, порожденные от одного класса) имеют один и тот же набор свойств и общее поведение, то есть одинаково реагируют на одинаковые сообщения.

Объект (object) - сущность, появляющаяся при создании экземпляра класса.

Говорят, что объект - это экземпляр класса. Говоря иначе, класс - это идея, а объект - её реализация.

Поля (свойства, properties) класса - данные, которые хранит класс.

Метод (method) класса - функция, объявленная внутри класса.

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

Поведение (behavior) - действия и реакции объекта, выраженные в терминах передачи сообщений и изменения состояния; видимая извне и воспроизводимая активность объекта.

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

- Инкапсуляция (encapsulation)

Это сокрытие реализации класса и отделение его внутреннего представления от внешнего (интерфейса).

Инкапсуляция JavaScript реализована через разделение данных объекта внутри класса. В отличии от классических ООП языков, JS не имеет разных степеней инкапсуляции (public, protected и private). Это означает, что ограничения доступа к данным по сути нет.

- Наследование (inheritance)

Это отношение между классами, при котором класс использует структуру или поведение другого класса (одиночное наследование), или других (множественное наследование) классов. Класс, от которого производится наследование, называется родительским, а новый класс — потомком или дочерним классом.

- Полиморфизм (polymorphism)

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

В JS почти все содержимое кода является объектом. Исключением являются null и undefined, которые не обрабатываются как объекты.

Методы и свойства объекта в ООП языках делятся на две большие группы:

  • Внутренний интерфейс - методы и свойства, которые доступны только из других методов того же конкретного объекта.
  • Внешний интерфейс - методы и свойства, которые доступны для всех объектов в текущем запуске приложения.

'use strict';
class User {
    constructor(name, age, phone, email) {
        this.name = name;
        this.age = age;
        this.phone = phone;
        this.email = email;
    }

    showInfo() {
        console.log('User information:');
        console.log('Name: ', this.name);
        console.log('Age: ', this.age);
        console.log('Phone: ', this.phone);
        console.log('Email: ', this.email);
    }
}

class Developer extends User {
    constructor(name, age, phone, email, department, salary) {
        super(name, age, phone, email);
        this.department = department;
        this.salary = salary;
    }

    showInfo() {
        super.showInfo();
        console.log('Extends user information:');
        console.log('Department: ', this.department);
        console.log('Salary: ', this.salary);
    }

    run() {
        this._myMethod('Hello, user!');
    }

    _myMethod(message) {
        console.log('Message: ', message);
    }
}

let user = new User('John', 25, '111', 'john@mail.com');
console.log(user);  // User {name: "John", age: 25, phone: "111", email: "john@mail.com"}
user.showInfo();  // User information: Name: John Age: 25 Phone: 111 Email: john@mail.com

let dev = new Developer('Orkhan', 26, '999', 'orkhanalyshov@gmail.com', 'Full-Stack Developer', 'example');
console.log(dev);  // Developer {name: "Orkhan", age: 26, phone: "999", email: "orkhanalyshov@gmail.com", department: "Full-Stack Developer", salary: "example"}
dev.showInfo();  // User information: Name: Orkhan Age: 26 Phone: 999 Email: orkhanalyshov@gmail.com Extends user information: Department: Full-Stack Developer Salary: example
dev.run();  // Message: Hello, user!

Классы

С внедреием стандарта ES2015 (ES6) в JavaScript появился новый способ определения объектов - с помощью классов. Класс представляет описание объекта, его состояния и поведения, а объект является конкретным воплощением или экземпляром класса.

Для определения класса используется ключевое слово class:

class Person {
    
}

После слова class идет название класса (в данном случае класс называется Person), и затем в фигурных скобках определяется тело класса. Также можно определить анонимный класс и присвоить его переменной:

let Person = class{}

После этого мы можем создать объекты класса с помощью конструктора:

class Person {
    
}

let tom = new Person();
let bob = new Person();

Для создания объекта с помощью конструктора сначала ставится ключевое слово new. Затем собственно идет вызов конструктора - по сути вызов функции по имени класса. По умолчанию классы имеют один конструктор без параметров. Поэтому в данном случае при вызове конструктора в него не передаются аргументы.

Также мы можем определить в классе свои конструкторы. Также класс может содержать свойства и методы:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    display() {
        console.log(this.name, this.age);
    }
}

let tom = new Person("Tom", 34);
tom.display();          // Tom 34
console.log(tom.name);  // Tom

Конструктор определяется с помощью метода с именем constructor. По сути это обычный метод, который может принимать параметры. Основная цель конструктора - инициализировать объект начальными данными. И в данном случае в конструктор передаются два значения - для имени и возраста пользователя. Для хранения состояния в классе определяются свойства. Для их определения используется ключевое слово this. В данном случае в классе два свойства: name и age. Поведение класса определяют методы. В данном случае определен метод display(), который выводит значения свойств на консоль. Поскольку мы определили конструктор, который принимает два параметра, то соответственно мы можем передать в конструктор два значения для инициализации объекта: new Person("Tom", 34).

Наследование

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

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    display() {
        console.log(this.name, this.age);
    }
}

class Employee extends Person {
    constructor(name, age, company) {
        super(name, age);
        this.company = company;
    }
    display() {
        super.display();
        console.log("Employee in", this.company);
    }
    work() {
        console.log(this.name, "is hard working");
    }
}

let tom = new Person("Tom", 34);
let bob = new Employee("Bob", 36, "Google");
tom.display();
bob.display();
bob.work();

Для наследования одного класса от другого в определении класса применяется оператор extends, после которого идет название базового класса. То есть в данном случае класс Employee наследуется от класса Person. Класс Person еще называется базовым классом, классом-родителем, суперклассом, а класс Employee - классом-наследником, подклассом, производным классом. Производный класс, как и базовый, может определять конструкторы, свойства, методы. Вместе с тем с помощью слова super производный класс может ссылаться на функционал, определенный в базовом. Например, в конструкторе Employee можно вручную не устанавливать свойства name и age, а с помощью выражения super(name, age); вызвать конструктор базового класса и тем самым передать работу по установке этих свойств базовому классу. Подобным образом в методе display в классе Employee через вызов super.display() можно обратиться к реализации метода display класса Person.

Консольный вывод программы:

Tom 34
Bob 36
Employee in Google
Bob is hard working

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

Статические методы вызываются для всего класса вцелом, а не для отдельного объекта. Для их определения применяется оператор static. Например:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    static nameToUpper(person) {
        return person.name.toUpperCase();
    }
    display() {
        console.log(this.name, this.age);
    }
}

let tom = new Person("Tom Soyer", 34);
let personName = Person.nameToUpper(tom);
console.log(personName);        // TOM SOYER

В данном случае определен статический метод nameToUpper(). В качестве параметра он принимает объект Person и переводит его имя в верхний регистр. Поскольку статический метод относится классу вцелом, а не к объекту, то мы НЕ можем использовать в нем ключевое слово this и через него обращаться к свойствам объекта.