Повышение уровня мастерства в JavaScript ES6
Принципы, о которых пойдет речь, являются фундаментальными для этого языка и будут оставаться таковыми независимо от того, какой фреймворк вы используете. Более того, понимание этих концепций окажется бесценным бонусом, когда вы решите изучить или использовать какую-либо популярную библиотеку или фреймворк из арсенала JavaScript.
Что такое this?
Если вы уже работали с другими языками, такими как C++ или Java, вы наверняка сталкивались с ключевым словом this
(или self
в случае Python). В этих языках this
ссылается на экземпляр текущего объекта при вызове метода класса.
В JavaScript, однако, несколько другой принцип действия: ключевое слово this
может использоваться тут вне контекста функции или метода, а именно в глобальном контексте, и будет демонстрировать различное поведение в зависимости от выбранного режима.
Глобальный контекст
Глобальный контекст — это просто глобальный объект, который определяется как объект window
в браузере или объект global
в Node JS. Независимо от того, используется ли “строгий” или “нестрогий” режим, this
будет вести себя всегда одинаково в глобальном контексте.
Например, следующий пример справедлив для любого браузера:
console.log(this === window) // truethis.foo = "This is a global variable"
console.log(window.foo) // Это глобальная переменная
Контекст функции
Поведение this
внутри функции определяется способом вызова функции и используемым режимом.
В JavaScript функция может быть вызвана 4 различными способами. В следующих примерах я покажу, как ведет себя this
в каждом отдельном случае.
Простой вызов функции
В "нестрогом" режиме this
ссылается на глобальный объект при вызове функции. Однако в "строгом" режиме значение this
будет установлено на undefined.
function checkThis() {
return window === this;
}
function checkThisStrict() {
"use strict";
return window === this;
}
console.log(checkThis()); // true
console.log(checkThisStrict()); // false
Вызов метода
Внутри метода объекта this
будет просто ссылаться на содержимый объект:
let address = {
country: "Lebanon",
getCountry: function () {
return this.country;
},
// ...
};
console.log(address.getCountry()); // Ливан
Но что если мы будем хранить getCountry
в другой переменной?
const getCountry = address.getCountry;
Если код выполняется в браузере, использующем "нестрогий" режим, this
осуществит возврат к window
. Таким образом, getCountry()
будет расцениваться как window.country
, поскольку родительский объект в данном случае не указан:
console.log(getCountry()); // не определено
Вот почему мы должны использовать Function.prototype.bind
. Метод bind
, как следует из названия, связывает новый объект с указанной функцией, раскрывая объект this
.
const getCountry = address.getCountry.bind({ country: "Russia" });
console.log(getCountry()); // Россия
Вызов конструктора
Функция может быть использована в качестве конструктора при инициализации ключевым словом new
. При использовании ключевого слова new
инициализируется новый объект, на который ссылается this
. Отсутствие ключевого слова new
приведет к установке значения this
в глобальное значение fallback и вызовет ошибки.
function Address(country) {
this.country = country;
}
Address.prototype.getCountry = function () {
return this.country;
};
var address = new Address("Kazahstan");
console.log(address.getCountry()); // Казахстан
var addressNoNew = Address("Russia");
console.log(addressNoNew.getCountry()); // Uncaught TypeError: невозможно прочитать свойства undefined (reading 'getCountry')
Дополнительная обработка функции конструктора поможет предотвратить такое поведение:
function Address(country) {
if (!(this instanceof Address)) {
throw Error('Для вызова функции следует использовать оператор "new"');
}
this.country = country;
}
// или использовать "new.target", введенный в ES6
function Address(country) {
if (!new.target) {
throw Error('Для вызова функции следует использовать оператор "new"');
}
this.country = country;
}
Подробнее об этом будет рассказано в разделе "Прототипы".
Косвенный вызов
Функции JavaScript при вызове поддерживают ссылки на другие объекты через ключевое слово this
. Мы можем добиться такого поведения, используя Function.prototype.call
или Function.prototype.apply
. И call
, и apply
делают одно и то же. Разница лишь в том, что call
ожидает объект и столько аргументов, сколько необходимо, а apply
ожидает, что все аргументы, кроме объекта, будут переданы в виде массива.
function displayPrice(currency, decimals) {
console.log(currency + this.price.toFixed(decimals));
}
let item = {
price: 10,
};
displayPrice.call(item, "$", 2); // $10.00
displayPrice.apply(item, ["$", 2]); // $10.00
!!! ВАЖНО. Обратите внимание на то, что стрелочные функции ES6 не имеют собственного контекста выполнения, а наследуют this
от внешнего родителя. Именно по этой причине мы не можем использовать стрелочные функции для определения методов функций или классов.
Более подробные примеры с this
смотрите в этой статье.
Прототипы
Все объекты JavaScript наследуют свойства и методы от прототипа. Прототипы позволяют нам добавлять методы и свойства к нашим объектам и наследовать свойства.
При использовании вызова конструктора, о котором мы говорили выше, видно, что JavaScript добавляет свойство prototype к нашему объекту, которое указывает на исходный конструктор:
function Person(name, age) {
this.name = name;
this.age = age;
}
Мы можем добавить метод в наш исходный конструктор для получения года рождения. После добавления этого метода все объекты, имеющие в качестве прототипа объект Person
, будут наследовать этот метод:
Person.prototype.getYearOfBirth = function () {
return new Date().getFullYear() - this.age;
};
Используя ту же процедуру, мы можем добавлять или изменять методы и свойства существующих объектов-прототипов JavaScript, таких как Array.prototype
или Date.prototype
, однако, как правило, делать этого не рекомендуется.
Я не буду углубляться в тему прототипов, но вы всегда можете обратиться к документации, чтобы изучить более сложные понятия, связанные с ними.
Классы
Классы ES6 — это специальные функции, построенные на основе наследования прототипов в JavaScript. Именно поэтому классы JavaScript всегда называют “синтаксическим сахаром”. Эти классы не предоставляют дополнительных возможностей, встречающихся в других языках ООП, таких как объектно-ориентированное наследование, однако они служат хорошим шаблоном для построения объектов.
Вот как мы можем реализовать класс, похожий на наш предыдущий объект-прототип:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
getYearOfBirth() {
return new Date().getFullYear() - this.age;
}
}
В целом, классы ES6 обеспечивают более чистый синтаксис для стандартных объектов-прототипов, чтобы они были похожи на классы в других языках, таких как Java и C++, без добавления каких-либо функций ООП.
Области видимости
Область видимости в JavaScript определяет, как с помощью кода можно получить доступ к значениям и функциям. Области видимости в JavaScript делятся на глобальные и локальные.
Глобальная область видимости
Говоря простым языком, все, что определено за пределами всех функций и фигурных скобок {}
, относится к глобальной области видимости. Переменные, определенные в глобальной области видимости, могут быть использованы в любом месте кода.
var foo = "This is a global variable";
function printFoo() {
console.log(foo);
}
console.log(foo); // Это глобальная переменная
printFoo(); // Это глобальная переменная
Как правило, программист стремится максимально ограничить количество глобальных переменных по следующим причинам:
- Использование глобальных переменных может привести к коллизии имен при масштабировании кода. Учтите, что повторное объявление переменных
const
илиlet
приведет к ошибке, а повторное объявлениеvar
переопределит переменную. - В браузерах глобальные переменные являются членами объекта
window
, который доступен пользователям и любому скрипту, выполняющемуся на странице. Следовательно, использование глобальных значений может привести к проблемам безопасности. - JavaScript выполняет поиск переменных, начиная с текущей области видимости и поднимаясь к следующему родителю, пока не достигнет глобальной области видимости. Наличие большого количества глобальных переменных может привести к проблемам с производительностью.
Локальная область видимости
Локальная область видимости — это область видимости локальных переменных. Локальная область видимости состоит из областей видимости функции и блока.
Область видимости функции
Переменные, объявленные внутри функции, доступны только в пределах одной функции. Это правило действует для переменных, объявленных с помощью var
, let
или const
:
function printFoo() {
var foo = "This is a local variable";
console.log(foo);
}
printFoo(); // Это локальная переменная
console.log(foo); // Uncaught ReferenceError: foo не определено
Область видимости блока
Введенные в ES6 переменные, объявленные с помощью const
или let
внутри любой фигурной скобки {}
, находятся в области видимости блока, то есть они доступны только внутри одного блока. Область видимости блока фактически является подмножеством области видимости функции, кроме случая стрелочных функций с неявным возвратом.
/ Использование const или let
{
const foo = "This value is block-scoped";
console.log(foo); // Это значение находится в области видимости блока
}
console.log(foo); // Ошибка, foo не определено
// Использование var
{
var bar = "This value is NOT block-scoped";
console.log(bar); // Это значение НЕ находится в области видимости блока
}
console.log(bar); // Это значение НЕ находится в области видимости блока
Поднятие
В JavaScript поднятие — это стандартный процесс перемещения объявлений переменных и функций в верхнюю часть текущей области видимости перед выполнением кода. Технически это означает, что вы можете использовать переменные до их объявления.
foo = "Variable to declare later";
console.log(foo); // Переменная, которую нужно объявить позже
var foo;
Важно знать, что перемещаются на самый верх только объявления, но не инициализации:
console.log(foo); // не определено
var foo = "Variable initialized later";
Здесь объявление foo
перемещено на самый верх, но поскольку оно инициализируется позже, сначала оно будет иметь значение undefined
.
Область видимости блока и поднятие
Как уже говорилось ранее, const
и let
находятся в области видимости блока. Блок "знает" об этих переменных, но не может использовать их до того, как они будут объявлены. Значения, объявленные с помощью const
and let
находятся во "временной мертвой зоне" до их объявления.
Инициализация let
перед объявлением приведет к ошибке ReferenceError
:
Однако инициализация const
перед объявлением приведет к ошибке SyntaxError, и код даже не будет запущен:
Поднятие функций
При использовании объявления функций, последние будут подняты на верх текущей области видимости. Это значит, что вы можете использовать функцию до того, как она будет объявлена.
Однако если вы используете выражение функции, функции не будут подняты, так что в этом случае вам нужно использовать функцию только после ее объявления:
printHelloDeclaration();
function printHelloDeclaration() {
console.log("hello"); // hello
}
printHelloExpression(); // Uncaught ReferenceError: printHelloExpression не определено
const printHelloExpression = function () {
console.log("hello");
};
Замыкание
В официальной документации находим следующее определение понятия "замыкание":
Комбинация функции и ее лексического окружения, в котором эта функция была объявлена.
Другими словами, замыкание — это внутренняя функция, которая может обращаться к переменным своей объемлющей или внешней функции. Отсюда можно сделать вывод, что у замыкания есть три цепочки областей видимости:
- доступ к собственной области видимости блока;
- доступ к области видимости своей объемлющей функции (или внешней функции);
- доступ к глобальной области видимости.
Взгляните на следующий пример:
var firstTimout = 500;
var secondTimout = 1000;
function startCounter() {
var counter = 0; // локально для startCounter
setTimeout(
// первое замыкание
function () {
var innerCounter = 0; // локально для первого замыкания
counter += 1; // из объемлющей функции (startCounter)
console.log("counter = " + counter);
setTimeout(
// второе замыкание
function () {
counter += 1; // из самой внешней функции (startCounter)
innerCounter += 1; // из объемлющей функции(первое замыкание)
console.log(
"counter = " + counter + ", innerCounter = " + innerCounter
);
},
secondTimout // из глобальной области видимости
);
},
firstTimout // из глобальной области видимости
);
}
startCounter();
В этом примере мы видим, что у нас могут быть вложенные замыкания, каждое из которых имеет доступ ко всем переменным объемлющих функций, их локальным и глобальным переменным. Я использовал здесь тайм-ауты, чтобы показать самую важную особенность замыканий, а именно:
Внутренняя функция будет иметь доступ к значениям внешней функции, даже если внешняя функция была уже возвращена.
Каррирование
Принцип замыкания в JavaScript позволяет нам воспользоваться преимуществами каррирования, которое является важной концепцией функционального программирования.
Каррирование — это преобразование функций, которое переводит функцию из вызываемой как f(a, b, c)
в вызываемую как f(a)(b)(c)
.
Другими словами, каррированная функция не будет возвращать значение после передачи аргумента. Вместо этого функция будет возвращать новую функцию, ожидая следующего аргумента из набора до тех пор, пока все аргументы не будут исчерпаны. Результат всех функций будет возвращен после передачи последнего аргумента. Ниже показана реализация функции сложения:
// Стандартный способ
const add = (first, second, third) => first + second + third;
console.log(add(1, 2, 3)); // 6
// С каррированием
const addCurried = (first) => (second) => (third) => first + second + third;
console.log(addCurried(1)(2)(3)); // 6
Более реалистичным случаем использования каррирования является внедрение обработчиков событий с предопределенными аргументами. Возьмем в качестве примера следующий обычный компонент React:
import React from "react";
export function ButtonGroup() {
const createClickHandler = (message) => (e) => {
alert(message);
};
return (
<div>
<button onClick={createClickHandler("Button 1 clicked!")}>#1</button>
<button onClick={createClickHandler("Button 2 clicked!")}>#2</button>
<button onClick={createClickHandler("Button 3 clicked!")}>#3</button>
</div>
);
}
В этом случае предупреждение появится только после нажатия на кнопку. Благодаря замыканию, предупреждающее сообщение будет тем же сообщением, которое было передано при первом вызове каррированной функции.
Можете ознакомиться с этой статьей, чтобы лучше понимать, что такое каррирование и как его использовать.
IIFE
IIFE (Immediately Invoked Function Expression) — это функция, которая вызывается сразу же после ее определения. IIFE важны, поскольку все переменные, объявленные внутри этих функций, недоступны во внешней области видимости. Вероятнее всего, эти функции понадобятся вам, когда вам нужно будет непосредственно выполнить код и гарантировать конфиденциальность данных.
(() => {
var foo = "This value is private";
console.log(foo); // Это значение является приватным
})();
console.log(foo); // Ошибка: foo не определено
Примером использования IIFE и шаблона модулей JavaScript может служить внедрение объекта Singleton:
class MyDAO {
// ...
}
export default (function () {
var instance;
function createInstance() {
var classObj = new MyDAO();
return classObj;
}
return {
getInstance: function () {
if (!instance) {
instance = createInstance();
}
return instance;
},
};
})();
В дальнейшем мы будем иметь доступ только к одному экземпляру, когда будем импортировать наш объект singleton в другое место:
import MySingletonDAO from "./MyDAO"
const instance1 = MySingletonDAO.getInstance()
const instance2 = MySingletonDAO.getInstance()
console.log(instance1 === instance2) // true