JavaScript. Теория — углубленное изучение
Общие темы
Ключевое слово `this` и контекст (контекст выполнения)
Контекст выполнения (execution context) или просто контекст — это абстрактная концепция, описывающая обстоятельства вызова функции или метода и, главное, так называемое окружение: свои и наследуемые переменные, методы, свойства, окружение.
this
this
— это ключевое слово, ссылка на определенный объект со всеми его свойствами и методами. Иными словами, this
открывает доступ к контексту объекта.
- В методе объекта
this
содержит ссылку на текущий объект. - В обработчиках событий
this
содержит ссылку на элемент-источник события. Если это, например, клик на кнопке — то на кнопке. - В функции в строгом режиме значение
this
не определено —undefined
. - В функции в НЕстрогом режиме — на глобальный объект.
- За пределами фигурных скобок функций, циклов и условий — на глобальный объект.
☝️🧐 Стрелочные функции не создают собственного значения this
и берут его из внешней функции.
☝️🧐 this
это ключевое слово, а не переменная. Изменить его значение (ссылку) невозможно. Оно зависит только от контекста, в котором используется, и режима — строгого или нестрогого.
☝️🧐 Методы call
и apply
позволяют передавать функциям ЛЮБОЙ контекст в ссылке this
.
Контекст объекта
При обращении к свойству/методу объекта внутри объекта для ссылки на этот объект используется слово this
.
const person = {
firstName: 'John',
lastName: 'Doe',
id: 5566,
fullName() {
return `${this.firstName} ${this.lastName}`;
},
};
const fiat = {
make: 'Fiat',
model: '500',
year: 1957,
color: 'Medium Blue',
passengers: 2,
convertible: false,
mileage: 88000,
started: false,
start() {
this.started = true;
},
stop() {
this.started = false;
},
drive() {
if (this.started) {
alert('Zoom zoom!');
} else {
alert('You need to start the engine first.');
}
},
};
Контекст обработчиков событий
В обработчиках событий this
ссылается на элемент-источник события. Если это, например, клик на кнопке — то на кнопку. Например
<button onclick="this.style.display='none'">
Click to Remove Me!
</button>
Глобальный контекст
В глобальном контексте this
ссылается на глобальный объект: в браузере — это window
.
this.alert('global alert');
this.console.log('global console');
const currentDocument = this.document;
Контекст функции
Если скрипт запускается в строгом режиме (директива 'use strict'
), то значение this
— undefined
.
function myFuncStrict() {
return this;
}
В НЕстрогом режиме this
функции содержал бы ссылку на глобальный объект window.
function myFuncNoStrict() {
return this;
}
☝️🧐 Рекомендации Airbnb: не сохранять ссылку на this
, чтобы передать во вложенную функцию контекст. Использовать стрелочные функции, которые наследуют внешний контекст.
/**
* ⛔️ плохо
* @return {*}
*/
function foo() {
const self = this;
return function () {
console.log(self);
};
}
/**
* 👍 хорошо
* @return {*}
*/
function bar() {
return () => {
console.log(this);
};
}
Явное указание `this`: методы `call()`, `apply()`
myFunc.call(useThisforThi, addionalParam1)
иmyFunc.apply( useThisforThis, [
some,
array,
for,
compute] )
.
По умолчанию, свойства и методы объектов JavaScript недоступны из других объектов. Например, myObj
не может воспользоваться методами someoneElseObj
и наоборот.
Однако есть способ обойти это ограничение: методы call()
, apply()
и bind() для привязки функции к любому объекту, как если бы она ему принадлежала.
Если говорить «по-програмистски»:
- для вызова с заданным контекстом
- или с явным указанием
this
.
Различия. call
и apply
вызывают функцию немедленно, а bind
возвращает функцию с правильным контекстом, которую можно вызвать с задержкой, в подходящий момент.
Метод call()
С методом call()
объект может использовать метод другого объекта.
Пример №1. В приведённом ниже коде мы вызываем sayHi в контексте различных объектов: sayHi.call(user) запускает sayHi, передавая this=user, а следующая строка устанавливает this=admin:
function sayHi() {
alert(this.name);
}
const user = { name: 'John' };
const admin = { name: 'Admin' };
/* используем 'call' для передачи различных объектов
в качестве `this` */
sayHi.call(user); // John
sayHi.call(admin); // Admin
Пример №2. Вызываем метод объекта с дополнительными аргументами.
const person = {
fullName(city, country) {
return `
${this.firstName} ${this.lastName},
${city},
${country}
`;
},
};
const person1 = {
firstName: 'John',
lastName: 'Doe',
};
person.fullName.call(person1, 'Oslo', 'Norway');
Метод apply()
Если нам неизвестно, с каким количеством аргументов понадобится вызвать функцию, можно использовать более мощный метод — apply
. Дополнительные аргументы в отличие от call
он принимает не списком, а массивом.
const maryDoe = {
firstName: 'Mary',
lastName: 'Doe',
};
/* Вызов вернет 'Mary Doe, Oslo, Norway'.
2-й и 3-й аргументы сгруппированы в массив.
Но преимущество apply перед call отчётливо
видно, когда массив аргументов формируется
динамически. */
personWithDetails.fullName.apply(
maryDoe, ['Oslo', 'Norway']
);
Пример №4. Массивы в JS не имеют методов, определяющих максимальное или минимальное значение. Но такие методы есть во встроенном объекте Math
. С помощью метода apply()
можно вызвать эти методы для массивов.
Вызов метода max()
нижe вернет 3.
/* ☝️🧐 В качестве контекста методу apply()
передан null. А можно было передать и 0,
и пустую строку — `Math.max` все равно игнорирует
контекст, не использует this. Зачем this, если
нужно всего лишь выбрать максимальный
из аргументов? */
Math.max.apply(null, [1, 2, 3]);
Преобразование значения this
Одной из серьёзных проблем с безопасностью в обычном, нестрогом режиме является передача контекста глобальному объекту window
. Например, при вызове метода apply()
или call()
c аргументом null
или undefined
.
А в строгом режиме значение this функции равно null, и при доступе к методу объекта, функции выскочит ошибка.
const color = 'green';
function displayColor() {
console.log(this.color);
}
/* В вызове в displayColor.call() передается
значение null. В обычном режиме значением this
функции станет глобальный объект window,
которому принадлежит переменная color,
и появится диалог со строкой green.
В строгом режиме так сделать не получится —
выскочит ошибка. */
displayColor.call(null);
Привязка контекста к функции / методу - `bind`()
Пропадающий в колбэке this
метода
При передаче методов объекта в качестве колбэков, например для setTimeout, возникает известная проблема – потеря this
.
Существует несколько решений. Самый простой — обернуть вызов метода в анонимную функцию-обёртку, создав замыкание.
const user = {
firstName: 'Вася',
sayHi() {
alert(`Привет, ${this.firstName}!`);
},
};
/* Со второй скобки начинается обёртка, она передана
в setTimeout в качестве первого аргумента */
setTimeout(() => {
user.sayHi(); // Привет, Вася!
}, 1000);
Теперь код работает корректно, так как объект user достаётся из замыкания, а затем вызывается его метод sayHi
.
Но теперь в нашем коде появилась небольшая уязвимость. Что произойдёт, если до момента срабатывания setTimeout
(ведь задержка составляет целую секунду!) в переменную user
будет записано другое значение? Тогда вызов неожиданно будет совсем не тот!
bind()
Проблему решает метод bind()
. Принцип действия напоминает call()
и apply()
, но в отличие от них bind()
создает и возвращает связанную функцию, выполнение которой можно отложить.
С его помощью можно связать любую функцию/метод с определенным объектом. И, в частности, с родительским — чтобы контекст не терялся при вызове в колбэке.
Пример привязки к обычной функции.
let user = {
firstName: "Вася"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // Вася
Здесь funcUser
– это «связанный вариант» func
, с фиксированным this
=user
.
Пример с методом объекта:
let user = {
firstName: "Вася",
sayHi() {
alert(`Привет, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // 🙋🏻♀️
sayHi(); // Привет, Вася!
setTimeout(sayHi, 1000); // Привет, Вася!
В строке 🙋🏻♀️ мы берём метод user.sayHi
и привязываем его к user
. Теперь sayHi
– это «связанная» функция, которая может быть вызвана отдельно или передана в setTimeout
(контекст всегда будет правильным).
Здесь мы можем увидеть, что связав метод мы можем передавать в него аргументы.
let user = {
firstName: "Вася",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
/* Привет, Вася (аргумент "Привет" передан
в функцию "say") */
say("Привет");
/* Пока, Вася (аргумент "Пока" передан
в метод "say") */
say("Пока");
Удобный метод bindAll
Если у объекта много методов и мы планируем их активно передавать, то можно привязать контекст для них всех в цикле:
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
Частично привязанные функции
В функции можно закрепить не только this
, но и аргументы. Например.
function mul(a, b) {
return a * b;
}
/* Фиксируем первый аргумент `mul` — в новой
функции `double` это будет два */
let double = mul.bind(null, 2);
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10
Это называется частичное применение – мы создаём новую функцию, фиксируя некоторые из существующих параметров. Применяется редко — в основном для создания слегка модифицированных «экземпляров» функций.
Условия и циклы
`if...else`
Использование функции в условном выражении
// Необходимые переменные
const myAge = prompt('How old are you?');
const legalDrivingAge = 18;
// Функция, которая будет использована в условии
const canIDrive = (age, legalAge) => {
if (age >= legalAge) {
return true;
}
return false;
};
// Функция в условии
if (canIDrive(myAge, legalDrivingAge)) {
console.log('You can legally drive!');
} else {
console.log(
'You\'ll have to wait a few more years!'
);
}
Вложенные условные конструкции
let result;
if (yourName.length > 0 && gender.length > 0) {
// Вложенная конструкция
if (gender === 'male' || gender === 'female') {
result = 'Thanks';
} else {
result = 'Please enter male or female.';
}
} else {
result = 'Tell us both your name and gender.';
}
☝️🧐 Стилистические рекомендации
Если в блоке if
выполняется оператор return
, последующий блок else
не нужен. return
внутри блока else if
, следующем за блоком if
, который содержит return
, может быть разделен на несколько блоков if
.
/**
* ⛔️ плохо
* @return {*}
*/
function foo() {
if (x) {
return x;
} else {
return y;
}
}
/**
* 👍 хорошо
* @return {*}
*/
function bar() {
if (x) {
return x;
}
return y;
}
☝️🧐 Для логического типа используем сокращение — переменные равные true
или false
в сравнении не нуждаются. Для строк и чисел используем явное сравнение.
// ⛔️ плохо
if (isValid === true) {
// ...
}
// 👍 хорошо
if (isValid) {
// ...
}
// ⛔️ плохо
if (name) {
// ...
}
// 👍 хорошо
if (name !== '') {
// ...
}
// ⛔️ плохо
if (collection.length) {
// ...
}
// 👍 хорошо
if (collection.length > 0) {
// ...
}
В условных операторах, результат выражения переводится в булевый тип по следующим правилам.
Object
= trueUndefined
= falseNull
соответствует falseBoolean
— по значению- Строка соответствует false, если пустая ‘’, в остальных случаях true
- Число соответствует false, если +0, -0, or
NaN
, в остальных случаях true
☝️🧐 Если управляющий оператор (if
, while
и т.д.) слишком длинный, то каждое (сгруппированное) условие можно поместить на новую строку. Логический оператор должен располагаться в начале строки.
// ⛔️ плохо
if ((foo === 123 || bar === 'abc') && doesItLookGoodWhenItBecomesThatLong() && isThisReallyHappening()) {
thing1();
}
// 👍 хорошо
if (
(foo === 123 || bar === 'abc')
&& doesItLookGoodWhenItBecomesThatLong()
&& isThisReallyHappening()
) {
thing1();
}
`switch`
☝️🧐 Использовать фигурные скобки для case
и default
, если они содержат присвоение значений переменных или объявление класса (так называемые «лексические декларации»).
Не объявлять в switch функций. eslint: no-case-declarations, no-inner-declarations
// ⛔️ плохо
switch (foo) {
case 1:
const x = 1;
break;
case 2:
const y = 2;
break;
default:
class C {}
}
// 👍 хорошо
switch (foo) {
case 1: {
const x = 1;
break;
}
case 2: {
const y = 2;
break;
}
case 3:
bar();
break;
default: {
class C {}
}
}
Примеры
const a = 2 + 2;
switch (a) {
case 3:
alert('Маловато');
break;
case 4:
alert('В точку!');
break;
case 5:
alert('Перебор');
break;
default:
alert('Я таких значений не знаю');
}
/* Сначала получаем номер дня недели из объекта
Date. Затем присваиваем переменной переменной
значения зависимости от этого номера */
switch (new Date().getDay()) {
case 6:
text = 'Today is Saturday';
break;
case 0:
text = 'Today is Sunday';
break;
default:
text = 'Looking forward to the Weekend';
}
Группировка case
Несколько значений case можно группировать. В примере ниже case 3 и case 5 выполняют один и тот же код
switch (a) {
case 4:
alert('Верно!');
break;
case 3:
case 5:
alert('Немного ошиблись, бывает.');
break;
default:
alert('Странный результат, очень странный');
}
`for`
for
Запись в круглых скобках состоит из трех частей. При этом все три части опциональны.
for (
инициализация счетчика;
проверка условия;
увеличение/ уменьшение счетчика
)
Первая часть — это, как правило, инициализация переменной цикла / счетчика. Однако в ней могут быть инициализированы и другие переменные. Например…
for (let i = 0, text = ''; i < cities.length; i++) {
text += `${cities} <br>`;
}
Вторая часть — проверка условия. Она выполняется при каждой итерации цикла. Если условие оказывается ложным, выполнение цикла прерывается. Если есть желание пропустить эту часть, в теле цикла нужно указать break.
Третья часть — увеличение / уменьшение счетчика. Оно происходит один раз за итерацию, после выполнения всех команд в ТЕЛЕ цикла. Изменение счетчика чаще всего производят инкрементом i++
. Но можно и декрементом i--
, изменением на указанную цифру i += 15
и т. д.
👍 хорошо
< короче/лучше, чем <=
/* Сокращенный вариант цикла, только с условием,
без счетчика */
for (;answer !== 'forty-two';) {
// ...
}
Вложенные циклы
Обычно вложенные циклы применяются для перебора многомерных массивов.
Например, когда нужно сложить аккуратно вещи в шкафчике из трех полок, внешним циклом мы перебираем полки: одну за другой. На каждой этапе запускаем вложенный цикл, в процессе которого перебираем и раскладываем вещи.
Те же процессы происходят, когда собираем колоду карт: поочереди масть за мастью — добавляем в каждой группе все номиналы. Или когда собираем многоуровневое главное меню сайта.
Никаких ограничений: for
в for
, for
в while
, наоборот, с любым уровнем вложенности.
Пример. Три раза подряд посчитать от 1 до 5.
☝️🧐 i
и j
— стандартные названия переменных для циклов. Эта традиция идет от тех времен, когда объем программы был ограничен (а код пробивался на перфокартах) и у коротких имен были преимущества перед значащими названиями. Теперь это всего лишь традиция, но всего лишь исключение из правила о выборе осмысленных имен переменных.
for (let i = 1; i < 4; i++) {
for (let j = 1; j < 6; j++) {
console.log(j);
}
}
😎 Трюк. Заполнение массива нулями — с тем, чтобы потом суммировать их с числами, результатами вычислений. Сложение пустой позиций с числом дает в результате undefined.
// Пять раз…
for (let i = 0; i < 5; i++) {
/* …добавляем в созданный в начале документа
массив по 0 — получаем массив из 5 нулей */
myArray[i] = 0;
}
Практический пример. Подсчет вероятности выпадения дубля на двух костях.
let isDouble = 0;
let totalCombos = 0;
// Каждая из шести сторон одной кости…
for (let i = 1; i < 7; i++) {
/* …Может составить пару с шестью сторонами
другой: 6x6 */
for (let j = 1; j < 7; j++) {
/* Увеличиваем счетчик дублей, если значения
сторон совпадают */
if (i === j) {
isDouble += 1;
}
totalCombos += 1;
}
}
/* Вероятность — количество дублей на общее
количество комбинаций */
const probability = isDouble / totalCombos;
Функции
Оператор `return`
Завершает выполнение текущей функции и возвращает её значение.
function square(x) {
return x * x;
/* return может находиться в любом месте тела
функции. Но после этой директивы код уже не будет
выполняться. */
console.log('В консоли это не появится');
}
// Значение demo будет равняться 9
const demo = square(3);
Вызовов return
может быть несколько.
function checkAge(age) {
if (age > 18) {
return true;
} else {
return confirm('А родители разрешили?');
}
}
let age = prompt('Сколько вам лет?', 18);
if ( checkAge(age) ) {
alert( 'Доступ получен' );
} else {
alert( 'Доступ закрыт' );
}
Использование return
для раннего выхода
Возможно использовать return
и без значения. Это приведёт к немедленному выходу из функции. Это один из способов раннего выхода — полезен в тех случаях, когда в расчетах соблюдается условие, после которого дальнейшие вычисления не нужны.
function showMovie(age) {
if ( !checkAge(age) ) {
return;
}
alert('Вам показывается кино');
// ...
}
☝️🧐 Надо помнить, что функция всегда возвращает значение, а если оно не указывается, как в случаях с ранним выходом, то возвращается undefined
.
☝️🧐 Поэтому, если в функции используется return для раннего выхода, лучше явно возвращать false. В таком случае, если функция будет использоваться в каком-то условии, не надо будет проверять на undefined
.
if (getName() === false) {/*👍*/}
if (getName() === undefined) {/*👎*/}
Реже для раннего выхода используются ключевые слова break
и throw
.
const getName = (name) => {
try {
//get out of here
if (name === 'Tommy Vercetti') throw 'exit';
} catch (e) {
// handle exception
}
};
const getFirstName = () => {
getName : {
console.log("I get logged");
break getName ;
console.log("I don't get logged");
}
};
Функции без return
вообще
Использование return
необязательно. Если функция не возвращает значение, а, например, просто назначает CSS-класс, можно не использовать return
.
☝️🧐 Хотя некоторые разработчики используют пустой return
в конце функции (не для раннего выхода, а для явного завершения выполнения), Airbnb не рекомендует этого делать.
Вызов (call) функции
myFunc('аргумент в param1', 'аргумент в param2');
Функции можно вызывать в других конструкциях, например в условной…
if (myFunc(24) % 3 === 0) {
// code…;
} else {
// code…;
}
Также функции можно вызвать в других функциях.
// Пример № 1
const square = x => x * x;
const cube = x => square(x) * x;
// Пример № 2
const isMultipleOfThree = x => x % 3 === 0;
Интересный пример использования оператора !
— логического «НЕ». Число сначала передаётся в первую функцию isMultipleOfThree
. То, что остаток деления на 3 не равен 0 обозначается ! перед использованной функцией
const isNotMultipleOfThree = x => !isMultipleOfThree(x);
Любая функция — это объект. И любая функция — это метод
Функция, как почти все в JS — это объект. И у этого объекта есть свойства и методы. Например, свойство name
содержит имя, указанное при объявлении функции, либо переменную / метод, в которую она сохранена.
// myFunc.name == 'myFunc'
function myFunc() {}
// mySecondFunc.name == "mySecondFunc"
const mySecondFunc = () => {};
/* Метод toString возвращает код функции в виде
строки */
console.log(myFunc.toString());
Кроме того, любая функция — это метод. Если она не определена, как метод в пользовательском объекте, значит, становится методом глобального объекта.
Функции высшего порядка (или функции первого класса)
- Могут быть присвоены переменным или сохранены в объекте.
- Могут передаваться в качестве аргументов другим функциям.
- Другие функции могут возвращать first-class function, как результат.
В практическом смысле это означает систему вложений (includ’ов) для функций.
В JS все функции — высшего порядка (первоклассные).
/**
* Функция которая будет передана в аргументе
* следующей функции - «инклуд»
*/
function hawaiianTranslator(word) {
let translation;
if (word === 'Hello') {
translation = 'Aloha';
} else if (word === 'Goodbye') {
translation = 'Aloha';
}
return translation;
}
/**
* «Мастер-функция» которая получит в аргументе
* «функцию-инклуд»
*
* @param {function} translator — функция, которая
* переводит полученный аргумент.
*/
function sayIt(translator) {
/* «Функция-инклуд», переданная в аргументе
translator, вызывается, и полученное значение
сохраняется в переменной phrase. */
const phrase = translator('Hello');
alert(phrase);
}
/* Вызываем sayIt, передавая ей в аргументе
«функцию-инклуд» */
sayIt(hawaiianTranslator);
Локальные переменные
Переменные, объявленные внутри функции, недоступны за пределами ее фигурных скобок (область видимости функции). Они создаются в момент вызова функции и очищаются после выполнения кода функции.
☝️🧐 Функции и объекты созданные внутри других функций, также ограничены локальной областью видимости и недоступны извне.
// code here can NOT use carName
function myFunc() {
const carName = 'Volvo';
// code here CAN use carName
}
// code here can NOT use carName
Параметры по умолчанию
Если функция не дополучает аргументов, пропущенные значения становятся undefined
. Иногда это не страшно, но иногда чревато ошибками. Решают проблему значения по умолчанию. Присваиваются через знак равенства.
const myFunc = (x = 'Икс', y = 100) => x + y;
Параметры со значениями по умолчанию пишутся в конце, после обычных параметров — так чтобы функция возвращала undefined
только в случае пропуска всех аргументов.
const sayMyHeight = (
name,
height = 186,
weight = 96
) => {
alert(`
Hi! My name is ${name}. My height
is ${height}cm and my weight is ${weight}kg.
`);
};
sayMyHeight('Vova');
Параметры по умолчанию могут быть любого типа: строками, числами, булевыми значениями, объектами…
function findProducts(
opts = { minPrice: 10, maxPrice: 20 },
) {
console.log(opts);
}
// { minPrice: 10, maxPrice: 20 }
findProducts();
/* {}. Даже пустой объект в аргументе заменит
значение по умолчанию */
findProducts({});
…и выражениями.
function sayHi(
who = getCurrentUser().toUpperCase()
) {
alert(`Привет, ${who}`);
}
// Привет, Вася
sayHi();
Анонимные функции
Анонимная функция (еще можно встретить название «лямбды») — функция без имени. Применяется обычно тогда, когда функция используется лишь однажды. Например, как в примерах ниже, функция-обработчик загрузки страницы. Добавляется в функциональном выражении после знака равенства или в параметрах другой функции.
Разработчики Airbnb предлагают заменять анонимные функции «старого образца» функциями-стрелками.
Пример. Замена именованной функции анонимной…
function handler() {
alert('Yeah, that page loaded!');
}
window.onload = handler;
/* Убрать имя переменной handler
Назначить свойству window.onload функциональное
выражение */
window.onload = () => {
alert('Yeah, that page loaded!');
};
// Или так
window.addEventListener('load', () => {
// code; event can be 'resize' too
}, false);
Стрелочные функции
Синтаксис функции через «стрелку» =>
, без ключевого слова function
и в простых, однострочных случаях без ключевого слова return
.
Компактность функций-стрелок значительно облегчает чтение кода. Но главная особенность стрелочных функций в другом. У них нет своего this
. Если происходит обращение к this
, его значение берётся снаружи, из внешней функции.
Функции-стрелки предназначены для небольшого кода (например, обратного вызова), который не имеет своего контекста, выполняясь в родительском.
В примере внутри forEach
использована стрелочная функция. И это значит, что this.title
в ней будет иметь точно такое же значение, как в методе showList
— group.title
.
const group = {
title: 'Наш курс',
students: ['Вася', 'Петя', 'Даша'],
showList() {
this.students.forEach(
// стрелочная функция, в которой this тот
// же, что и у родительского метода
student => alert(`${this.title}: student`),
);
},
};
⚠️ Стрелочные функции не надо писать везде. Главным образом — там, где нужно сохранить контекст родителя: обработчиках событий, ajax-запросах, анимации (setInterval
, setTimeout
). То есть, когда функция передается, как аргумент в другую функцию или встроенный метод.
⛔️ Стрелочные функции не могут быть использованы как конструкторы, с new
— потому что у них нет this
. У функций-стрелок нет свойства prototype
.
Простейший вариант стрелочной функции
Слева от =>
находится параметр (x
в данном случае), а справа – выражение, которое нужно вернуть. Без слова return
— в кратком синтаксисе стрелочных функций результат возвращается без лишних формальностей.
const myFunc = x => x + 1;
// Сейчас функция myFunc вернет 2
alert(myFunc(1));
// Та же функция в традиционном синтаксисе:
const myFunc2 = function (x) { return x + 1; };
Если аргументов несколько, то нужно обернуть их в скобки ()
const sum = (a, b) => a + b;
// Функция sum вернет 3
alert(sum(1, 2));
Если нужно задать функцию без аргументов, используются пустые скобки.
В примере вызов getTime()
будет возвращать текущий час.
const getTime = () => `${new Date().getHours()}:`;
alert(getTime());
Когда тело функции достаточно большое, то можно его обернуть в фигурные скобки.
const getTime = () => {
const date = new Date();
const hours = date.getHours();
const minutes = date.getMinutes();
return `${hours}: ${minutes}`;
};
☝️🧐 Заметим, что как только тело функции оборачивается в {…}, то её результат уже не возвращается автоматически, как в кратком синтаксисе. Такой функции нужен явный return
, если, конечно, нужно что-либо возвратить.
const myFunc3 = x => x * x;
const myFunc4 = (x, y) => {
const newSum = x + y;
// Здесь нужно явно вернуть результат
return newSum;
};
Примеры использования стрелочных функций для обратного вызова.
const myArr = [5, 8, 3];
/* В методе Array.sort используем стрелочную
функцию обратного вызова. Такая запись –
коротка и понятна… */
const sorted = myArr.sort((a, b) => a - b);
alert(sorted); // 3, 5, 8
/* В gulpfil'e функции-стрелки также используются
для обратного вызова */
gulp.task('twig', () =>
gulp.src('/src/*.twig')
.pipe(gulp.dest('/dist')),
);
Когда стрелочные функции использовать нельзя
Во-первых, их бесполезно использовать, как методы объектов.
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
Если вызвать cat.jumps
из примера, количество жизней не уменьшается. Это происходит потому, что this
в стрелочной функции не привязывается к внешнему объекту.
Во-вторых. Если нам нужен динамический контекст — в частности, в обратном вызове стрелочная функция вернет TypeError. Это связано с тем, что this
не привязана к источнику события.
const button = document.getElementById('press');
button.addEventListener('click', () => {
// `this` здесь будет возвращать ошибку
this.classList.toggle('on');
});
Решение — использовать вместо this
объект event
и его свойства currentTarget
(объект на который установлен слушатель) или target
(инициатор события, может быть потомком целевого объекта — например, span
‘ом в кнопке).
button.addEventListener('click', (e) => {
e.currentTarget.classList.toggle('on');
});
Когда точно стоит использовать стрелочные функции
Стрелочные функции отлично подойдут для случаев, когда вам не нужен собственный контекст функции.
Также мне очень нравится использовать стрелочные функции во всяких forEach
(с использованием e.currentTarget), map
и reduce
— код так лучше читается.
Кстати, у функций-стрелок нет переменной arguments
Но они могут использовать псевдомассив аргументов внешней, традиционной функции.
function myFunc5(...args) {
const showArg = () => alert(args[0]);
/* Вызов showArg() вернет «Ты это видел?»,
получив его из аргументов myFunc5 */
showArg();
}
myFunc5('Ты это видел?');
Функция обратного вызова, callback
Синтаксис функции через «стрелку» =>
, без ключевого слова function
и в простых, однострочных случаях без ключевого слова return
.
Компактность функций-стрелок значительно облегчает чтение кода. Но главная особенность стрелочных функций в другом. У них нет своего this
. Если происходит обращение к this
, его значение берётся снаружи, из внешней функции.
Функции-стрелки предназначены для небольшого кода (например, обратного вызова), который не имеет своего контекста, выполняясь в родительском.
В примере внутри forEach
использована стрелочная функция. И это значит, что this.title
в ней будет иметь точно такое же значение, как в методе showList
— group.title
.
const group = {
title: 'Наш курс',
students: ['Вася', 'Петя', 'Даша'],
showList() {
this.students.forEach(
// стрелочная функция, в которой this тот
// же, что и у родительского метода
student => alert(`${this.title}: student`),
);
},
};
⚠️ Стрелочные функции не надо писать везде. Главным образом — там, где нужно сохранить контекст родителя: обработчиках событий, ajax-запросах, анимации (setInterval
, setTimeout
). То есть, когда функция передается, как аргумент в другую функцию или встроенный метод.
⛔️ Стрелочные функции не могут быть использованы как конструкторы, с new
— потому что у них нет this
. У функций-стрелок нет свойства prototype
.
Простейший вариант стрелочной функции
Слева от =>
находится параметр (x
в данном случае), а справа – выражение, которое нужно вернуть. Без слова return
— в кратком синтаксисе стрелочных функций результат возвращается без лишних формальностей.
const myFunc = x => x + 1;
// Сейчас функция myFunc вернет 2
alert(myFunc(1));
// Та же функция в традиционном синтаксисе:
const myFunc2 = function (x) { return x + 1; };
Если аргументов несколько, то нужно обернуть их в скобки ()
const sum = (a, b) => a + b;
// Функция sum вернет 3
alert(sum(1, 2));
Если нужно задать функцию без аргументов, используются пустые скобки.
В примере вызов getTime()
будет возвращать текущий час.
const getTime = () => `${new Date().getHours()}:`;
alert(getTime());
Когда тело функции достаточно большое, то можно его обернуть в фигурные скобки.
const getTime = () => {
const date = new Date();
const hours = date.getHours();
const minutes = date.getMinutes();
return `${hours}: ${minutes}`;
};
☝️🧐 Заметим, что как только тело функции оборачивается в {…}, то её результат уже не возвращается автоматически, как в кратком синтаксисе. Такой функции нужен явный return
, если, конечно, нужно что-либо возвратить.
const myFunc3 = x => x * x;
const myFunc4 = (x, y) => {
const newSum = x + y;
// Здесь нужно явно вернуть результат
return newSum;
};
Примеры использования стрелочных функций для обратного вызова.
const myArr = [5, 8, 3];
/* В методе Array.sort используем стрелочную
функцию обратного вызова. Такая запись –
коротка и понятна… */
const sorted = myArr.sort((a, b) => a - b);
alert(sorted); // 3, 5, 8
/* В gulpfil'e функции-стрелки также используются
для обратного вызова */
gulp.task('twig', () =>
gulp.src('/src/*.twig')
.pipe(gulp.dest('/dist')),
);
Когда стрелочные функции использовать нельзя
Во-первых, их бесполезно использовать, как методы объектов.
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
Если вызвать cat.jumps
из примера, количество жизней не уменьшается. Это происходит потому, что this
в стрелочной функции не привязывается к внешнему объекту.
Во-вторых. Если нам нужен динамический контекст — в частности, в обратном вызове стрелочная функция вернет TypeError. Это связано с тем, что this
не привязана к источнику события.
const button = document.getElementById('press');
button.addEventListener('click', () => {
// `this` здесь будет возвращать ошибку
this.classList.toggle('on');
});
Решение — использовать вместо this
объект event
и его свойства currentTarget
(объект на который установлен слушатель) или target
(инициатор события, может быть потомком целевого объекта — например, span
‘ом в кнопке).
button.addEventListener('click', (e) => {
e.currentTarget.classList.toggle('on');
});
Когда точно стоит использовать стрелочные функции
Стрелочные функции отлично подойдут для случаев, когда вам не нужен собственный контекст функции.
Также мне очень нравится использовать стрелочные функции во всяких forEach
(с использованием e.currentTarget), map
и reduce
— код так лучше читается.
Кстати, у функций-стрелок нет переменной arguments
Но они могут использовать псевдомассив аргументов внешней, традиционной функции.
function myFunc5(...args) {
const showArg = () => alert(args[0]);
/* Вызов showArg() вернет «Ты это видел?»,
получив его из аргументов myFunc5 */
showArg();
}
myFunc5('Ты это видел?');
Деструктуризация в параметрах
См. также деструктурирующее присваивание или просто деструктуризацию
Деструктуризация в параметрах — это возможность функции получать значения из объекта или массива, переданного в параметры. Для этого параметры функции заключаются в фигурные скобки, как объект.
function showMenu({ title, width, height }) {
alert(`${title} ${width} ${height}`);
}
let options = {
title: 'Меню',
width: 100,
height: 200,
};
/* Объект options будет разбит на переменные.
В диалог будет выведено «Меню 100 200» */
showMenu(options);
Можно использовать деструктуризацию со значениями по умолчанию.
function showNewMenu({
title = 'Заголовок',
width = 100,
height = 200
}) {
alert(`${title} ${width} ${height}`);
}
/* И в таком случае можно передавать объект только
с измененным значением */
options = {
title: 'Меню',
};
// Меню 100 200
showNewMenu(options);
Чтобы функция могла быть вызвана вообще без аргументов, нужно добавить ей параметр по умолчанию — пустой объект, пустые фигурные скобки после объекта с параметрами по умолчанию и знака равенства.
function showThirdMenu({
title = 'Заголовок',
w = 100,
h = 200
} = {}) {
alert(`${title} ${w} ${h}`);
}
// Заголовок 100 200
showThirdMenu();
Остаточные параметры, оператор расширения и псевдомассив `arguments`
В JS-функции можно передавать произвольное количество параметров в виде массива. Как в пользовательские, так и в некоторые встроенные методы. Например:
Math.max(...numbers)
Остаточные (или rest) параметры
Вызывать функцию можно с любым количеством аргументов. Даже если она определена с одним параметром, а в нее передали десять, ошибки не будет. Просто 9 аргументов не будут использованы в расчетах, но и не пропадут бесследно — сохранятся в пседомассиве arguments
.
Остаточные параметры принимают множество значений, которые интерпретируются функцией, как массив.
Записываются как произвольное имя после трех точек. Может быть, как единственным параметром, так и одним из нескольких. Во втором случае записывается последним.
const pupil = (name, ...marks) => {
console.log(name);
console.log(marks);
};
/* Первый аргумент передается в параметр name.
Остальные — в оператор расширения …marks */
pupil('Вова', 5, 4, 5);
Пример №2
const abccc = (a, b, ...c) => {
console.log(a);
console.log(b);
console.log(c);
};
/* В консоль будет выведены поочередно 'one',
'two', ['three', 'four', 'five'] */
abccc('one', 'two', 'three', 'four', 'five');
Пример №3
function showName(firstName, lastName, ...rest) {
alert(`${firstName} ${lastName} - ${rest}`);
}
/* В диалог будет выведено: Юлий Цезарь -
Император Рима. В rest попадёт массив всех
аргументов, начиная с третьего. Собранные
в rest данные станут настоящим массивом,
с методами map, forEach и пр. */
showName('Юлий', 'Цезарь', 'Император', 'Рима');
Оператор расширения (spread-оператор)
Если троеточие в параметрах — признак остаточных параметров. То троеточие в caller’е указывает на оператор расширения или спред-оператор. Он позволяет передавать в функцию массив.
const myArr1 = [3, 5, 1];
/* Вызов метода max с оператором расширения
аналогичен Math.max(3, 5, 1) — оператор
"раскрывает" массив в список аргументов) */
alert(Math.max(...myArr1));
Этим же способом мы можем передать несколько итерируемых объектов:
const myArr2 = [8, 3, -8, 1];
alert(Math.max(...myArr1, ...myArr2));
Мы можем комбинировать оператор расширения с обычными значениями:
alert(Math.max(1, ...myArr1, 2, ...myArr2, 25)); // 25
Оператор расширения можно использовать и для слияния массивов:
const merged = [0, ...myArr1, 2, ...myArr2];
alert(merged); // 0, 3, 5, 1, 8, 3, -8, 1
Использование spread-оператора для создания копии массива
const shallowCopy = [...myArr1];
const anotherShallowCopy = [
...myArr1, ...myArr2, 41
];
Spread-оператор, также, как и метод assign()
создают так называемую мелкую копию (shallow copy
). Это означает, что при дублировании вложенных объектов в новый объект будет скопирован полностью только первый уровень. Вместо вложенных объектов будут добавлены ссылки на них в «подлиннике».
☝️🧐 Чтобы полностью дублировать вложенные свойства (методы, в принципе, нельзя), используют методы JSON stringify
(конвертация в строку) и parse
.
let source =
{
a: 1,
b: {
c: 2,
},
};
let dest = JSON.parse(JSON.stringify(source));
Spread-оператор работает с любыми перебираемыми объектами
В примерах выше мы использовали массив, чтобы продемонстрировать свойства оператора расширения, но он работает с любым перебираемым объектом.
Например, оператор расширения подойдёт для того, чтобы превратить строку в массив символов:
const str = 'Привет';
alert([...str]); // П,р,и,в,е,т
Оператор расширения перебирает строку как последовательность символов, поэтому из ...str
получается “П”, “р”, “и”, “в”, “е”, “т”. Получившиеся символы собираются в массив при помощи стандартного объявления массива, через квадратные скобки — [...str]
.
Впрочем, задачу перевода разных данных в массив лучше выполнять более универсальным методом Array.from
. Он работает не только с итерируемыми объектами (такими, как строка), но и с псевдомассивами (такими, как NodeList
).
alert(Array.from(str)); // П,р,и,в,е,т
Объект arguments
— устаревший способ доступа к аргументам
Раньше в языке не было остаточных параметров, и получить все аргументы функции можно было только с помощью псевдомассива arguments
. Хотя способ всё ещё работает, мы можем найти его в старом коде.
function showName() {
alert( arguments.length );
alert( arguments[0] );
alert( arguments[1] );
// Объект arguments можно перебирать
for (let arg of arguments) alert(arg);
}
// Вывод: 2, Юлий, Цезарь
showName("Юлий", "Цезарь");
// 1, Вова, undefined (второго аргумента нет)
showName("Вова");
Хотя arguments
можно перебирать, псевдомассив не поддерживает методы массивов, например, map()
.
К тому же, arguments
всегда содержит все аргументы функции — нельзя получить их часть. А остаточные параметры позволяют это сделать.
☝️🧐 Стрелочные функции не имеют arguments
. Если мы обратимся к arguments
из стрелочной функции, то получим аргументы внешней «нормальной» функции.
Соответственно, для более удобной работы с аргументами лучше использовать остаточные параметры.
Замыкание (closure) и самовызывающиеся функции
Замыкание — вложенная функция, имеющая доступ к переменным внешних функций; при этом все её внутренние переменные для внешних скриптов скрыты. Собственные, а также доступные родительские переменные и параметры называются окружением.
Замыкание используется для ограничения области видимости и создания частных переменных, которые не могут изменить другие скрипты на странице.
Замыкания часто используются для сохранения состояния в обработчиках событий.
Самовызывающиеся функции и выражения
Замыканием может стать любая вложенная функция. Но когда замыкание создают специально, обычно в качестве родителя используют самовызывающиеся функциональное выражение или анонимную функцию. Родительские переменные и параметры передаются вложенным функциям, и в дальнейшим не возвращаются к исходным значениям до окончания сессии.
После самовызова родительской функции код оперируют только значениями, возвращаемыми вложенными функциями.
Самовызывающееся выражение или функция, как следует, из названия срабатывают без вызова, сразу после своего объявления. Используется, когда нужно выполнить код один раз и сохранить его результаты для вложенных функций, без объявления глобальных переменных). Записывается с помощью двух пар круглых скобок и точки с запятой.
/* Анонимная самовызывающаяся функция IIFE
(Immediately-invoked Function Expression) */
(() => {
// код функции
})();
/**
* Самовызывающееся функциональное выражение.
* Создает замыкание для вложенной функции.
* Переменной add присваивается результат
* самовызывающейся функции, а это — вложенная
* функция increment().
*/
const add = (() => {
/* Переменная counter, созданная в add, передает
значение вложенной функции increment() только
раз. Далее в коде уже используются результаты
increment(). В примере counter будет
равняться 0 ровно до момента, когда
increment() не увеличит его до единицы. */
let counter = 0;
/**
* Вложенная функция.
* @return {Number}
*/
function increment() {
counter += 1;
return counter;
}
return increment;
})();
add(); // Выражение вернет 1
add(); // Выражение вернет 2
add(); // Выражение вернет 3
IIFE с параметрами
// В первой строке мы определяем два параметра
((msg, times) => {
// В цикл передаем аргументы…
for (let i = 1; i <= times; i++) {
console.log(msg);
}
/* …которые получаем при вызове функции в последней
строке */
}("Hello!", 5));
// Пример из jQuery
(function($, global, document) {
// use $ for jQuery, global for window
}(jQuery, window, document));
Локальная область видимости
С каждой функцией связывается окружение, которое содержит локальные переменные из внешней области видимости (функции-родителя). Вызывая функцию на другом уровне, мы получаем и связанные с ней локальные переменные.
На книжном языке это явление еще объясняют так. Каждая функция определяют свою лексическую область действия, объект переменных LexicalEnvironment. Каждый запуск функции создает новый такой объект. На верхнем уровне им является «глобальный объект», в браузере – window. Таким образом программа создает несколько объектов с независимыми свойствами и методами.
Рекурсивные функции
В теле функции могут быть вызваны другие функции для выполнения подзадач. Но также функция может вызвать сама себя. Это называется рекурсией.
Любая рекурсивная функция может быть заменена циклом.
Пример №1
function countSheep(number) {
if (number === 0) {
console.log('Zzzzzz');
// Пока количество овец не равно 0…
} else {
// …Комментировать прыжок очередной овцы.
console.log('A sheep jumps over the fence.');
// Рекурсивный вызов функции. Аргумент number
// уменьшается на единицу
countSheep(number - 1);
}
}
countSheep(24);
Хотя в большинстве случаев цикл проще, понятнее рекурсии и не создает проблем со стеком (выделенным ресурсом оперативной памяти), используют то, что более удобно. Если реализация очевидна в терминах цикла, не следует использовать рекурсию. И наоборот.
Так, в цикле разумнее решать задачи, где результат следующего полностью зависит от результата предыдущего.
При таких задачах, как обход вложенных каталогов, когда у каждого имеется ряд своих отдельных переменных (например, количество файлов в данном каталоге), легче поддерживать рекурсию. Потому что обход одного каталога совсем не зависит от результатов обхода другого соседнего каталога, и они могут работать параллельно, независимо друг от друга. А затем в конце просто объединяют все свои результаты.
Скорость вычислений и потребление ресурсов. На малых числах циклы и рекурсии почти не отличаются. На больших — рекурсия быстрее, но при этом использует больше оперативной памяти и ресурсов ЦП (центрального процессора; CPU).
Промежуточные результаты вычисления рекурсивных функций часто сохраняются не в переменных, а в стеке (см. термины)
Пример №2. возведение в степень
function pow(x, n) {
// Пока n не равно 1…
if (n !== 1) {
/* …Функция вызывает сама себя, возвращает
промежуточные результаты, всякий раз
уменьшая аргумент степени на единицу
return 2 * pow(2, 2)
return 2 * pow(2, 1) */
return x * pow(x, n - 1);
}
return x;
}
// 2^3 = 8
alert(pow(2, 3));
Пример №3
const n = 3;
function howManyDollars(amountOfMoney) {
if (n < 0) {
/* Как правило, рекурсивная функция требует
условия прекращения — во избежание
бесконечных циклов */
return console.log(
'The negative numbers are not allowed'
);
}
/* Т.н. базовый случай. Обычно — в конструкции
if(). Условие окончания вычислений, выхода
из функции, возвращение перехода к дальнейшим
инструкциям в порядке исполнения. */
if (amountOfMoney < 1) {
return 0;
}
/* Теперь функция вызывает сама себя —
рекурсивный случай. И будет вызывать до тех
пор, пока не достигнет условия базового
случая — в рекурсивном случае должны быть
заложены соответствующие вычисления. В данном
примере (расчет сдачи при оплате покупки),
если в сдаче меньше доллара, функция
возвращает ноль — долларов покупателю
не вернут. Если amountOfMoney больше доллара,
функция откладывает 1 доллар «в сторону»,
вычитает его из суммы сдачи. И снова
запускает себя — до тех пор, пока не вычтет
из суммы сдачи все целые доллары. */
return
1 + this.howManyDollars(amountOfMoney - 1.00);
}
Пример №4 — факториал
const factorial = (n) => {
if (n <= 1) {
return 1;
}
/* Рекурсивный вызов с механизмом достижения
базового случая - уменьшением аргумента */
return n * factorial(n - 1);
};
/* В этом примере функция вызвана с аргументом 3.
Факториал числа n — произведение всех
натуральных чисел от 1 до n включительно. То
есть, нам нужно перемножить 3 * 2 * 1
и получить 6.
1) Первая итерация. 3 больше 1, значит
выполняется рекурсивный вызов. Умножаем
аргумент на факториал числа, меньшего на
единицу.
n = 3
n - 1 = 2
3 * factorial(2)
Но факториал двух нам тоже не известен.
2) Рекурсивный вызов — factorial(2).
2 * factorial(1)
Факториал единицы пока также не известен,
поэтому процедура повторяется.
3) Достигнут базовый случай n <= 1
Мы получили первый ответ, который используем
поднимаясь по стеку, по логической лестнице
вверх.
4) Компьютер вычисляет факториал двух 2.
2 * 1 = 2
5) Затем, когда получено значение факториала
двух, компьютер возвращается к расчету
исходного задания — факториалу трех:
3 * 2 = 6 */
factorial(3);
Пример №5
let change = 0;
/* Рекурсивные функции могут иметь сколько угодно
параметров… */
function howManyCoins(
coinName, coinAmount, coinsSoFar
) {
if (change < coinAmount) {
console.log(`${coinsSoFar} ${coinName}`);
} else {
change -= coinAmount;
}
/* …Но в рекурсивном случае меняется, как
правило, один или два. При этом
задействованные параметры должны меняться
так, чтобы приближать исполнение условия
базового случая. В данном примере
увеличивается количество монет определенного
номинала в сдаче — до тех пор, пока сдача
больше суммы монеток этого номинала */
return howManyCoins(
coinName, coinAmount, coinsSoFar + 1
);
}
howManyCoins('dollar bills', 1.00, 0);
howManyCoins('quarters', 0.25, 0);
howManyCoins('dimes', 0.10, 0);
Объекты
Введение
Составной тип данных — коллекция свойств и методов (функций) в формате ключ-значение.
Объект может быть пользовательским или встроенным. За исключением примитивных типов данных (строк, чисел, булевых), всё в JS является объектами: массивы (не только ассоциативные, но и индексные), функции, глобальный объект, объект Date
, объект Math
и т.д.
В языках программирования выражение, создающее объект, называется литералом. После закрывающей скобки ставится точка с запятой.
☝️🧐 Рекомендация Airbnb — добавлять запятую после последней пары. Так минимизируется риск ошибки при правке объектов. eslint: comma-dangle
const myObj = {
key: 'value',
key2: 'value',
key3: 'value',
objMethod(some, params) { /* … */ },
};
Объекты могут содержать данные любого типа…
const myPerson = {
name: 'Вова',
type: 'Most excellent',
// …в том числе — массивы…
interests: ['rock', 'women', 'cognac'],
// …в том числе — методы (функции)
love(some, params) { /* … */ },
read(some, params) { /* … */ },
watchSeries(some, params) { /* … */ },
// Объекты могут содержать и другие объекты
daughter: { name: 'Paula', age: 27 },
wife: { name: 'Sonya', age: 48 },
/* Если в «ключе» есть пробелы, его необходимо
заключить в кавычки. */
'has job': true,
};
Создание пустого объекта
Пустые объекты создаются для того, чтобы их свойства можно было добавлять динамически в зависимости от логики кода.
const emptyObject = {};
Создание экземпляра существующего объекта
Метод Object.create()
создаёт новый объект с указанным прототипом и свойствами.
const animal = {
say() {
console.log(`${this.name} goes ${this.voice}`);
},
};
const dog = Object.create(animal);
dog.name = 'Dog';
dog.voice = 'woof';
Однако более практичный способ — создать экземпляр класса.
const cat = new Animal();
cat.name = 'Cat';
cat.voice = 'meow';
Динамические ключи
Потребность в такой возможности возникает редко, но в ключах объекта можно использовать переменные.
const prefix = 'my';
const person = {
[`${prefix}Name`]: 'Vova',
[`${prefix}Age`]: 50,
};
Свойства
Свойства объектов представляют собой пары ключ: значение
.
const myObj = {
// Это свойства
name: 'Вова',
age: 50,
whatIamDoingRightNow() {
// А это метод
},
};
«Точечная запись» или «точечная нотация»
☝️🧐 В вычислениях свойство не используют напрямую. Сначала сохраняют в переменную.
let itsMe = myObj.name;
Но используют в условиях, в том числе условиях циклов…
if (fiat.year < 1965)
for (let i = 0; i < fiat.passengers; i++)
А также в шаблонных строках
const speak = `${dog.name} says ${dog.bark} when he wants to ${dog.activity}`;
Квадратные скобки
Свойство можно вызвать и из квадратных скобок. Способ используется реже, но полезен, когда при вызове вместо имени свойства нужно использовать переменную. В квадратных скобках может находиться любое выражение — необходимо лишь, чтобы при его вычислении получалось имя свойства, представленное строкой.
itsMe = myObj[name];
// eslint-disable-next-line no-useless-concat
itsMe = myObj['na' + 'me'];
itsMe = 'name';
console.log(myObj[itsMe]);
Изменение или добавление свойств и методов
Достаточно указать существующий или новый ключ и его значение.
myObj.name = 'Вова';
Удаление свойства объекта
С помощью оператора delete
удаляются и ключ, и значение. delete
не может быть использован для удаления свойств встроенных объектов.
delete myObj.age;
Атрибуты свойств
Специфичный атрибуты свойств — writable
. Определяет, доступно ли данное свойство для записи (по умолчанию, true).
Общие атрибуты свойств и методов:
enumerable
— определяет доступность свойства для перечисленияconfigurable
— определяет доступность свойства для изменения (например, может ли свойство быть удалено, может ли быть изменен какой-либо атрибут свойства).
Перебор свойств
Для перебора используется цикл метод forEach
, а также методы объекта keys
, values
, entries
и методы массива map
, every
, some
, filter
, reduce
, find
, findIndex
.
⛔️ Ранее для перебора использовались цикл for...in
, однако он перебирает свойства по всей цепочке прототипов, поэтому Airbnb рекомендует его избегать. Равно, как и перебор с помощью цикла for...of
.
Методы
Это функции объектов. Полезны, чтобы
- обновить свойства объектов,
- сделать вычисления на основе свойств объектов
const fiat = {
make: 'Fiat',
model: '500',
year: 1957,
started: false,
/* Синтаксис: как объявление функции, только без
ключевого слова */
beep(times) {
for (let i = times; i >= 0; i--) {
alert.log('Бип!');
}
},
};
Вызов метода объекта
Методы вызываются так же, как функции: имя, аргументы в скобках. Только перед именем метода стоит еще имя объекта.
fiat.beep(4);
const goComeOn = fiat.beep(10);
Добавление метода
fiat.start = () => {
this.started = true;
};
Атрибуты методов
Специфичные атрибуты методов:
set
— содержит setter, функцию, которая вызывается при записи свойства;get
— содержит getter, функцию, которая вызывается при чтении свойства.
Общие атрибуты свойств и методов:
enumerable
— определяет доступность свойства для перечисленияconfigurable
— определяет доступность свойства для изменения (например, может ли свойство быть удалено, может ли быть изменен какой-либо атрибут свойства).
Методы встроенных объектов
Методы встроенных объектов — Global
, String
, Array
, Number
, Math
, Function
, Date
, RegExp
, Boolean
, Object
— предназначены для типовых операций с данными
const message = 'Hello world!';
const sayItLoud = message.toUpperCase();
Прототип объекта
У каждого объекта в JS, кроме самого общего Object
и созданных из null
, есть свой шаблон, по которому он был создан. Символ, в терминологии графических редакторов. Шаблон, чьи свойства и методы наследуют экземпляры — последующие поколения. Это и есть прототип — объект, ставший шаблоном для других объектов.
Все свойства и методы, которые наследуют потомки, хранятся в одном свойстве — prototype
. Это набор, к которому можно обратиться через точечную нотацию: myObj.prototype
.
Объект Date
передает наследникам Date.prototype
. Массив — Array.prototype
. Объект Person
в примере передает объектам myDad
и myMum
Person.prototype
. И все они наследуют свойства и методы общего предка — Object.prototype
. В частности, myDad
и myMom
наследуют через Object.prototype
методы keys
, values
и т. д.
class Person {
/**
* @param {string} firstName
* …other params
*/
constructor(firstName, lastName, age, eyes) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.eyes = eyes;
}
}
// Создаем объекты
const myDad = new Person(
'Александр', 'Никишин', 70, 'blue',
);
const myMum = new Person(
'Галина', 'Никишина', 73, 'brown',
);
Добавление свойств и методов в прототип предка
Свойства и методы экземплярам можно добавить, набрав названия экземпляра и свойства через точку. А если обратиться похожим образом к предку, но добавив после его имени еще и свойство prototype
, то можно изменить конструктор предка и все его экземпляры.
⚠️ Однако расширять можно только пользовательские объекты и никогда встроенные.
Person.prototype.nationality = 'Russian';
Person.prototype.name = () =>
`${this.firstName} ${this.lastName}`;
Добавление или удаление свойств и методов
class Dog {
constructor(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
}
goodBoyReaction() {
console.log(`${this.name} wags its tail`);
}
}
// Экземпляр класса Dog
const fido = new Dog('Fido', 'Mixed', 38);
Объекту можно добавить свойство или метод, даже если они не созданы в классе. Можно удалить унаследованное.
fido.bark = () => {
console.log('Woof! Woof!');
};
fido.owner = 'Bob';
Свойства могут быть удалены оператором delete. Методы удалить невозможно — экземпляр класса наследует все методы.
delete fido.weight;
`Object.defineProperty` — изменение или добавление свойства
Изменение или добавление свойства
const person = {
firstName: 'John',
lastName: 'Doe',
language: 'EN',
};
Object.defineProperty(
person, 'language', { value: 'NO' }
);
Object.defineProperty(
person, 'year', { value: 2021 }
);
Изменение атрибутов свойств
Тот же метод defineProperty
позволяет изменить атрибуты свойств, выбрав булево значение true или false.
writable
— возможность записиenumerable
— возможность перечисленияconfigurable
— возможность настройки
Object.defineProperty(
person, 'language', { writable: false }
);
Установка геттеров и сеттеров
Object.defineProperty(person, 'fullName', {
get() {
return `${this.firstName} ${this.lastName}`;
},
});
Object.defineProperty(person, 'age', {
set(value) { this.age = value; },
});
Добавление или изменение нескольких свойств
Object.defineProperties(person, {
lastName: {
value: 'Nikishin',
writable: true,
},
language: {
value: 'RU',
writable: false,
},
});
Метод `Object.assign()` — копирование свойств
Используется для копирования значений всех собственных перечисляемых свойств из одного или более объектов в целевой объект. Вложенные объекты копируются, как ссылки.
const dest = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
const obj = Object.assign(dest, source1, source2);
// В консоль будет выведено { a: 1, b: 2, c: 3 }
console.log(obj);
Также можно использовать для дублирования объекта. Однако предпочтительней для этих целей использовать оператор расширения (spread-оператор).
Другие встроенные методы объекта
getOwnPropertyNames
— получение списка свойств объекта
const person = {
firstName: 'John',
lastName: 'Doe',
language: 'EN',
year: 2021,
age: 51,
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
};
/* Результатом примера будет массив
[ 'firstName', 'lastName', 'language', 'year',
'fullName', 'age' ] */
console.log(Object.getOwnPropertyNames(person));
keys
— получение списка только перечисляемых свойств объекта
/* Результатом примера будет массив
[ 'firstName', 'lastName', 'year', 'fullName', 'age' ] */
Object.defineProperty(
person, 'language', { enumerable: false }
);
Object.keys(person);
getOwnPropertyDescriptor
— получение значения и атрибутов собственного свойства
/* Результатом примера будет
{ value: 'Vladimir', writable: true,
enumerable: true, configurable: true } */
console.log(
Object.getOwnPropertyDescriptor(
person, 'firstName'
)
);
getPrototypeOf
— доступ к прототипу
Возвращает прототип, то есть набор свойств и методов указанного объекта.
console.log(Object.getPrototypeOf(person));
preventExtensions
— запрет добавления новых свойств
Object.preventExtensions(person);
isExtensible
— проверка возможности добавления новых свойств
Возвращает true, если можно добавлять новые свойства.
Object.isExtensible(person);
seal
— запрет изменения значений существующих свойств
Object.seal(person);
isSealed
— проверка возможности изменения значений существующих свойств
Object.isSealed(person);
freeze
— запрет любых изменений объекта
Object.freeze(person);
isFrozen
— проверка возможности изменений объекта
Object.isFrozen(person);
JSON.stringify
и toString
— перевод в строку
Универсальный метод toString
полезен для работы с массивами и числами, но для объектов практически непригоден.
const person = {
firstName: 'Vladimir',
lastName: 'Nikishin',
};
/* результатом применения метода будет строка
'[object Object]' */
const meAsObj = person.toString();
Вместо toString
с объектами лучше использовать метод JSON.stringify
или функции, отделяющие ключи и составляющие строки исключительно из значений.
/* результатом применения метода будет строка
'{firstName: 'Vladimir', lastName: 'Nikishin'}' */
const iAm = JSON.stringify(person);
Добавление объекта в массив
Объект будет добавлен в конец списка.
const contacts = ['some', 'data'];
contacts[contacts.length] = {
firstName: 'Vladimir',
lastName: 'Nikishin',
phoneNumber: '+791612345678',
email: 'vladimir.nikishin@gmail.com',
};
Сравнение объектов
При сравнении объектов сравниваются ссылки на них, а не их содержимое.
const myObj1 = {
name: 'Vladimir',
age: 51,
};
const myObj2 = {
name: 'Vladimir',
age: 51,
};
const myObj3 = myObj1;
Не важно, что хранится в этих объектах. Если переменные разные, и объекты будут считаться разными. Результат сравнения в примере — false
.
if (myObj1 === myObj2) { /* ... */ }
Две ссылки равны только в том случае, если они ссылаются на один объект. В примере myObj3
ссылается на тот же объект, что и myObj1
. Результат сравнения — true
if (myObj1 === myObj3) { /* ... */ }
Деструктурирующее присваивание или просто деструктуризация
См. также деструктуризацию в параметрах
Деструктуризация – это особый синтаксис присваивания, при котором можно присвоить нескольким переменным свойства объекта или элементы массива.
Деструктуризация
- упрощает получение свойств из объектов;
- поддерживает вложенность и значения по умолчанию;
- поддерживает остаточные свойства (rest-элемент);
- работает с параметрами функций.
Базовый синтаксис для объектов
// создаем переменные из свойств объекта gulp
const { src, dest } = gulp;
//... или
const { var1, var2 } = { var1: 'one', var2: 2 };
Объект справа – уже существующий, который мы хотим разбить на переменные. А слева – список переменных, в которые нужно соответствующие свойства записать. Таким образом мы явным образом указываем, какие свойства в какие переменные должны быть сохранены. Пример.
let options = {
title: 'Меню',
width: 100,
height: 200,
};
const { title, width, height } = options;
alert(title); // Меню
alert(width); // 100
alert(height); // 200
Свойства options.title
, options.width
и options.height
были присвоены соответствующим переменным.
Если хочется присвоить свойство объекта в переменную с другим именем, например, чтобы свойство options.width
пошло в переменную w
, то можно указать соответствие через двоеточие, вот так:
options = {
title: 'Меню',
width: 100,
height: 200,
};
const { width: w, height: h, title } = options;
alert(title); // Меню
alert(w); // 100
alert(h); // 200
В примере выше свойство width
отправилось в переменную w
, свойство height
– в переменную h
, а title
– в переменную с тем же названием.
Значения по умолчанию
Если каких-то свойств в объекте нет, можно указать значение по умолчанию через знак равенства =
.
options = {
title: 'Меню',
};
const { width = 100, height = 200, title } = options;
alert(title); // Меню
alert(width); // 100
alert(height); // 200
Можно и сочетать одновременно двоеточие и равенство, чтобы одновременно изменить наследуемое имя и значение:
options = {
width: 100,
title: 'Меню',
};
const { width: w = 200, height: h = 200, title } = options;
alert(title); // Меню
alert(w); // 100
alert(h); // 200
Значениями по умолчанию могут быть любые выражения или даже функции. Они выполнятся, если значения отсутствуют.
В коде ниже prompt
запросит width
, но не title
:
options = {
title: 'Menu',
};
const { width = prompt('width?'), title = prompt('title?') } = options;
alert(title); // Menu
alert(width); // (результат prompt)
Необязательно сохранять все свойства в переменные
Если у нас есть большой объект с множеством свойств, можно взять только то, что нужно:
options = {
title: 'Menu',
width: 100,
height: 200,
};
// взять только title, игнорировать остальное
const { title } = options;
alert(title); // Menu
// импортировать только src и dest, игнорировать остальное
import { src, dest } from 'gulp';
«Остаток» объекта (остаточные свойства, rest-элемент)
В случаях, когда свойств больше, чем переменных, «остаток» можно сохранить с помощью spread-оператора.
options = {
title: 'Menu',
height: 200,
width: 100,
};
// title = свойство с именем title
// rest = объект с остальными свойствами
const { title, ...rest } = options;
// сейчас title="Menu",
// rest={height: 200, width: 100}
alert(rest.height); // 200
alert(rest.width); // 100
Вложенная деструктуризация
Деструктурировать можно и вложенные объекты.
const person = {
name: {
first: 'Vladimir',
last: 'Nikishin',
},
age: 51,
};
const { name: { first, last } } = person;
Умные параметры функций — см. в справочнике.
Объединение объектов
// Есть два объекта
const person = {
name: 'Vladimir Nikishin',
gender: 'Male'
};
const tools = { computer: 'Mac', editor: 'VS Code' };
Создадим новый объект, который унаследует свойства предшественников. Используем оператор расширения ...
const summary = { ...person, ...tools };
В результате объект summary
наследует содержание (свойства) двух предков.
{
"computer": "Mac",
"editor": "VSCode",
"gender": "Male",
"name": "Vladimir Nikishin",
}
Тем же манером можно сделать копию только одного объекта.
const shallowCopy = { ...person };
Комбинированный синтаксис создания объектов
Spread-оператор можно комбинировать с любым другим синтаксисом создания объектов.
const defaults = { host: 'localhost' };
const preferences = { user: 'root' };
const port = 8080;
const result = {
...defaults,
...preferences,
port,
connect() {
// some method
},
};
Object.assign
Тех же результатов можно достичь с помощью метода [Object.assign](/js/advanced-theory.html#topic-objects-assign-copy-properties)
, но с оператором расширения операция получается лаконичней и изящней.
Аксессоры: «геттеры» и «сеттеры»
Есть два типа свойств объекта.
Первый тип это стандартые свойства-данные (data properties) — пары «ключ: значение».
Второй тип свойств — аксессоры (accessor properties). По сути это функции, которые используются для присвоения и получения значения, но во внешнем коде вызываются через точечную нотацию — как обычные свойства объекта.
Конечно, чтение и изменение свойств объектов возможно и без акссесоров. Но они позволяют программно обрабатывать входные данные, что даёт, минимум, два преимущества перед свойствами-данными.
- Аксессоры позволяют избегать дублирования данных. Например, когда в объекте или классе уже есть свойства
firstName
иlastName
, а нужно еще иfullName
. - Позволяют проверять входные данные и контролировать вывод данных — например, в верхнем регистре.
Впрочем, там где логика для доступа и изменения свойств не нужна, конечно, не нужно усложнять программу и обходиться свойствами-данными.
Синтаксис
Для создания свойств-аксессоров в современном JS предусмотрены ключевые слова get
и set
.
const person = {
firstName: 'John',
lastName: 'Doe',
language: 'en',
// Сеттер
set name(value) {
const split = value.split(' ');
[this.firstName, this.lastName] = split;
},
// Геттер
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
/* В принципе, геттер можно заменить вот таким
методом. Но так никто не делает — дурной тон */
fullNameOnceAgain: () =>
`${this.firstName} ${this.lastName}`,
};
Геттер срабатывает, когда obj.propName
читается, сеттер – когда значение присваивается.
Сеттер
Обычно создает новое свойство на основе полученного аргумента (set
должен иметь ровно один параметр).
В примере сеттер принимает строку, разделяет ее по пробелу и сохраняет первый фрагмент firstName
, а второй — в lastName
.
Сеттеры используются чаще всего в сочетании с геттерами.
Геттер
В примере геттер возвращает псевдо-свойство fullName
.
⚠️ Кстати, геттер не может иметь параметров.
Изменение свойств firstName
и lastName
с помощью сеттера name
.
person.name = 'Владимир Никишин';
console.log(user.firstName); // Владимир
console.log(user.lastName); // Никишин
Доступ к полному имени с помощью геттера.
console.fullName(person.fullName);
Доступ к полному имени с помощью метода fullNameOnceAgain — со скобками
console.log(person.fullNameOnceAgain());
Добавление геттеров и сеттеров в существующий объект — Object.defineProperty()
const myObj = { counter: 0 };
Object.defineProperty(myObj, 'reset', {
get() { this.counter = 0; },
});
Object.defineProperty(myObj, 'increment', {
get() { this.counter += 1; },
});
Object.defineProperty(myObj, 'decrement', {
get() { this.counter -= 1; },
});
Object.defineProperty(myObj, 'add', {
set(value) { this.counter += value; },
});
Object.defineProperty(myObj, 'subtract', {
set(value) { this.counter -= value; },
});
Вызовы геттеров и сеттеров объекта изменяют показания счетчика
myObj.reset;
myObj.add = 5;
myObj.subtract = 1;
myObj.increment;
myObj.decrement;
console.log(myObj.counter);
Передача объектов в функции — в качестве аргументов
// Объявляем функцию
const ageDifference = (person1, person2) => {
person1.age - person2.age;
}
/**
* Создаем класс
*/
class Person {
/**
* @param {string} name
* @param {number} age
*/
constructor(name, age) {
this.name = name;
this.age = age;
}
}
// Создаем объекты
const alice = new Person('Alice', 30);
const billy = new Person('Billy', 25);
/* Объявляем переменную diff и передаем ей значение
функции ageDifference */
const diff = ageDifference(alice, billy);
Встроенные объекты и глобальные свойства и методы
Встроенные объекты
В JavaScript есть стандартные встроенные объекты, доступные из любой точки программы: Date
, Array
, Object
и др. Их пользовательские экземпляры наследуют методы и свойства.
Создаем свой объект на основе встроенного объекта Date
.
const now = new Date();
Используем методы встроенного в своем экземпляре.
const theYear = now.getFullYear();
Создаем еще один объект на основе встроенного, передаем ему строку
const birthday = new Date('May 1, 1983');
⛔️ Встроенные объекты, в принципе, можно расширять, добавляя свойства и методы через свойство prototype
, но не рекомендуется (eslint no-extend-native).
String.prototype.cliche = () => {
const cliche = [
'lock and load',
'touch base',
'open the kimono',
];
for (let i = 0; i < cliche.length; i++) {
const index = this.indexOf(cliche[i]);
if (index >= 0) {
return true;
}
}
return false;
};
Фундаментальные объекты
Общие языковые объекты, функции и ошибки.
- Object
- Function
- Boolean
- Symbol
- Error
- EvalError
- InternalError
- RangeError
- ReferenceError
- SyntaxError
- TypeError
- URIError
Number
Является объектом-обёрткой, позволяющей работать с числовыми значениями.
Создаётся через конструктор Number(). Приводит значение к числовому типу. Если это невозможно, возвращает NaN
.
console.log(
`${Number(true)}
${Number(false)}
${Number(new Date())}
${Number('999')}
${Number('999 888')}`,
);
String
Используется, чтобы представить и конструировать последовательность символов.
console.log(String(Boolean(0))); // Вернет false
console.log(String(Boolean(1))); // True
// Sun Feb 03 2019 11:06:45 GMT+0100 (CET)
console.log(String(new Date()));
console.log(String('12345')); // 12345, как строка
console.log(String(12345)); // 12345, как строка
Другие
Глобальные свойства и методы JS
Глобальные свойства и методы JS могут быть использованы с любым объектом программы.
Глобальные свойства, возвращающие простое значение
NaN
— значение Not-a-NumberInfinity
— значение, представляющее положительную бесконечность.-Infinity
— значение, представляющее отрицательную бесконечность. Хотя типInfinity
числовой в коде свойство используется, как слово.undefined
— свойство возвращается когда, переменная не инициализирована (т. е. ей не присвоено значение) или свойство объекта, элемент массива отсутствуют.- литерал
null
.
Функции
Традиционно методы глобального объекта называют глобальными функциями.
encodeURIComponent
/decodeURIComponent()
иdecodeURI()
/encodeURI()
parseInt()
parseFloat()
isFinite()
encodeURIComponent
/decodeURIComponent()
и decodeURI()
/encodeURI()
Кодируют и декодируют строки согласно стандарту веб-адресов. Превращая, например, пробел в управляющую последовательность %20
и обратно.
decodeURI()
/encodeURI()
предназначены для использования в полном URI. ПоэтомуencodeURI()
не кодирует стандартные спецсимволы, используемые в веб-адресах:; / ? : @ & = + $ , #
.encodeURIComponent
/decodeURIComponent()
предназначены для использования на частях URI, которая лежит между разделителями;/?: @и = + $, #
. В отличие отencodeURI
encodeURIComponent
кодирует всё.
Результатом применения encodeURI()
в примере будет
https://w3schools.com/my%20test.asp?name=st%C3%A5le&car=saab
… А результатом применения encodeURIComponent —
https%3A%2F%2Fw3schools.com%2Fmy%20test.asp%3Fname%3Dst%C3%A5le%26car%3Dsaab
То есть все двоеточия, слеши и пр. будут закодированы.
const uri = 'my test.asp?name=ståle&car=saab';
const enc = encodeURI(uri);
const encComp = encodeURIComponent(uri);
const dec = decodeURI(enc);
const decComp = decodeURIComponent(encComp);
parseInt()
Извлекает из строки первое целое число, если строка начинается с цифры или пробела. Вторым параметром, указывается основа система счисления, так называемый radix — целое число в диапазоне между 2 и 36.
☝️🧐 В большинстве случаев система десятичная, b 10
— это значение redix’a по умолчанию. Но все равно всегда следует его указывать, чтобы исключить ошибки считывания.
// Вернет 10, как число
console.log(parseInt('10', 10));
console.log(parseInt('10.00', 10)); // 10
console.log(parseInt('10.33', 10)); // 10
console.log(parseInt('34 45 66', 10)); // 34
console.log(parseInt(' 60 ', 10)); // 60
console.log(parseInt('40 years', 10)); // 40
// В этом случае вернет NaN, т.к. строка
// начинается ни с пробела, и ни с числа
console.log(parseInt('He was 40', 10));
console.log(parseInt('010', 10)); // 10
parseInt()
не рекомендуется использовать с двоичной, восьмеричной и шестнадцатеричной системами. Вместо этого использовать числовые литералы.
console.log(parseInt('10', 16)); // 16
console.log(parseInt('0x10', 16)); // 16
console.log(0x10); // 16
parseFloat()
Извлекает из строки первое десятичное число, если строка начинается с цифры или пробела.
// Вернет 48.3, как число
console.log(parseFloat('48.3'));
console.log(parseFloat(' 4')); // 4
console.log(parseFloat('He is 48')); // NaN
isFinite()
Определяет, является ли переданное значение конечным числом. Если необходимо, параметр сначала преобразуется в число.
// Результат проверки — false
isFinite(Infinity);
// false
isFinite(NaN);
// false
isFinite(-Infinity);
// true
isFinite(0);
// true
isFinite(2e64);
// true, но было бы false если использовать более
// надежный вариант проверки Number.isFinite('0')
isFinite('0');
Объект `Date`
С помощью этого объекта можно создать экземпляр, содержащий дату и время. Если никаких аргументов передано не было, конструктор создаёт объект Date
для текущих даты и времени, согласно системным настройкам.
Свойство constructor
Свойство всех встроенных объектов, содержит функцию-конструктор, которая, в свою очередь, создает новый экземпляр.
Вызывается с помощью ключевого слова new
. В параметрах можно указать нужную дату и формат.
let d = new Date();
JS автоматически конвертирует объект в строку при любой операции. Поэтому в примере ниже в HTML будет передана одинаковая строка с указанием часового пояса. На момент конспектирования это было ‘Fri Feb 08 2019 10:45:14 GMT+0100 (CET)’
document.getElementById('demo').innerHTML = d;
document.getElementById('demo').innerHTML = d.toString();
☝️🧐 При выводе переменной d
из примера в консоль, получим дату в формате браузера (в примерах — формат консоли VSCode)
1995-12-17T02:24:00.000Z
В консоли Chrom’а таже дата будет выведена, как
Sun Dec 17 1995 03:24:00 GMT+0100 (Центральная Европа, стандартное время)
☝️🧐 Если при создании экземпляра Date()
в параметры передается больше одного аргумента, данные интерпретируются как локальное время.
☝️🧐 Объект, созданный с помощью конструктора Date
статичный. Время в нем не меняется вместе с компьютерными часами.
// Дата и время
d = new Date('December 17, 1995 03:24:00');
// Запись дня и месяца может быть европейской
d = new Date('15 February 2019"');
// Месяц может быть записан аббревиатурой
d = new Date('15 Feb 2019"');
// Только дата в предпочтительном ISO-формате
d = new Date('2019-02-28');
// Вместо дефисов можно использовать слеши
d = new Date('2019/02/28');
// Дата и время в ISO-формате
d = new Date('1995-12-17T03:24:00');
// Дата и время по Гривичу
d = new Date('2015-03-25T12:00:00Z');
Чтобы создать объект даты-времени по часовому поясу, отличному от Гринвича и часового пояса системы, надо отнять или добавить разницу к времени по Гринвичу в формате GMT +/- HHMM. Окончание Z (время по Гринвичу) перед арифметическим символом ставить не нужно.
В примере добавляется один час — создается дата в центральноевропейском времени.
d = new Date('2019-02-15T18:26:31+01:00');
d = new Date(''); // Снова — текущая дата и время
Можно указать от двух до шести параметров через запятую: год, месяц, день, часы, минуты, секунды, миллисекунды. При попытке указать только год, цифра будет рассчитана, как миллисекунды и будет добавлена к 1 января 1970-го.
// Дата и время без миллисекунд
d = new Date(1995, 11, 17, 3, 24, 0);
d = new Date(1995, 11, 17); // Только дата
Можно задавать дату в миллисекундах — цифра будет прибавлена или вычтена из 1 января 1970-го.
d = new Date(-100000000000);
Свойство prototype
Свойство всех объектов — в том, числе встроенных — содержит все остальные свойства и методы, передаваемые по наследству.
⛔️ Однако использовать это свойство встроенных объектов опасно и не рекомендуется.
// eslint-disable-next-line no-extend-native
Date.prototype.myMet = () => {
if (this.getMonth() === 0) {
this.myProp = 'January';
}
if (this.getMonth() === 1) {
this.myProp = 'February';
}
// etc
};
Базовые методы получения даты и времени
getDate()
. Возвращает день месяца: с 1-го по 31-йgetDay()
. День недели: с 0 (воскресенье) по 6-йgetMonth()
. Месяц: с 0 (январь) по 11getFullYear()
. Год — четырехзначное числоgetHours()
. Час: 0-23getMinutes()
. Минуты: 0-59getSeconds()
. Секунды: 0-59getMilliseconds()
. Миллисекунды: 0-999
d.getDate(); // 6 февраля вернул 6
d.getDay(); // 6 февраля, в среду вернул 3
d.getMonth(); // 6 февраля вернул 1
d.getFullYear(); // 06.02.2019 вернул 2019
d.getHours(); // В 12:16 вернул 12
То же самое по Гринвичу
getUTCDate()
. День месяца: с 1-го по 31-йgetUTCDay()
. День недели: с 0 по 6-йgetUTCFullYear()
. Год — четырехзначное числоgetUTCHours()
. Час: 0-23getUTCMilliseconds()
. Возвращает the milliseconds, according to universal time (from 0-999)getUTCMinutes()
. Возвращает the minutes, according to universal time (from 0-59)getUTCMonth()
. Возвращает the month, according to universal time (from 0-11)getUTCSeconds()
. Возвращает the seconds, according to universal time (from 0-59)
d.getUTCHours(); // В 12:17, в ЧГ вернул 11
Метод getTimezoneOffset()
Возвращает разницу между временем по Гринвичу и местным временем в минутах
d.getTimezoneOffset(); // В ЧГ вернул -60 (минут)
Время с полночи первого января 1970 до указанной даты
getTime()
. Возвращает миллисекундыnow()
. То же самое с использованием объета DatevalueOf()
. То же самое (метод общий для всех потомков Object)UTC()
. То же самое по Гринвичуparse()
. Возвращает время до указанной даты
// 6 февраля 2019 в 12:21 вернул 1549452065468
d.getTime();
// 6 февраля 2019 в 12:25 вернул 1549452314657
Date.now();
Date.parse('October 31, 1970'); // 26175600000
Базовые методы установки даты и времени
setDate()
. Установка дня месяца в объекте датыsetTime()
. Установка даты в миллисекундах относительно полуночи 01.01.1970setMonth()
. МесяцsetFullYear()
. ГодsetHours()
. ЧасsetMinutes()
. МинутыsetSeconds()
. СекундыsetMilliseconds()
. Миллискекунды
Если вывести в консоль, получим дату и время в формате браузера. Например, 2019-02-15T09:38:01.511Z
☝️🧐 Z после времени означает, что дата и время показаны по Гринвичу
d.setDate(15);
// 2043-11-29T09:58:02.588Z
d.setTime(2332403882588);
Тоже самое по Гринвичу
setUTCDate()
. День месяцаsetUTCMonth()
. МесяцsetUTCFullYear()
. ГодsetUTCHours()
. ЧасsetUTCMinutes()
. МинутыsetUTCSeconds()
. СекундыsetUTCMilliseconds()
. Миллискекунды
Методы конвертации в строку
toLocaleString()
. Конвертирует весь объект Data в строку без указания часового пояса.toLocaleDateString()
. Только дата в местном формате без указания ч. пояса.toLocaleTimeString()
. Только время в местном формате без указания ч. пояса.toString()
. Конвертирует весь объект Data. JS использует этот метод автоматически при любой операции с экземпляромDate()
.toUTCString()
. То же самое по Гринвичу.toDateString()
. Конвертирует только дату.toTimeString()
. Конвертирует только время.toISOString()
. Строка в ISO стандарте.toJSON()
. Строка в формате JSON, который, на самом деле идентичен предыдущему, ISO-формату. Оба метода вернут что-то типа этого: 2019-02-08T09:50:41.294Z.
d.toLocaleString(); // '2019-2-8 10:47:38'
d.toLocaleDateString(); // '2019-2-8'
Объект `Math`
Методы
Позволяют производить математические операции над числами. В отличие от других глобальных объектов Math()
не имеет конструктора. То есть нельзя создать его экземпляр.
Округление. Меньше 0.5 — в меньшую сторону, больше 0.5 — в большую.
let myNum = Math.round(4.7); // 5
myNum = Math.round(4.4); // 4
Округление в меньшую сторону вне зависимости от цифр после запятой.
myNum = Math.floor(4.7); // 4
Практически то же самое. Получить целую часть числа, удалив всех дробные знаки
myNum = Math.trunc(4.7); // 4
Округление в большую сторону.
myNum = Math.ceil(4.4); // 5
Выбор максимального числа из двух или более аргументов
myNum = Math.max(0, 150, 30, 20, -8, -200); // 150
Выбор минимального числа из двух или более аргументов
myNum = Math.min(0, 150, 30, 20, -8, -200); // -200
√ Квадратный корень
myNum = Math.sqrt(64); // 8
Двоичный логарифм — по основанию 2
myNum = Math.log2(64)); // 6 (2^6 = 64)
Десятичный логарифм — по основанию 10
myNum = Math.log10(100000)); // 5 (10^5 = 100000)
Перевод в абсолютное — т.е. положительное — число
myNum = Math.abs(-4.7); // 4.7
Синус угла
☝️🧐 Методы получения синуса, косинуса, тангенса принимают аргументы в радианах. Чтобы использовать значение угла в градусах, надо в аргументе перевести его, умножив на π/180.
В примере вычисляется синус прямого угла.
myNum = Math.sin(90 * (Math.PI / 180)); // 1
Косинус угла
// myNum === 0.7071067811865476
myNum = Math.cos(45 * (Math.PI / 180));
Тангенс угла
// myNum === 1.7320508075688767
myNum = Math.tan(60 * (Math.PI / 180));
Произведение в степень. Первый параметр — возводимое число, второй — степень.
⛔️ Вместо этого метода рекомендуется использовать оператор **
.
myNum = Math.pow(8, 2); // 64 👎
myNum = 8 ** 2; // 64 👍
Константы
Объект Math содержит несколько математических констант: логарифмы, число е
. А для повседневной практики может пригодится число π
.
🧐☝️ Логарифм — это показатель степени, в которую надо возвести основание a
, чтобы получить число b
. Записывается, как log
a b
. Произносится, как «логарифм b по основанию a».
myNum = Math.PI; // 3.141592653589793
Константы для узкоспециализированных задач
Квадратный корень из 2 — √2
myNum = Math.SQRT2; // 1.4142135623730951
Квадратный корень из ½ — √½
myNum = Math.SQRT1_2; // 0.7071067811865476
Число e
— основание натурального логарифма, иррациональное и трансцендентное число, приблизительно равное 2,71828. Иногда e называют числом Эйлера.
myNum = Math.E; // 2.718281828459045
Натуральный логарифм двух. (Натуральный логарифм — это логарифм по основанию числа e
).
myNum = Math.LN2; // 0.6931471805599453
Натуральный логарифм десяти
myNum = Math.LN10; // 2.302585092994046
Двоичный логарифм из e
myNum = Math.LOG2E; // 1.4426950408889634
Десятичный логарифм из e
myNum = Math.LOG10E; // 0.4342944819032518
Методы для узкоспециализированных задач
Кубический корень числа
myNum = Math.cbrt(125); // 5
Натуральный (по основанию e) логарифм числа
// То же, что и Math.LN2 - 0.6931471805599453
myNum = Math.log(2);
Гиперболический синус числа
myNum = Math.sinh(3); // 10.017874927409903
Гиперболический косинус числа
myNum = Math.cosh(3); // 10.067661995777765
Гиперболический тангенс числа
myNum = Math.tanh(3); // 0.7615941559557649
Арксинус в радианах
myNum = Math.asin(0.5); // 0.5235987755982989
Гиперболический арксинус числа
myNum = Math.asinh(1); // 0.881373587019543
Арккосинус в радианах
myNum = Math.acos(0.5); // 1.0471975511965979
Гиперболический арккосинус числа
myNum = Math.acosh(2); // 1.3169578969248166
Арктангенс числа в радианах
myNum = Math.atan(2); // 1.1071487177940904
Арктангенс от частного аргументов
myNum = Math.atan2(8, 4); // 1.1071487177940904
Гиперболический арктангенс числа
myNum = Math.atanh(0.5); // 0.5493061443340548
Степень числа Эйлера. Степень передается, как аргумент.
myNum = Math.exp(1); // 2.718281828459045
Метод `Math.random()` и простой генератор случайных чисел
Генерирует дробные числа от 0 (включительно) до 1 (исключительно).
Случайное число от 0 до 0.999999999999999
let myNum = Math.random();
Случайное число от 0 до 9.999999999999999 (до 10 исключительно)
myNum = Math.random() * 10;
Чтобы случайным образом получать целые цифры, надо умножать на целый множитель (по желаемому максимуму плюс один) и затем округлять в меньшую сторону. В примере выражение генерирует случайное целое число от 0 до 9.
myNum = Math.floor(Math.random() * 10);
А чтобы исключить 0 из возможных результатов, надо к результату random
и floor
добавлять единицу.
В примере выражение генерирует случайное целое число от 1 до 10.
myNum = Math.floor(Math.random() * 10) + 1;
/**
* Функция для получения случайного числа
* в указанном диапазоне
* @param {Number} min
* @param {Number} max - Максимально возможное число
*/
function getRandomInteger(min, max) {
return Math.floor(
Math.random() * ((max - min) + 1),
) + min;
}
⛔️ Math.random() не выдает криптографически стойкие случайные числа. Поэтому непригоден ни для чего, требующего безопасности данных. Вместо него следует использовать Web Crypto API (API криптографии в вебе), метод RandomSource.getRandomValues()
.
Math.random
и длина массива
Так как
- индекс массива начинается с нуля,
- и индекс последнего элемента поэтому на единицу меньше длины массива
- а единица в результат возвращаемый
Math.random
не входит
для генерации случайного индекса массива достаточно конструкции
Math.floor(Math.random() * words.length);
const words = [
'24/7', 'multi-tier',
'30,000 foot', 'B-to-B', 'win-win',
];
const rand = Math.floor(
Math.random() * words.length
);
`NaN` / `isNaN()`, `isFinite()`
Если математическая операция не может быть совершена, то возвращается специальное значение NaN
(Not-A-Number). NaN
используется для обозначения математической ошибки.
Например, деление 0/0
в математическом смысле не определено, поэтому его результат NaN
. Тоже самое получится, если попробовать умножить строку на число.
alert(0 / 0); // NaN
alert('food' * 1000); // NaN
Значение NaN – единственное в своем роде, которое не равно ничему, включая себя. Следующий код ничего не выведет — оба вызова не сработают.
if (NaN == NaN) alert('==');
if (NaN === NaN) alert( '===' );
⛔️ Так как значение NaN
не равно ничему, даже самому себе, любые проверки равенства с NaN
исключаются…
if (myNum === NaN) { // так делать не надо
myNum = 0;
}
Вместо этого используют специальную функцию isNaN.
// true — 0 / 0 равно NaN
alert(isNaN(0 / 0));
// false — 12 это число
alert(isNaN('12'));
☝️🧐 Рекомендации Airbnb. Использовать Number.isNaN
вместо глобального isNaN
. eslint: no-restricted-globals
// ⛔️ плохо
isNaN('1.2'); // false
isNaN('1.2.3'); // true
// 👍 хорошо
Number.isNaN('1.2.3'); // false
Number.isNaN(Number('1.2.3')); // true
isFinite()
Глобальная, не связанная не с одним объектом функция isFinite()
, а также аналогичный метод объекта Number
, определяет: является ли переданное значение конечным числом. То есть числом, и не бесконечным, как, например 3.14…
☝️🧐 Рекомендации Airbnb. Использовать Number.isFinite
вместо глобального isFinite
. eslint: no-restricted-globals
// ⛔️ плохо
isFinite('2e3'); // true
isFinite(Infinity); // false
isFinite(0); // true
// true, но было бы false если использовать более
// надежную проверку Number.isFinite
isFinite('0');
// 👍 хорошо
Number.isFinite('2e3'); // false
Number.isFinite('0'); // false
Классы
Общие сведения
Класс — это шаблон для однотипных объектов, «экземпляров класса». Особый тип функции, для запуска которого вместо ключевого слова function
используется ключевое слово class
.
Содержит метод constructor
, в котором создаются заготовки свойств, которые затем наследуются экземплярами.
Также может другие методы — функции для взаимодействия с внешним кодом.
Наследуемые свойства и методы всегда можно переопределить в экземпляре. Кроме того, можно добавить уникальные.
В объектно-ориентированной программе каждый объект является экземпляром одного из классов.
А сама идея классов пришла из работ по базам знаний. Используемые человеком классификации в зоологии, ботанике, химии несут в себе основную идею, что любую вещь всегда можно представить частным случаем некоторого более общего понятия. Конкретное яблоко, the apple является и яблоком вообще, an apple. А «яблоко вообще» является фруктом.
Синтаксис
class ClassName {
constructor(param1, param2) {
this.property1 = param1;
this.property2 = param2;
}
method1() { ... }
method2() { ... }
method3() { ... }
}
С наследованием от другого класса.
class ClassName extends Parent {
constructor(param1, param2) {
super();
this.property1 = param1;
this.property2 = param2;
}
method1() { ... }
method2() { ... }
method3() { ... }
}
Пример
/**
* Имя класса начинается в прописной буквы.
* Кстати, Google рекомендует комментировать классы
* и их методы в формате
* {@link https://bit.ly/3VmBtGa JSDoc}.
*/
class User {
constructor(name) {
/* Ключевое слово this в конструкторе ссылается
на создаваемый новый объект.
Название параметров конструктора и свойств
класса не обязаны совпадать, но часто
совпадают — это удобное соглашение.
Конструктор ничего не возвращает */
this.name = name;
}
/**
* Класс состоит из методов. constructor — метод
* для заготовки свойств, sayHi() - метод для
* примера.
*/
sayHi() {
alert(this.name);
}
// Запятые между членами класса не нужны.
sayBye() {
alert(`Bye, ${this.name}`);
}
}
Класс — это «синтаксический сахар». То есть конструкция, облегчающая кодирование, но не создающая новых структур данных или понятий.
Класс создали, как удобную замену самодельным классам, которые программисты создавали с помощью свойства prototype
.
Экземпляр класса
Для создания экземпляра используется оператор new
, за которым следует имея класса. Значения свойств передаются в параметры. По-программистски можно назвать команду «вызовом конструктора».
// user — экземпляр класса User
const user = new User('Вася');
user.sayHi(); // Метод вернёт строку 'Вася'
// myCar — экземпляр класса Car
const myCar = new Car('Audi');
console.log(myCar.present()); // I have an Audi
Не стоит создавать класс ради одного экземпляра
Классы применяются, когда надо создать множество однотипных объектов. Но когда объект нужен один, предпочтительнее создавать объект (объектный литерал). Кроме того, объектные литералы используются для передачи конструкторам параметров — когда параметров больше трех, легко спутать их порядок.
const newCar = {
make: 'GM',
model: 'Cadillac',
year: 1955,
color: 'tan',
passengers: 5,
convertible: false,
mileage: 12892,
};
Строчные классы — классы определенные в Class Expression
Классы можно записать, как функциональное выражение, — присвоив переменной. Это называется Class Expression (выражение класса).
const User = class {
sayHi() {
alert(this.name);
}
};
Строчному классу можно дать имя. Тогда оно, как и в функциональном выражении, будет доступно только внутри класса.
Наиболее очевидная область применения этой возможности – создание вспомогательного класса прямо при вызове функции.
const SiteGuest = class User3 {
sayHi() { alert(this.name); }
};
new SiteGuest().sayHi(); // Привет
new User3(); // ошибка
Статические методы
Статические методы класса описывают внутреннюю логику класса. Статический метод можно вызвать на классе, но нельзя вызвать на экземпляре класса.
class Car {
constructor(name) {
this.name = name;
}
static hello() {
return 'Hello!!';
}
}
const myCar = new Car('Ford');
/* Мы можем вызвать статический метод на самом
классе */
document.getElementById('demo').innerHTML
= Car.hello();
/* ⚠️ Но не можем на экземпляре класса.
Это вызовет ошибку.
document.getElementById("demo").innerHTML
= myCar.hello();
*/
Можно посылать
Наследование
Для наследования свойств и методов от другого класса при создании нового используется ключевое слово extends
. В конструктор принимаются наследуемые свойства с помощью метода super()
.
class SuperUser extends User {
/**
* Новые свойства можно добавлять только после
* получения наследуемых с помощью `super()`.
*/
constructor(name, permissions) {
/* Здесь вызывается конструктор родительского класса
с атрибутом параметра `name`, получаемым экземпляром */
super(name);
this.permissions = permissions;
}
/**
* Переопределение родительского метода
*/
sayHi() {
/**
* Наследование родительского метода:
* выполнить все, что написано в родительском
* методе, а потом выполнять то, что написано
* ниже
*/
super.sayHi();
alert('Btw, I am super user');
}
}
См. также цепочку прототипов.
Наследование — методы `hasOwnProperty`, `isPrototypeOf`
Используются для проверки различных аспектов наследования. Во избежание ошибок, рекомендуется вызывать эти методы не на экземпляре, а на первом звене цепочки прототипов — Object.prototype
(или {}.prototype
).
// 👎 Так плохо
const bad = foo.hasOwnProperty('bar');
// 👍 Так хорошо
const good = {}.prototype
.hasOwnProperty.call(foo, 'bar');
hasOwnProperty
Метод возвращает логическое значение — есть ли у объекта собственное свойство, не из цепочки прототипов (см. первый пример).
isPrototypeOf()
Метод проверяет, входит ли объект в цепочку прототипов другого объекта.
// Object.prototype можно заменить литералом `{}`
const isPrototypeOfBar = {}.isPrototypeOf
.call(foo, bar);
propertyIsEnumerable()
Метод возвращает логическое значение — является ли указанное свойство перечисляемым?
const barIsEnumerable = {}.propertyIsEnumerable
.call(foo, 'bar');
Наследование — `instanceof`
Оператор позволяет установить, принадлежит ли объект к определённому классу.
// Класс
class Rabbit extends Animal { /* super etc */ }
// создаём объект
const rabbit = new Rabbit();
// проверяем этот объект создан Rabbit?
alert(rabbit instanceof Rabbit); // true, верно
Ещё примеры.
// Является ли объект cadi экземпляром класса Car?
if (cadi instanceof Car) {
console.log('Congrats, it\'s a Car!');
}
// Проверка нескольких объектов
if ((fido instanceof Dog) && (spot instanceof Dog)) {
// do smth
}
Добавление или удаление свойств и методов класса, `prototype`
class Dog {
constructor(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
}
goodBoyReaction() {
console.log(`${this.name} wags its tail`);
}
}
Чтобы добавить метод классу используется свойство prototype
, прототип объекта. Это набор всех наследуемых свойств и методов класса. В примере с помощью свойства prototype
классу Dog
был добавлен новый метод bark
.
Dog.prototype.bark = () => {
console.log('Woof! Woof!');
};
☝️🧐 Если класс наследует свойства и методы предка из библиотеки (например, jQuery), тогда prototype использовать необходимо — чтобы при каждом обновлении библиотеки не менять класс.
Аксессоры — «геттеры» и «сеттеры» — класса
Геттеры и сеттеры класса выполняют те же роли, что и в объектах вообще.
class Employee {
constructor(name) {
this.name = name;
}
doWork() {
return `${this.name} is working`;
}
get name() {
return this.name.toUpperCase();
}
set name(newName) {
if (newName) {
this.name = newName;
}
}
}
const employee1 = new Employee('John Doe');
Для доступа к свойству name
используется «геттер».
if (employee1.name) { /* ... */ }
Для смены значения свойства экземпляра используется «сеттер».
employee1.name = 'Jane Doe';
Приватные и публичные поля классов, инкапсуляция
Один из важнейших принципов объектно-ориентированного программирования – разделение свойств и методов на 2 группы.
Внутренний и внешний интерфейсы
В JavaScript есть два типа полей (свойств и методов) объекта:
- Приватные: доступны только внутри класса. Они относятся к внутреннему интерфейсу. В материальном мире им аналогична сложная начинка бытовых приборов, скрытая под защитным кожухом.
- Публичные: доступны отовсюду. Они составляют внешний интерфейс. В материальном мире им соответствуют кнопки включения и бегунки регулировки.
Во многих других языках также существуют защищённые поля. Они доступны внутри класса, как приватные. Но кроме того, доступны и в наследуемых классах.
В JS таких пока нет. Зато приватные поля официально добавлены в JavaScript в спецификации ECMAScript 2022 и поддерживаются 97% браузеров.
Приватные поля
Названия приватных полей — свойств и методов — начинаются со знака решетки. И отдают ошибку при попытки обращения к ним из-за пределов класса.
class Human {
#name = "John";
setName(name) {
this.name = name;
}
}
const human = new Human()
human.#name = 'Amy' // ОШИБКА!
human.setName('Amy') // ОК
- [Приватные поля класса](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Classes/Private_class_fields)
- [Working with private class features](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_With_Private_Class_Features)
- [8 new JavaScript features to start using today](https://www.infoworld.com/article/3665748/8-new-javascript-features-to-start-using-today.html)
#### Итог
В терминах ООП отделение внутреннего интерфейса от внешнего называется [инкапсуляция](https://ru.wikipedia.org/wiki/Инкапсуляция_(программирование)){:target="_blank"}.
Это даёт следующие выгоды:
- Защита для пользователей, чтобы они не выстрелили себе в ногу. Если пользователь класса изменит вещи, не предназначенные для изменения извне – последствия непредсказуемы.
- Поддерживаемость. Приватные методы можно безопасно переименовывать, их параметры можно изменять и даже удалять, потому что от них не зависит внешний код. В новой версии вы можете полностью всё переписать, но пользователю будет легко обновиться, если внешний интерфейс остался такой же.
- Сокрытие сложности. Люди обожают использовать простые вещи. По крайней мере, снаружи. Что внутри – это другое дело. Для сокрытия внутреннего интерфейса мы используем защищённые или приватные свойства:
- Защищённые поля имеют префикс `_`. Это хорошо известное соглашение, не поддерживаемое на уровне языка. Программисты должны обращаться к полю, начинающемуся с `_`, только из его класса и классов, унаследованных от него.
- Приватные поля имеют префикс `#`. JavaScript гарантирует, что мы можем получить доступ к таким полям только внутри класса.
</div>
</details>
<details class="accordion" id="topic-classes-new-instance">
<summary class="accordion__header">
Создание нового объекта, экземпляра класса. Доступ к его свойствам и методам
</summary>
<div class="accordion__body" markdown="1">```javascript
const objectName = new ClassName(arg1, arg2, etc);
Пример.
class Person {
/**
* @param {string} firstName
* @param {string} lastName
* @param {number} age
*/
constructor(firstName, lastName, age) {
this.name = firstName;
this.lastName = lastName;
this.age = age;
}
}
// Создаем новые объекты класса Person
const john = new Person('John', 'Smith', 30);
const susan = new Person('Susan', 'Jordan', 25);
Создание массива объектов с использованием класса
const family = [
new Person('Alice', '', 40),
new Person('Bob', '', 42),
new Person('Michelle', '', 8),
new Person('Timmy', '', 6),
];
☝️🧐 При использовании new
с классом создается новый̆ пустой̆ объект. Свойства объекта инициализируются только передачей аргументов.
Состояние и stateful-компоненты
Обновлять интерфейс страницы можно, выбирая элемент через querySelector
и пр., добавляя innerHTML
, меняя классы через classList
или строковые стили в свойстве style
.
Современные фреймворки — React, Vue etc — предлагают управление интерфейсом через состояния. Без выбора элементов по CSS-селекторам и дальнейших манипуляций. Обновляешь состояние и выводишь на экран свежую копию интерфейса на основе новых данных.
Состояние — это всего лишь данные (обычно — простой объект) в определенный момент времени. Значения состояний (states
) устанавливаются внутри компонента — по аналогии с переменными, которые объявлены внутри функции.
Для того, чтобы сохранять состояние UI элементов (один выбран, другой помечен, как важный и т.д.) используются stateful-компоненты (другие названия: компоненты с состояниями, классовые компоненты). Они известны, прежде всего, по фреймворкам, но их можно написать и на «ванильном» JS.
Простой пример с состоянием, сохраненном в глобальной переменной.
// Вот это состояние
const data = {
greeting: 'Hello',
name: 'there',
};
// Простой компонент
const greeting = () => {
return `<p>${data.greeting}, ${data.name}!</p>`;
};
// Выводим на экран
const app = document.querySelector('#app');
app.innerHTML = greeting();
Улучшенный вариант. Состояние (данные) стало свойством компонента и доступно только изнутри.
// Компонент
const greeting = () => {
return `
<p>${greeting.data.greeting},
${greeting.data.name}!</p>
`;
};
// Состояние
greeting.data = {
greeting: 'Hello',
name: 'there',
};
// Вывод на экран
const app = document.querySelector('#app');
app.innerHTML = greeting();
См. также примеры:
Планирование компонента — вопросы, которые надо себе задавать перед тем, как начать писать код
- Как мы собираемся в странице выбирать один компонент из множества и не загрязнить глобальное пространство имен?
- Как мы собираемся рендерить HTML для компонента?
- Как мы будем сохранять и контролировать внутреннее состояние компонента?
- Как мы будем удалять компонент и обработчики событий, когда они перестанут быть нужными?
Прокси
Для отслеживания изменений во фреймворках используются прокси — объект, который «оборачивается» вокруг другого объекта и может перехватывать (и, при желании, самостоятельно обрабатывать) разные действия с ним — в частности, чтение/запись свойств.
Эту же технологию использует npm-пакет on-change, который можно использовать в ванильных stateful-компонентах.
import onChange from 'on-change';
class App {
constructor() {
// Состояние
const state = {
currentTodoItemID: null,
};
// Слушатель с помощью пакета 'on-change'
this.state = onChange(state, this.update);
}
/* Изменение компонента в ответ на изменения
состояния */
static update(path, current, previous) {
console.log(
`${path} changed
from ${previous} to ${current}`
);
}
}
const app = new App();
app.state.currentTodoItemID = 1;
Можно обойтись и без дополнительных npm-пакетов.
class Component {
constructor() {
// Состояние
const state = {};
}
setState(state) {
this.state = new Proxy(state, this.stateHandler);
}
stateHandler(target, prop, value) {
// updates the object state
target[prop] = value;
// triggers the rendering
this.render();
}
}
Ещё о прокси(https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Proxy){:target=”_blank”}
Предки классов — функции-конструкторы
Раньше в качестве шаблонов пользовательских объектов использовались функции, которые назывались «конструкторами».
// Конструктор объекта
function MyConstructor(arg1, arg2) {
this.firstName = arg1;
this.lastName = arg2;
}
// Создаем новый экземпляр объекта
const x = new MyConstructor('John', 'Doe');
/* Внутри console.log — вызов функции, как метода
экземпляра объекта, созданного с помощью
конструктора. */
console.log(x.firstName);
В современных реализациях языка шаблонами пользовательских объектов служат классы. И в них используется метод constructor
— определяющий свойства объекта. Методы определяются за скобками constructor'а
.
Советы и ссылки
14 советов по оптимизации
1. Удалить неиспользуемый код и функции
Чем больше кода содержится в приложении, тем больше данных необходимо передать клиенту, а браузеру потребуется больше времени для анализа и интерпретации кода.
Не надо включать в сборку неиспользуемые функции — пусть они хранятся в папках исходниках.
Неиспользуемый код можно удалить вручную, a также при сборке с UglifyJS или Google Closure Compiler.
При сборке webpack’ом применяют технику «встряхивания дерева».
Неиспользуемые пакеты npm, можно удалить командой npm prune
.
2. Кешировать
Использовать Cache API или HTTP-кэширования.
3. Избегайте утечек памяти
Будучи языком высокого уровня, JS заботится о нескольких низкоуровневых системах управления, таких как управление памятью. Сборка мусора — это процесс, общий для большинства языков программирования. Сборка мусора с точки зрения непрофессионала — это просто сбор и освобождение памяти, которая была выделена объектам, но которая в настоящее время не используется. В таких языках программирования, как C, разработчик должен позаботиться о распределении и освобождении памяти, используя функции malloc()
и dealloc()
.
Даже если сборка мусора выполняется в JavaScript автоматически, могут быть определенные случаи, когда она не будет идеальной. В JavaScript ES6 Map
и Set
были представлены со своими «более слабыми» братьями и сестрами. Этот «более слабый» аналог, известный как WeakMap и WeakSet, содержит «слабые» ссылки на объекты. Они позволяют собирать ненужные значения и предотвращать утечки памяти.
4. Попытайтесь выходить из циклов раньше
Обработка больших циклов может занять много времени. Поэтому следует выходить из них как можно раньше — с помощью ключевого слова break
и ключевого слова continue
.
В приведенном ниже примере, если вы не вышли из цикла, ваш код будет запускать цикл 1000000000 раз, что явно слишком.
let arr = new Array(1000000000).fill('----');
arr[970] = 'found';
for (let i = 0; i < arr.length; i++) {
if (arr[i] === 'found') {
console.log("Found");
break;
}
}
В приведенном ниже примере, если вы не сделали continue, когда цикл не соответствует условию, вы все равно будете запускать функцию 1000000000 раз. Мы обрабатываем элемент массива только в том случае, если он находится в четном положении. Это уменьшает выполнение цикла почти вдвое.
let arr = new Array(1000000000).fill('----');
arr[970] = 'found';
for (let i = 0; i < arr.length; i++) {
if(i%2!=0){
continue;
};
process(arr[i]);
}
5. Минимизируйте количество вычислений переменных
Чтобы уменьшить количество вычислений переменной, вы можете использовать замыкания. С точки зрения непрофессионала, замыкания в JavaScript предоставляют доступ к области видимости внешней функции из внутренней функции. Замыкания создаются каждый раз, когда вызывается функция created-not. Внутренние функции будут иметь доступ к переменным внешней области видимости, даже после возврата внешней функции.
Давайте рассмотрим два примера, чтобы увидеть это в действии. Эти примеры взяты из блога Брета.
function findCustomerCity(name) {
const texasCustomers = ['John', 'Ludwig', 'Kate'];
const californiaCustomers = ['Wade', 'Lucie','Kylie'];
return texasCustomers.includes(name) ? 'Texas' :
californiaCustomers.includes(name) ? 'California' : 'Unknown';
};
Если мы вызываем вышеупомянутые функции несколько раз, каждый раз создается новый объект. Для каждого вызова память излишне перераспределяется на переменные texasCustometrs и californiaCustomers. Используя решение с замыканиями, мы можем создать экземпляр переменных только один раз. Давайте рассмотрим приведенный ниже пример.
function findCustomerCity() {
const texasCustomers = ['John', 'Ludwig', 'Kate'];
const californiaCustomers = ['Wade', 'Lucie','Kylie'];
return name => texasCustomers.includes(name) ? 'Texas' :
californiaCustomers.includes(name) ? 'California' : 'Unknown';
};
let cityOfCustomer = findCustomerCity();
cityOfCustomer('John');//Texas
cityOfCustomer('Wade');//California
cityOfCustomer('Max');//Unknown
В приведенном выше примере с помощью замыканий внутренняя функция, которая возвращается в переменную cityOfCustomer, имеет доступ к константам внешней функции findCustomerCity(). И всякий раз, когда вызывается внутренняя функция с именем, переданным в качестве параметра, ей не нужно снова создавать экземпляры констант.
6. Минимизируйте доступ к DOM
Доступ к DOM медленный по сравнению с другими операторами JavaScript. Если вы внесете изменения в DOM, которые приведут к перерисовке макета, это может привести к замедлению работы.
Чтобы уменьшить количество раз, когда вы обращаетесь к элементу DOM, обращайтесь к нему один раз и используйте его как локальную переменную. Когда необходимость отпадет, обязательно удалите значение переменной, установив для него null. Это предотвратит утечку памяти, поскольку позволит работать процессу сбора мусора.
7. Сожмите файлы
Используя такие методы сжатия, как Gzip, вы можете уменьшить размер файлов JavaScript. Эти файлы поменьше приведут к увеличению производительности сайта, так как браузер должен будет загружать меньшие ресурсы. Эти сжатия могут уменьшить размер файла до 80%. Подробнее о сжатии читайте здесь.
8. Оптимизируйте собранный код
Некоторые люди считают, что минимизация и сжатие – это одно и то же. Вовсе нет – это разные вещи. В сжатии используются специальные алгоритмы для изменения выходного размера файла. При минимизации в JS удаляются комментарии и лишние пробелы, переменные и функции переименовываются: длинные имена заменяются однобуквенными.
9. Используйте throttling
и debouncing
Используя эти два метода, мы можем строго следить за тем, сколько раз событие должно обрабатываться кодом.
При throttling
(встречается перевод «тормозящий») указывается максимальное количество раз, когда функция может быть вызвана. Например, «выполнять функцию события onkeyup
не чаще, чем раз в 1000 миллисекунд». Это будет означать, что если вы нажмете клавиши 20 раз в секунду, событие будет срабатывать только один раз в секунду. Это уменьшит нагрузку на код.
С другой стороны, debouncing
— это то, где вы указываете минимальную продолжительность времени для повторного запуска функции с момента предыдущего выполнения той же функции. Другими словами, «выполняйте эту функцию, только если прошло 600 миллисекунд без ее вызова».
Можно написать собственные функции debounce
и throttle
— первая проще, вторая универсальная. А можно импортировать готовые функции из библиотеки Lodash. Импортировать можно при сборке Webpack’ом, который включает loadash.
10. Избегайте использования ключевого слова delete
Ключевое слово delete используется, чтобы удалить свойство из объекта. В качестве альтернативы, вы можете просто установить нежелательное свойство как undefined
.
const object = {name:"Jane Doe", age:43};
object.age = undefined;
Вы также можете использовать объект Map, так как его метод delete
работает быстрее.
11. Используйте асинхронный код для предотвращения блокировки потоков
Вы должны знать, что JavaScript является синхронным по умолчанию, а также однопоточным. Но могут быть случаи, когда коду требуется много времени для вычислений. Будучи синхронным по своей природе, для JavaScript это будет означать, что этот фрагмент кода будет блокировать выполнение других операторов кода до тех пор, пока он не будет выполнен. Это снизит производительность в целом.
Но мы можем предотвратить эту ситуацию, внедрив асинхронный код. Асинхронный код был ранее написан в форме обратных вызовов, но с ES6 был введен новый стиль обработки асинхронного кода. Этот новый стиль был назван промисами.
12. Используйте разделение кода
Если у вас есть опыт работы с Google Lighthouse, вы знакомы с метрикой, которая называется «первое значимое отображение». Это один из шести показателей, отслеживаемых в разделе «Производительность» отчета Light House. First Contentful Paint (FCP) измеряет, сколько времени браузеру требуется для отображения первого фрагмента содержимого DOM после перехода пользователя на вашу страницу. Изображения, небелые
Один из лучших способов получить более высокий балл FCP — использовать разделение кода. Разделение кода — это метод, при котором вы сначала отправляете пользователю только необходимые модули. Это сильно повлияет на оценку FCP, уменьшая размер полезной нагрузки, передаваемой изначально.
Популярные пакеты модулей, такие как webpack, предоставляют функционал разделения кода. Вы также можете использовать собственные модули ES, чтобы загрузить отдельные модули.
13. Используйте async
и defer
14. Используйте Web Worker для выполнения интенсивных задач процессора в фоновом режиме
Web Worker позволяют запускать скрипты в фоновых потоках. Если у вас есть очень интенсивные задачи, вы можете назначить их для Web Worker, которые будут выполнять их без вмешательства в пользовательский интерфейс. После создания Web Worker может общаться с кодом JavaScript, отправляя сообщения в обработчик событий, указанный этим кодом. Это может происходить и наоборот.
Полезные ссылки
- Веб-компоненты
- Символ — уникальный и неизменяемый примитивный тип данных, который может быть использован как ключ для свойств объектов. См. в W3Schools и «современном учебнике».
- Поверхностное и глубокое копирование
Array.prototype.slice
\\[\].slice.call()
— прием для применения методов массива кNodeList
- Интерфейсы веб API:
- WebAssembly - это технология, предоставляющая новый тип кода, который можно запускать в современных веб-браузерах. Код для WebAssembly не пишется вручную, а компилируется из низкоуровневых исходных языков C, C++, Rust и.т.д.
- Декораторы и переадресация вызова, call/apply
Справочники
- What is the difference
- Favorite JavaScript Utilities
- CanIuse DevTools - раздел JS; надо клинкуть на чекбокс, чтобы открыть инструкции.
См. также шпаргалку и практическое руководство.