Вопросы и ответы с собеседований по JavaScript
Вопросы и ответы по JavaScript для собеседования фронтенд-разработчика. Что такое ECMAScript, как работает цикл событий (Event Loop), область видимости, замыкание, какое значение у this, что такое Hoisting, что скрывает Shadow DOM, как устроены прототипы и многое другое.
Что такое ECMAScript?
ECMAScript — это спецификация, стандарт скриптовых языков программирования, он является основой JS, поэтому любые изменения ECMAScript отражаются на JS. ECMAScript сам по себе - это не язык программирования. А вот JavaScript - это язык программирования, реализующий стандарт ECMAScript. Чем отличаются JavaScript и ECMAScript? ECMAScript 2015, 2016, 2017, 2018, 2019, 2020, 2021
Какие различия между ES5 и ES6?
ECMAScript 5 (ES5) — 5-е издание ECMAScript, выпущенное в 2009 году. Поддерживается современными браузерами практически полностью.
ECMAScript 6 (ECMAScript 2016, ES6) — 6-е издание ECMAScript, выпущенное в 2015 году. Частично поддерживается большинством современных браузеров.
Несколько ключевых отличий двух стандартов:
1. Стрелочные функции и интерполяция в строках
// В новом стандарте можно сделать так:
const greetings = (name) => {
return `hello ${name}`;
}
// и даже так:
const greetings = name => `hello ${name}`
2. Ключевое слово const
Константы в JavaScript отличаются от констант в других языках программирования. Они сохраняют неизменной только ссылку на значение. Таким образом, вы можете добавлять, удалять и изменять свойства объявленного константным объекта, но не можете перезаписать текущую переменную, в которой лежит этот объект.
const NAMES = [];
NAMES.push("Jim");
console.log(NAMES.length === 1); // true
NAMES = ["Steve", "John"]; // error
3. Блочная видимость
Переменные, объявленные с помощью новых ключевых слов let
и const
имеют блочную область видимости, то есть недоступны за пределами {}
-блоков. Кроме того, они не поднимаются, как var
-переменные.
4. Параметры по умолчанию
Теперь функцию можно инициализировать с дефолтным значением параметров. Оно будет использовано, если параметр не будет передан при вызове.
function multiply (a, b = 2) {
return a * b;
}
multiply(5); // 10
5. Классы и наследование
Новый стандарт ввел в язык поддержку привычного синтаксиса классов (class
), конструкторы (constructor
) и ключевое слово extend
для оформления наследования.
6. Оператор for-of для перебора итерируемых объектов в цикле
7. spread и rest операторы
const obj1 = { a: 1, b: 2 }
const obj2 = { a: 2, c: 3, d: 4}
const obj3 = {...obj1, ...obj2}
8. Обещания (Promises)
Механизм для обработки результатов и ошибок асинхронных операций. По сути, это то же самое, что и коллбэки, но гораздо удобнее. Например, промисы можно чейнить (объединять в цепочки).
const isGreater = (a, b) => {
return new Promise ((resolve, reject) => {
if(a > b) {
resolve(true)
} else {
reject(false)
}
})
}
isGreater(1, 2)
.then(result => {
console.log('greater')
})
.catch(result => {
console.log('smaller')
})
9. Модули
Способ разбития кода на отдельные модули, которые можно импортировать при необходимости.
import myModule from './myModule';
Синтаксис экспорта позволяет выделить функциональность модуля:
const myModule = {
x: 1,
y: () => {
console.log('This is ES5')
}
}
export default myModule;
Что такое операторы spread и rest?
Spread оператор (оператор расширения) "берет" каждый отдельный элемент итерируемого объекта (массив) и "распаковывает" его в другой итерируемый объект (массив).
К итерируемым объектам можно отнести все, что можно перебрать с помощью цикла for..of
. Большая часть задач, где приходится использовать оператор spread, касается массивов и строк.
Так будет выглядеть создание одного общего массива с помощью оператора spread:
const tvSeriesOne = ['Ozark', 'Fargo', 'Dexter'];
const tvSeriesTwo = ['Mr. Robot', 'Barry', 'Suits'];
const tvSeries = [...tvSeriesOne, ...tvSeriesTwo];
console.log(tvSeries);
// [ "Ozark", "Fargo", "Dexter", "Mr. Robot", "Barry", "Suits" ]
Оператор rest …
(три точки) похож на оператор spread, но выполняет противоположную функцию.
Spread забирает каждый элемент из массива и распаковывает в новый массив. Оператор rest забирает каждый элемент из массива и создает из них новый массив.
Есть 2 типа задач, где оператор rest используется чаще всего – в функциях и в процессе деструктуризации.
function buyPizza(price, ...rest) {
console.log(price); // 500
console.log(rest);
// Array(4) [ 3, 10, 6, 20 ]
}
buyPizza(500, 3, 10, 6, 20);
const pizza = ['Pepperoni', 2222, 5, 6, 10, 30, 1];
const [name, id, ...rest] = pizza;
console.log(name); // Pepperoni
console.log(id); // 2222
console.log(rest);
// [ 5, 6, 10, 30, 1 ]
7 трюков с Rest и Spread операторами при работе c JS объектами
Что такое деструктуризация?
Деструктуризация в JavaScript позволяет извлечь данные из массива или свойства объекта и присвоить их отдельным переменным. Деструктуризация удобна тем, что позволяет не писать лишний код для доступа к данным внутри объектов/массивов по индексам или ключам.
Деструктуризация массива:
let array = [1, 2, 3];
let [x, y, z] = array;
console.log(x); // 1
console.log(y); // 2
console.log(z); // 3
Деструктуризация объекта:
let user = {
name: "Василий",
lastname: "Пупкин",
age: 30
};
let {name, lastname, age} = user;
console.log(name); // "Василий"
console.log(lastname); // "Пупкин"
console.log(age); // 30
Вы не знаете деструктуризацию, пока
Перечислите типы данных JavaScript
- undefined
- null
- boolean
- number
- string
- bigint
- object
- symbol
(ES6)
«Сложно о простом». Типы данных JS. В поисках истины примитивов и объектов
Глубокий JS. В память о типах и данных
Что такое Map?
Map
– это коллекция ключ/значение наподобие Object
, но с более удобным и декларативным API и некоторыми функциональными особенностями. Например, Map
позволяет использовать ключи любого типа.
Методы и свойства:
new Map()
– создаёт коллекцию.
Map.prototype.set(key, value)
– записывает по ключу key
значение value
.
Map.prototype.get(key)
– возвращает значение по ключу или undefined
, если ключ key
отсутствует.
Map.prototype.has(key)
– возвращает true, если ключ key
присутствует в коллекции, иначе false
.
Map.prototype.delete(key)
– удаляет элемент (пару «ключ/значение») по ключу key
.
Map.prototype.clear()
– очищает коллекцию от всех элементов.
Map.prototype.size
– возвращает текущее количество элементов.
let map = new Map();
map.set("1", "str1"); // строка в качестве ключа
map.set(1, "num1"); // цифра как ключ
map.set(true, "bool1"); // булево значение как ключ
// помните, обычный объект Object приводит ключи к строкам?
// Map сохраняет тип ключей, так что в этом случае сохранится 2 разных значения:
alert(map.get(1)); // "num1"
alert(map.get("1")); // "str1"
alert(map.size); // 3
Для перебора коллекции Map
есть 3 метода:
Map.prototype.keys()
– возвращает итерируемый объект по ключам,
Map.prototype.values()
– возвращает итерируемый объект по значениям,
Map.prototype.entries()
– возвращает итерируемый объект по парам вида [ключ, значение], этот вариант используется по умолчанию в for..of
.
В отличие от обычных объектов, в Map
перебор происходит в том же порядке, в каком происходило добавление элементов.
Что такое Set?
Set
– это особый вид коллекции: «множество» значений (без ключей), где каждое значение может появляться только один раз.
Его основные методы это:
new Set(iterable)
– создаёт Set
, и если в качестве аргумента был предоставлен итерируемый объект (обычно это массив), то копирует его значения в новый Set
.
Set.prototype.add(value)
– добавляет значение (если оно уже есть, то ничего не делает), возвращает тот же объект Set
.
Set.prototype.delete(value)
– удаляет значение, возвращает true
, если value
было в множестве на момент вызова, иначе false
.
Set.prototype.has(value)
– возвращает true
, если значение присутствует в множестве, иначе false
.
Set.prototype.clear()
– удаляет все имеющиеся значения.
Set.prototype.size
– возвращает количество элементов в множестве.
Основная «изюминка» – это то, что при повторных вызовах set.add()
с одним и тем же значением, оно добавляется в коллекцию лишь единожды.
Set
имеет те же встроенные методы для перебора коллекции, что и Map
:
Set.prototype.keys()
– возвращает перебираемый объект для значений,
Set.prototype.values()
– то же самое, что и set.keys()
, присутствует для обратной совместимости с Map
,
Set.prototype.entries()
– возвращает перебираемый объект для пар вида [значение, значение], присутствует для обратной совместимости с Map
.
Ускоряем JavaScript-код с использованием типа данных Set
Что такое цикл событий?
Цикл событий - это механизм, на каждом тике выполняющий функции из стека вызовов и, если он оказывается пустым, перемещающий задачи из очереди задач в стек вызовов для выполнения. Лежит в основе функционирования любого JavaScript-движка. Стек вызовов - это структура данных (первым вошел, последним вышел), используемая для отслеживания порядка выполнения функций в текущем контексте (области видимости). Очередь задач - это структура данных (первым вошел, первым вышел), используемая для отслеживания выполнения асинхронных функций, готовых оказаться в стеке вызовов. Что ты такое, Event Loop? Или как устроен цикл событий в браузере Chrome Как работает JS: цикл событий, асинхронность и пять способов улучшения кода с помощью async / await
Что такое область видимости?
Область видимости — это область, ограничивающая доступ к переменным и функциям внутри себя, как бы инкапсулирующая их. JS имеет три типа областей видимости: глобальная, функциональная и блочная (ES6).
Глобальная — переменные и функции, объявленные в глобальном пространстве имен, имеют глобальную область видимости и доступны из любого места в коде.
Функциональная — переменные, функции и параметры, объявленные внутри функции, доступны только внутри этой функции.
Блочная — переменные, объявленные с помощью ключевых слов let
и const
, доступны только внутри блока {}
, в котором были объявлены.
Область видимости — это также набор правил, по которым осуществляется поиск переменной. Если переменной не существует в текущей области видимости, ее поиск производится выше, во внешней по отношению к текущей области видимости. Если и во внешней области видимости переменная отсутствует, ее поиск продолжается вплоть до глобальной области видимости. Если в глобальной области видимости переменная обнаружена, поиск прекращается, если нет — выбрасывается исключение. Поиск осуществляется по ближайшим к текущей областям видимости и останавливается с нахождением переменной. Это называется цепочкой областей видимости (Scope Chain).
Область видимости в JavaScript и «поднятие» переменных и объявлений функций
Области видимости в JavaScript
Какое значение у "this"?
this
ссылается на значение объекта, который в данный момент выполняет функцию.
В конструкторе класса, например, this
будет указывать на создаваемый экземпляр.
На самом верхнем уровне исполнения this
будет указывать на глобальный объект (window
в браузере).
Function.prototype.call
и Function.prototype.apply
принимают в качестве первого аргумента объект, который будет являться значением this
внутри функции.
Function.prototype.bind
позволяет создать новую функцию с подмененным контекстом (this
).
Стрелочные функции не имеют собственного значения this
. Они копируют значение this
из внешнего лексического окружения.
Ключевое слово this в javascript — учимся определять контекст на практике
Ключевое слово this в JavaScript. Полное* руководство
Что такое замыкание?
function makeCounter(initial = 0) {
let counter = initial;
return function() {
return counter += 1;
}
}
const counterA = makeCounter(0);
console.log(counterA()); // 1
console.log(counterA()); // 2
const counterB = makeCounter(100);
console.log(counterB()); // 101
1. Замыкание — это способность функции во время создания запоминать ссылки на переменные и параметры, находящиеся в текущей области видимости. Обычно область видимости определяется при создании функции.
2. Замыкание - это комбинация функции и лексического окружения, в котором она была определена. Это позволяет ей обращаться к переменным и функциям этого лексического окружения в дальнейшем.
В примере кода выше мы определяем функцию makeCounter()
, которая фактически является фабричной функцией. При вызове она возвращает дочернюю функцию, которая имеет доступ к лексическому окружению внешней функции, то-есть функции makeCounter()
. Таким образом, возвращаемая функция запоминает переменную counter и в последующем изменяет ее значение.
Что ты такое, замыкания в JavaScript?
Замыкания в JavaScript для начинающих
Что такое DOM?
DOM (Document Object Model) — это специальная древовидная структура, которая позволяет управлять HTML-разметкой из JavaScript-кода. Управление обычно состоит из добавления и удаления элементов, изменения их стилей и содержимого.
Браузер создаёт DOM при загрузке страницы, складывает его в переменную document и сообщает, что DOM создан, с помощью события DOMContentLoaded
.
Выразительный JavaScript: Document Object Model (объектная модель документа)
Что такое Shadow DOM?
Shadow DOM (от англ. "shadow" - тень и "DOM" - Document Object Model) - это технология, которая позволяет создавать изолированные деревья DOM внутри основного дерева DOM веб-страницы.
Shadow DOM используется для инкапсуляции. Благодаря нему в компоненте есть собственное «теневое» DOM-дерево, к которому нельзя просто так обратиться из главного документа, у него могут быть изолированные CSS-правила и т.д.
Средствами теневого DOM устроены и стилизованы сложные браузерные элементы управления, например, <input type="range">
Браузер рисует их своими силами и по своему усмотрению. Их DOM-структура обычно нам не видна, но в инструментах разработчика можно её посмотреть. К примеру, в Chrome для этого нужно активировать пункт «Show user agent shadow DOM».
Мы не можем получить доступ к теневому DOM встроенных элементов с помощью обычных JavaScript-вызовов или с помощью селекторов. Это не просто обычные потомки, это мощное средство инкапсуляции.
Как работает JS: технология Shadow DOM и веб-компоненты
Про Shadow DOM
Что такое Event Propagation?
Когда какое-либо событие происходит в элементе DOM, оно на самом деле происходит не только в нем. Событие «распространяется» от объекта window
до вызвавшего его элемента (event.target
). При этом событие последовательно пронизывает всех предков целевого элемента.
Этот процесс называется Event Propagation (распространение события).
Event Propagation имеет три стадии:
1. Фаза погружения — событие возникает в объекте window
и опускается до цели события через всех ее предков.
2. Целевая фаза — это когда событие достигает целевого элемента.
3. Фаза всплытия — событие поднимается от event.target
, последовательно проходит через всех его предков и достигает объекта window
.
Выразительный JavaScript: Обработка событий
Что такое всплытие события?
Всплытие - это фаза события, когда оно переходит от элемента-инициатора (event.target
) к объекту window
, проходя по пути через все элементы, лежащие между ними.
Что такое погружение события?
Погружение - это фаза, в которой событие опускается от объекта window
до цели события через всех его предков.
Зачем нужны e.preventDefault() и e.stopPropagation()?
Метод event.preventDefault()
отключает поведение элемента по умолчанию. Если использовать этот метод в элементе form, то он предотвратит отправку формы (submit). Если использовать его в contextmenu, то контекстное меню будет отключено (данный метод часто используется в keydown
для переопределения клавиатуры, например, при создании музыкального/видео плеера или текстового редактора — прим. пер.).
Метод event.stopPropagation()
отключает распространение события (его всплытие или погружение).
Что такое делегирование событий?
Делегирование событий — подход при работе с событиями DOM-дерева, при котором обработчики событий добавляются не на каждый конкретный элемент, а только на общий родительский, в то время как необходимость вызова это обработчика для конкретного интересующего нас элемента определяется через инициатора события, узнать который можно из свойства объекта события event.target
.
Такой подход возможен благодаря особенностям событийной модели DOM-дерева, а конкретно такой особенности, как всплытие событий.
Простое объяснение делегирования событий в JavaScript
Что такое строгий режим ("use strict")?
«use strict»
— это директива ES5, которая заставляет весь наш код или код отдельной функции выполняться в строгом режиме. Строгий режим вводит некоторые ограничения по написанию кода, тем самым позволяя избегать ошибок на ранних этапах.
Вот какие ограничения накладывает строгий режим:
1. Нельзя присваивать значения или обращаться к необъявленным переменным
2. Запрещено присваивать значения глобальный переменным, доступным только для чтения или записи
3. Нельзя удалить «неудаляемое» свойство объекта
4. Запрещено дублирование параметров функции
5. Нельзя создавать функции с помощью функции eval
6. Значением this
по умолчанию является undefined
Зачем в JavaScript нужен строгий режим?
JavaScript Strict Mode
Различия между «var», «let» и «const»
var
- поднимается в начало области видимости функции при компиляции (имеет область видимости функции)
- объявляет переменную, которая может быть перезаписана
- объявляет переменную, которая может быть переопределена
let
- не поднимается в начало области видимости блока при компиляции (имеет блочную область видимости)
- объявляет переменную, которая может быть перезаписана
- объявляет переменную, которая не может быть переопределена
const
- не поднимается в начало области видимости блока при компиляции (имеет блочную область видимости)
- объявляет переменную, которая не может быть перезаписана или переопределена
Что такое приведение типов?
Конвертация встроенных типов данных друг в друга в JavaScript называется приведением типов (coercion). Оно бывает явное (explicit) и неявное (implicit).
Пример явного приведения:
let a = "42"; // строка
let b = Number(a);
a; // "42" - строка
b; // 42 - число
Пример неявного приведения:
let a = "42"; // строка
let b = a * 1; // строка "42" неявно приводится к числу
a; // "42" - строка
b; // 42 - число
Особенно важно понимать, как происходит приведение к логическому типу, так как оно часто происходит в условиях.
Следующие значения приводятся к false
:
- ""
(пустая строка)
- 0, -0, NaN
- null
, undefined
- false
А эти — к true
:
- «hello»
(любая непустая строка, даже " "
— строка с пробелом)
- 42 (число, отличное от нуля)
- true
- [ ]
, [ 1, "2"]
(любой массив, даже пустой)
- { }
, { a: 42 }
(любой объект, даже пустой)
- function foo() { }
(любая функция)
[1] + [2] — [3] === 9!? Исследование внутренних механизмов приведения типов в JavaScript
Неявное преобразование типов в JavaScript. Сколько будет !+[]+[]+![]?
В чем разница между "==" и "==="?
Разница между оператором нестрогого равенства ==
и оператором строгого равенства ===
состоит в том, что первый сравнивает значения, осуществляя их приведение к одному типу (Coersion), а второй — без такого преобразования.
console.log(10 == "10") // true
console.log(10 === "10") // false
Оператор строгого равенства ===
выдаст true
только в том случае, если сравниваемые сущности относятся к одному и тому же типу данных и равны по значениям.
Почему "typeof null" возвращает "object"?
typeof null == 'object'
всегда будет возвращать true
по историческим причинам.
От сообщества поступало предложение исправить эту ошибку, изменив typeof null = 'object'
на typeof null = 'null'
, но оно было отклонено в интересах сохранения обратной совместимости (такое изменение повлекло бы за собой большое количество ошибок).
Для проверки, является ли значение null
можно использовать оператор строгого равенства ===
:
function isNull(value){
return value === null
}
В чем разница между "null" и "undefined"?
undefined
представляет собой значение по умолчанию:
- переменной, которой не было присвоено значение;
- функции, которая ничего не возвращает явно, например, console.log(1);
- несуществующего свойства объекта.
null
— это значение, которое присваивается переменной явно.
При сравнении null
и undefined
мы получаем true
, когда используем оператор ==
, и false
при использовании оператора ===
.
Зачем нужен тип данных Symbol?
Символ (Symbol) — примитивный тип, значения которого создаются с помощью вызова функции Symbol
. Каждый созданный символ уникален.
Символы могут использоваться в качестве имён свойств в объектах. Символьные свойства могут быть прочитаны только при прямом обращении и не видны при обычных операциях.
Для создания символа нужно вызвать функцию Symbol
:
const sym = Symbol()
const symTwo = Symbol()
console.log(sym === symTwo)
// false
Символы используются для создания скрытых свойств объектов. В отличие от свойств, ключом которых является строка, символьные свойства может читать только владелец символа. Скрытые свойства не видны при его обходе с помощью for...in
.
Это может пригодиться, когда необходимо добавить свойства объекту, который могут модифицировать другие части программы. Таким образом только вы сможете читать созданное свойство, а гарантия уникальности символов гарантирует и отсутствие конфликтов имён.
Символы активно используются внутри самого JavaScript, чтобы определять поведение объектов. Такие символы называются «хорошо известными» (well-known symbols).
Самый известный символ Symbol.iterator
, который позволяет реализовать обход конструкции с помощью синтаксических конструкций for..of
и спред-синтаксиса.
Полный список таких символов доступен в спецификации, но на практике он нужен нечасто.
Особенности использования типа данных Symbol в JavaScript
Детальный обзор Well-known Symbols
Можно ли сравнить объекты оператором сравнения?
let a = {
a: 1
}
let b = {
a: 1
}
let c = a
console.log(a === b) // false
console.log(a === c) // true
В JS объекты и примитивы сравниваются по-разному. Примитивы сравниваются по значению. Объекты — по ссылке или адресу в памяти, где хранится переменная.
Вот почему первый console.log
возвращает false
, а второй — true
. Переменные a
и c
ссылаются на один объект, а переменные a
и b
— на разные объекты с одинаковыми свойствами и значениями.
Как итог: сравнивая две переменных-объекта с помощью оператора сравнения результат будет равен true
только в том случае, если обе переменных ссылаются на один и тот же объект.
Как выполняется клонирование объекта?
Если объект не содержит вложенных объектов, как в приведенном ниже примере, для клонирования можно использовать оператор расширения ...
или метод Object.assign()
:
const obj = {
firstName: 'Василий',
lastName: 'Пупкин'
};
const copy1 = {...obj};
console.log(copy1); // {firstName: 'Василий', lastName: 'Пупкин'}
// или
const copy2 = Object.assign({}, obj);
console.log(copy2); // {firstName: 'Василий', lastName: 'Пупкин'}
Если объект содержит вложенные объекты, нужно выполнить глубокое копирование. Относительно медленный вариант – с использованием JSON:
const obj = {
data: {
id: 1
}
};
const copy = JSON.parse(JSON.stringify(obj));
Другой вариант – с использованием метода cloneDeep
из библиотеки lodash.
Современный способ глубокого клонирования объектов в JavaScript
Что такое поднятие переменных (Hoisting)?
Поднятие (hoisting) — это термин, описывающий подъем переменной или функции в глобальную или функциональную области видимости.
Для того, чтобы понять, что такое Hoisting, необходимо разобраться с тем, что представляет собой контекст выполнения.
Контекст выполнения — это среда, в которой выполняется код. Контекст выполнения имеет две фазы — компиляция и выполнение.
Компиляция. В этой фазе функциональные выражения и переменные, объявленные с помощью ключевого слова «var», со значением undefined поднимаются в самый верх глобальной (или функциональной) области видимости - как бы перемещаются в начало нашего кода. Это объясняет, почему мы можем вызывать функции до их объявления
Выполнение. В этой фазе переменным присваиваются значения, а функции вызываются и выполняются.
Запомните: поднимаются только функциональные выражения и переменные, объявленные с помощью ключевого слова var
. Обычные функции и стрелочные функции, а также переменные, объявленные с помощью ключевых слов let
и const
не поднимаются.
В чем разница между обычной функцией и функциональным выражением?
Допустим, у нас есть следующее:
hoistedFunc()
notHoistedFunc()
function hoistedFunc(){
console.log('I am hoisted')
}
var notHoistedFunc = function(){
console.log('I will not be hoisted!')
}
Вызов notHoistedFunc
приведет к ошибке, а вызов hoistedFunc
нет, потому что hoistedFunc
«всплывает», поднимается в глобальную область видимости, а notHoistedFunc
нет.
Что такое прототип объекта?
Прототип - это независимый объект, хранящий свойства и методы, доступные всем объектам, чей __proto__
ссылается на него (на этот прототип). На этом принципе строится прототипное наследование в JS.
const o = {}
console.log(o.toString()) // [object Object]
Несмотря на то, что объект о
не имеет свойства toString
, обращение к этому свойству не вызывает ошибки. Если определенного свойства нет в объекте, его поиск осуществляется сначала в прототипе объекта, затем в прототипе прототипа объекта и так до тех пор, пока свойство не будет найдено. Это называется цепочкой прототипов. На вершине цепочки прототипов находится объект, доступный по обращению Object.prototype
.
console.log(o.toString === Object.prototype.toString) // true
Прототипы в JS и малоизвестные факты
Как создать объект без прототипа?
Это можно сделать с помощью Object.create
:
const o1 = {}
console.log(o1.toString) // [object Object]
// первым параметром передается объект-прототип
// нам прототип не нужен, поэтому передаем null
const o2 = Object.create(null)
console.log(o2.toString) // o2.toString is not a function
Как проверить наличие свойства в объекте?
В JavaScript есть два основных способа проверить наличие свойства в объекте – метод hasOwnProperty
и оператор in
.
Метод hasOwnProperty()
возвращает true
, если указанное свойство является прямым свойством объекта, и false
в противном случае. Этот метод не проверяет свойства в цепочке прототипов объекта.
Оператор in
возвращает true
, если указанное свойство существует в объекте, независимо от того, является ли оно собственным свойством или унаследовано:
const myObject = {
test: 'hello'
}
console.log('toString' in a) // true
console.log('test' in a) // true
console.log(a.hasOwnProperty('toString')) // false
console.log(a.hasOwnProperty('test')) // true
Что такое объектная обертка (Wrapper Objects)?
Примитивы строка, число и boolean имеют свойства и методы, несмотря на то, что они не являются объектами:
let name = 'marko'
console.log(typeof name) // string
console.log(name.toUpperCase()) // MARKO
name
— это строка (примитивный тип), у которого нет свойств и методов, но когда мы вызываем метод toUpperCase()
, это приводит не к ошибке, а к «MARKO»
.
Причина такого поведения заключается в том, что name
в момент обращение к его свойству временно преобразуется в объект. У каждого примитива кроме null
и undefined
есть объект-обертка. Такими объектами являются String
, Number
, Boolean
, Symbol
и BigInt
. В нашем случае код принимает следующий вид:
console.log(new String(name).toUpperCase()) // MARKO
Временный объект отбрасывается по завершении работы со свойством или методом.
Что такое классы?
Классы — это относительно новый способ написания функций-конструкторов в JS. Это синтаксический сахар для функций-конструкторов. В основе классов лежат те же прототипы и прототипное наследование:
// ES5
function Person(firstName, lastName, age, address){
this.firstName = firstName
this.lastName = lastName
this.age = age
this.address = address
}
Person.self = function(){
return this
}
Person.prototype.toString = function(){
return '[object Person]'
}
Person.prototype.getFullName = function(){
return this.firstName + ' ' + this.lastName
}
// ES6
class Person{
constructor(firstName, lastName, age, address){
this.firstName = firstName
this.lastName = lastName
this.age = age
this.address = address
}
static self(){
return this
}
toString(){
return '[object Person]'
}
getFullName(){
return `${this.firstName} ${this.lastName}`
}
}
Переопределение методов и наследование от другого класса:
// ES5
Employee.prototype = Object.create(Person.prototype)
function Employee(firstName, lastName, age, address, jobTitle, yearStarted){
Person.call(this, firstName, lastName, age, address)
this.jobTitle = jobTitle
this.yearStarted = yearStarted
}
Employee.prototype.describe = function(){
return `I am ${this.getFullName()} and I have a position of #{this.jobTitle} and I started at ${this.yearStarted}}`
}
Employee.prototype.toString = function(){
return '[object Employee]'
}
// ES6
class Employee extends Person{ // наследуемся от Person
constructor(firstName, lastName, age, address, jobTitle, yearStarted){
super(firstName, lastName, age, address)
this.jobTitle = jobTitle
this.yearStarted = yearStarted
}
describe(){
return `I am ${this.getFullName()} and I have a position of #{this.jobTitle} and I started at ${this.yearStarted}}`
}
toString(){ // переопределяем метод toString класса Person
return '[object Employee]'
}
}
Как узнать об использовании прототипов?
class Something{ }
function AnotherSomething(){ }
const as = new AnotherSomething()
const s = new Something()
console.log(typeof Something) // function
console.log(typeof AnotherSomething) // function
console.log(as.toString()) // [object Object]
console.log(a.toString()) // [object Object]
console.log(as.toString === Object.prototype.toString)
console.log(a.toString === Object.prototype.toString)
// в обоих случаях получаем true
// Object.prototype находится на вершине цепочки прототипов
// Something и AnotherSomething наследуют от Object.prototype
JavaScript-классы — это не просто «синтаксический сахар»
Для чего используется ключевое слово «new»?
Ключевое слово new
используется в функциях-конструкторах для создания нового объекта (нового экземпляра класса).
Допустим, у нас есть такой код:
function Employee(name, position, yearHired){
this.name = name
this.position = position
this.yearHired = yearHired
}
const emp = new Employee('Marko Polo', 'Software Development', 2017)
Ключевое слово new
делает 4 вещи:
1. Создает пустой объект.
2. Привязывает к нему значение this
.
3. Функция наследует от functionName.prototype
.
4. Возвращает значение this
, если не указано иное.
Что такое Function.prototype.apply?
apply
используется для привязки определенного объекта к значению this
вызываемой функции.
Этот метод похож на Function.prototype.call
. Единственное отличие состоит в том, что в apply
аргументы передаются в виде массива.
Что такое Function.prototype.call?
call
используется для привязки определенного объекта к значению this
вызываемой функции.
Этот метод похож на Function.prototype.apply
. Отличие состоит в том, что в call
аргументы передаются через запятую.
Что такое Function.prototype.bind?
Метод bind()
создает новую функцию, для которой зафиксирован this
и последовательность аргументов, предшествующих аргументам при вызове новой функции.
Удобно использовать для привязки контекста или закрепления параметров.
function fullName() {
return "Hello, this is " + this.first + " " + this.last;
}
console.log(fullName()); // => Hello this is undefined undefined
// привязка контекста
var person = {first: "Foo", last: "Bar"};
console.log(fullName.bind(person)()); // => Hello this is Foo Bar
Что такое объект arguments?
arguments
— это коллекция аргументов, передаваемых функции. Он имеет свойство length
и поддерживает обращение по индексу к элементам, но не является массивом, из-за чего не имеет методов forEach
, reduce
, filter
, map
и так далее.
Преобразовать arguments
в массив можно, например, с помощью Array.prototype.slice
.
В стрелочных функциях объект arguments
не работает.
Что такое IIFE?
IIFE или Immediately Invoked Function Expression — это функция, которая вызывается или выполняется сразу же после создания или объявления.
Для создания IIFE необходимо обернуть функцию в круглые скобки (оператор группировки), превратив ее в выражение, и затем вызвать ее с помощью еще одних круглых скобок.
(function( ) { }( ))
(function( ) { })( )
(function named(params) { })( )
(( ) => { })
(function(global) { })(window)
const utility = (function( ) {
return {
// утилиты
}
})
Все эти примеры являются валидными. Предпоследний пример показывает, что мы можем передавать параметры в IIFE. Последний пример показывает, что мы можем сохранить результат IIFE в переменной.
Лучшее использование IIFE — это выполнение функций настройки инициализации и предотвращение конфликтов имен с другими переменными в глобальной области видимости (загрязнение глобального пространства имен).
Почему пора перестать использовать JavaScript IIFE
Что такое стрелочные функции?
(argument1, argument2, ... argumentN) => {
// тело функции
}
Это анонимные функции с особым синтаксисом, выполняющиеся в контексте включающей их области видимости, то есть в контексте блока, в котором они объявлены.
У стрелочных функций нет доступа к объекту arguments
. Также они не имеют своего this
, а следовательно их нельзя использовать как конструкторы с ключевым словом new
.
Что такое Promise (Обещания)?
1. Promise — объект JavaScript, предназначенный для обработки асинхронного кода. Промисы позволяют откладывать блоки кода до момента выполнения или отклонения действия.
2. Promise – это специальный объект, который содержит своё состояние. Вначале pending («ожидание»), затем – одно из: fulfilled («выполнено успешно») или rejected («выполнено с ошибкой»).
const promise = new Promise(function(resolve, reject) {
// Эта функция будет вызвана автоматически
// В ней можно делать любые асинхронные операции,
// А когда они завершатся — нужно вызвать одно из:
// resolve(результат) при успешном выполнении
// reject(ошибка) при ошибке
})
Перечислите методы Promise.*
Promise.all()
- возвращает промис, который выполнится тогда, когда будут выполнены все промисы, переданные в виде перечисляемого аргумента, или отклонено любое из переданных промисов.
Promise.allSettled()
- возвращает промис, который исполняется когда все полученные промисы завершены (исполнены или отклонены), содержащий массив результатов исполнения полученных промисов.
Promise.any()
- принимает итерируемый объект содержащий объекты промисов. Как только один из промисов выполнится успешно, метод возвратит единственный объект Promise со значением выполненного промиса. Если ни один из промисов не завершится успешно, тогда возвращённый объект будет отклонён.
Promise.race()
- возвращает выполненный или отклонённый промис, в зависимости от того, с каким результатом завершится первый из переданных промисов, со значением или причиной отклонения этого промиса.
Promise.reject(reason)
- возвращает объект Promise, который был отклонён по указанной причине.
Promise.resolve(value)
- возвращает Promise выполненный с переданным значением.
Что такое async/await?
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("готово!"), 1000)
});
let result = await promise; // будет ждать, пока промис не выполнится (*)
alert(result); // "готово!"
}
f();
async/await
— относительно новый способ написания асинхронного (неблокирующего) кода в JS. Он делает код более читаемым и чистым, чем промисы и функции обратного вызова.
Использование ключевого слова async
перед функцией заставляет ее возвращать промис.
Ключевое слово await
можно использовать только внутри асинхронной функции. await
ожидает завершения выражения справа, чтобы вернуть его значение перед выполнением следующей строчки кода.
Как проверить, является ли объект массивом?
Для такой проверки можно использовать встроенный метод Array.isArray()
.
Он принимает объект в качестве аргумента и возвращает true
, если объект является массивом, и false
в противном случае:
let arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true
let obj = { a: 1, b: 2 };
console.log(Array.isArray(obj)); // false
Какие есть способы перебора массива?
Array.prototype.forEach
const a = ["a", "b", "c"];
a.forEach(function(entry) {
console.log(entry);
});
Цикл for
Может применяться как с инкрементом, так и с декрементом.
for (let i = 0; i < a.length; i += 1) {
console.log(a[i]);
}
Цикл for...in
Вопреки распространенному заблуждению цикл for...in
перебирает не индексы массива, а перечисляемые (iterable) свойства объекта.
Тем не менее, в некоторых случаях, таких как перебор разреженных массивов, for...in
может оказаться полезным, если только соблюдать при этом меры предосторожности, как показано в примере ниже:
// a - разреженный массив
var a = [];
a[0] = "a";
a[10] = "b";
a[10000] = "c";
for (var key in a) {
if (a.hasOwnProperty(key) &&
/^0$|^[1-9]\d*$/.test(key) &&
key <= 4294967294) {
console.log(a[key]);
}
}
В данном примере на каждой итерации цикла выполняется две проверки:
- то, что массив имеет собственное свойство с именем key
(не наследованное из его прототипа).
- то, что key
— строка, содержащая десятичную запись целого числа, значение которого меньше 4294967294. Откуда берется последнее число? Из определения индекса массива в ES5, из которого следует, что наибольший индекс, который может иметь элемент в массиве: (2^32 - 2) = 4294967294.
Конечно, такие проверки отнимут лишнее время при выполнении цикла. Но в случае разреженного массива этот способ более эффективен, чем цикл for
, поскольку в этом случае перебираются только те элементы, которые явно определены в массиве. Так, в примере выше будет выполнено всего 3 итерации (для индексов 0, 10 и 10000) — против 10001 в цикле for
.
Цикл for...of
Оператор for (переменная) of (сущность)
позволяет пройти в цикле по свойствам сущности. Оператор работает только с итерируемыми сущностями. В начале цикла оператор достаёт из сущности итератор. Каждая итерация цикла – это вызов метода .next()
итератора.
let iterable = [10, 20, 30]
for (let value of iterable) {
value += 1
console.log(value)
}
// 11
// 21
// 31
И несколько методов, осуществляющих перебор и некоторые побочные действия вместе с ним:
Array.prototype.every
— возвращает true
, если для каждого элемента массива колбек возвращает значение приводимое к true.
Array.prototype.some
— возвращает true
, если хотя бы для одного элемента массива колбек возвращает значение приводимое к true.
Array.prototype.filter
— создает новый массив, включающий те элементы исходного массива, для которых колбек возвращает true.
Array.prototype.map
— создает новый массив, состоящий из значений возращаемых колбеком.
Array.prototype.reduce
— сводит массив к единственному значению, применяя колбек по очереди к каждому элементу массива, начиная с первого (может быть полезен для вычисления суммы элементов массива и других итоговых функций).
Array.prototype.reduceRight
— работает аналогично reduce, но перебирает элементы в обратном порядке.
Как очистить массив?
1. Присвоить переменной новый пустой массив
let arr = [1, 2, 3];
arr = [];
Этот способ можно использовать, если ваш код никак не ссылается на оригинальный массив, так как при этом создается совершенно новый объект. Если в какой-то переменной есть ссылка на arr
, то она не изменится.
let arrayList = ['a', 'b', 'c', 'd', 'e', 'f'];
let anotherArrayList = arrayList;
arrayList = [];
console.log(anotherArrayList); // ['a', 'b', 'c', 'd', 'e', 'f']
2. Обнулить длину массива
let arr = [1, 2, 3];
arr.length = 0;
Существующий массив очистится, так же как и все ссылки на него.
let arrayList = ['a', 'b', 'c', 'd', 'e', 'f'];
let anotherArrayList = arrayList;
arrayList.length = 0;
console.log(anotherArrayList); // []
3. Вырезать лишние элементы
let arr = [1, 2, 3];
arr.splice(0, arr.length);
Работает так же, как предыдущий способ:
let arrayList = ['a', 'b', 'c', 'd', 'e', 'f'];
let anotherArrayList = arrayList;
arrayList.splice(0, arrayList.length);
console.log(anotherArrayList); // []
4. Удалять элементы по одному
let arr = [1, 2, 3];
while(arr.length) {
arr.pop();
}
Довольно громоздко, но может быть полезно, если вы хотите перед удалением что-нибудь еще сделать с элементами.
Перечислите методы Array.prototype.*
- Array.prototype.at() - Array.prototype.concat() - Array.prototype.copyWithin() - Array.prototype.entries() - Array.prototype.every() - Array.prototype.fill() - Array.prototype.filter() - Array.prototype.find() - Array.prototype.findIndex() - Array.prototype.findLast() - Array.prototype.findLastIndex() - Array.prototype.flat() - Array.prototype.flatMap() - Array.prototype.forEach() - Array.prototype.includes() - Array.prototype.indexOf() - Array.prototype.join() - Array.prototype.keys() - Array.prototype.lastIndexOf() - Array.prototype.map() - Array.prototype.pop() - Array.prototype.push() - Array.prototype.reduce() - Array.prototype.reduceRight() - Array.prototype.reverse() - Array.prototype.shift() - Array.prototype.slice() - Array.prototype.some() - Array.prototype.sort() - Array.prototype.splice() - Array.prototype.toLocaleString() - Array.prototype.toString() - Array.prototype.unshift() - Array.prototype.values() Столько хватит? Есть ещё.
Что такое шаблонные литералы?
Шаблонные литералы — относительно новый способ создания строк в JS. Шаблонные литералы создаются с помощью двойных обратных кавычек ``
:
// ES5
var greet = 'Hi I\'m Mark'
// ES6
let greet = `Hi I'm Mark`
В шаблонных литералах нам не нужно экранировать одинарные кавычки.
// ES5
var lastWords = '\n'
+ ' I \n'
+ ' am \n'
+ 'Iron Man \n'
// ES6
let lastWords = `
I
am
Iron Man
`
В ES6 нам не нужно использовать управляющую последовательность "\n"
для перевода строки.
// ES5
function greet(name){
return 'Hello ' + name + '!'
}
// ES6
function greet(name){
return `Hello ${name}!`
}
В ES6 нам не нужно использовать конкатенацию строк для объединения текста с переменной: мы можем использовать выражение ${expr}
для получения значения переменной.
Что такое модули?
Модули позволяют объединять (использовать) код из разных файлов и избавляют нас от необходимости держать весь код в одном большом файле. До появления модулей в JS существовало две популярные системы модулей для поддержки кода:
CommonJS — Nodejs
AMD (AsyncronousModuleDefinition) — Browsers
Синтаксис модулей очень простой: мы используем import
для импорта функциональности или значений из другого файла или файлов и export
для экспорта.
Экспорт функциональности в другой файл (именной экспорт):
// ES5 CommonJS - helpers.js
exports.isNull = function(val){
return val === null
}
exports.isUndefined = function(val){
return val === undefined
}
exports.isNullOrUndefined = function(val){
return exports.isNull(val) || exports.isUndefined(val)
}
// ES6 модули
export function isNull(val){
return val === null;
}
export function isUndefined(val) {
return val === undefined;
}
export function isNullOrUndefined(val) {
return isNull(val) || isUndefined(val);
}
Импорт функциональности в другой файл:
// ES5 CommonJS - index.js
const helpers = require('./helpers.js')
const isNull = helpers.isNull
const isUndefined = helpers.isUndefined
const isNullOrUndefined = helpers.isNullOrUndefined
// либо с помощью деструктуризации
const { isNull, isUndefined, isNullOrUndefined } = require('./helpers.js')
// ES6 модули
import * as helpers from './helpers.js' // helpers - это объект
// либо
import { isNull, isUndefined, isNullOrUndefined as isValid} from './helpers.js' // используем "as" для переименовывания
Экспорт по умолчанию:
// ES5 CommonJS - index.js
class Helpers {
static isNull(val){
return val === null
}
static isUndefined(val){
return val === undefined
}
static isNullOrUndefined(val){
return this.isNull(val) || this.isUndefined(val)
}
}
module.exports = Helpers
// ES6 модули
class Helpers {
static isNull(val){
return val === null
}
static isUndefined(val){
return val === undefined
}
static isNullOrUndefined(val){
return this.isNull(val) || this.isUndefined(val)
}
}
export default Helpers
Импорт:
// ES5 CommonJS - index.js
const Helpers = require('./helpers.js')
console.log(Helpers.isNull(null))
// ES6 модули
import Helpers from './helpers.js'
console.log(Helpers.isNull(null))
Это базовое использование модулей.
Модули в JavaScript
Что такое функция обратного вызова (Callback Function)?
Коллбэк — это функция, которая передается в другую функцию как аргумент и выполняется после того, как закончатся какие-то другие операции. В примере ниже коллбэк логирует в консоль.
function modifyArray(arr, callback) {
// некоторые операции с массивом arr
arr.push(100);
// после того, как операции закончены, выполняется коллбэк
callback();
}
let arr = [1, 2, 3, 4, 5];
modifyArray(arr, function() {
console.log("массив модифицирован", arr);
});
Что такое полифиллы и шимы (shim)?
Шим — это любой фрагмент кода, который перехватывает обращение к API и добавляет уровень абстракции в приложение. Шимы существуют не только в вебе. Полифилл — это шим для веба и браузерных API — специальный код (или плагин), который позволяет добавить некоторую функциональность в среду, которая эту функциональность по умолчанию не поддерживает (например, добавить новые функции JS в старые браузеры). Полифиллы не являются частью стандарта HTML5.
Что такое транспиляция?
Транспиляция — преобразование кода, написанного в новом стандарте в его эквивалент в старом стиле (ES6 в ES5). Как правило, для осуществления этой процедуры используется babel.
Почему 0.1 + 0.2 === 0.3 — это false?
Действительно, в JavaScript 0.1 + 0.2
на самом деле равно 0.30000000000000004
.
Дело в том, что все числа в языке (даже целые) представлены в формате с плавающей запятой (float
). В двоичной системе счисления эти числа — бесконечные дроби. Для их хранения выделяется ограниченный объем памяти, поэтому возникают подобные неточности.
Что такое генераторы?
Генераторы – специальный тип функций в JavaScript. Они отличаются от обычных тем, что могут приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять его позже, в произвольный момент времени.
Для объявления генератора используется синтаксическая конструкция: function*
(функция со звёздочкой). Её называют «функция-генератор» (generator function).
При запуске generateSequence()
код такой функции не выполняется. Вместо этого она возвращает специальный объект, который как раз и называют «генератором». Можно воспринимать генератор как «замороженный вызов функции». При создании генератора код находится в начале своего выполнения.
Основным методом генератора является next()
. При вызове он возобновляет выполнение кода до ближайшего ключевого слова yield
. По достижении yield
выполнение приостанавливается, а значение – возвращается во внешний код:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}
let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}
let three = generator.next();
alert(JSON.stringify(three)); // {value: 3, done: true}
Новые вызовы generator.next()
больше не имеют смысла. Впрочем, если они и будут, то не вызовут ошибки, но будут возвращать один и тот же объект: {done: true}
.
Какие существуют методы для хранения данных в браузере?
Есть 3 основных метода хранения данных в браузере:
LocalStorage и SessionStorage используются для хранения пар ключ-значение. Данные, сохраненные в них, сохраняются после обновления страницы. При этом только LocalStorage может сохранять данные после перезапуска браузера. Оба хранилища могут использовать только строки в качестве ключей и значений, поэтому объекты необходимо преобразовать с помощью JSON.stringify()
.
Cookie – небольшие строки данных, которые хранятся в браузере. Cookie обычно устанавливаются веб-сервером с использованием заголовка Set-Cookie
. Браузер затем автоматически добавляет их почти ко всем запросам на тот же домен с использованием заголовка Cookie
. Один экземпляр cookie может содержать до 4 кб данных. В зависимости от браузера, допускается более 20 cookie на сайт.
IndexedDB – встроенная база данных, более мощная, чем LocalStorage. Это NoSQL-хранилище данных в формате JSON внутри браузера, где доступны несколько типов ключей, а значения могут быть практически любым. IndexedDB поддерживает асинхронный доступ, транзакции для обеспечения согласованности данных и создание индексов для эффективного поиска. Позволяет хранить больше данных, чем LocalStorage, может быть связана с Service Workers и другими технологиями, которые обеспечивают функционирование PWA в оффлайне.
В чем разница между sessionStorage и localStorage?
Сессионное хранилище sessionStorage
и локальное хранилище localStorage
позволяют сохранять данные в формате ключ-значение в браузере. Оба они используются для хранения данных на стороне клиента, но имеют некоторые отличия:
1. Объем хранимых данных – localStorage
может хранить до 10 МБ данных, в то время как sessionStorage
может хранить только до 5 МБ данных.
2. Срок хранения данных – в localStorage
данные не удаляются, когда закрывается браузер или вкладка. И напротив, данные в sessionStorage
удаляются, когда закрывается вкладка или окно браузера.
3. Доступность данных – из localStorage
данные доступны в любом окне браузера, в то время как данные из sessionStorage
доступны только из того же окна браузера, где они были сохранены.
В чем разница между WeakSet, WeakMap и обычными Set и Map?
WeakSet
и WeakMap
– это специальные структуры данных в JavaScript, которые отличаются особенностью хранения ссылок на объекты.
В обычных Set
и Map
хранятся сильные ссылки на объекты. Это значит, что пока существует ссылка на объект в этих структурах, сборщик мусора не удалит этот объект из памяти, даже если больше нигде в коде нет ссылок на него.
И напротив, в WeakSet
и WeakMap
хранятся слабые ссылки. Это означает, что если объект, на который есть ссылка в этих структурах, больше недоступен в коде (т.е. нигде больше нет сильных ссылок на него), то сборщик мусора может удалить этот объект из памяти, даже если в WeakSet
или WeakMap
все еще есть ссылка на него. Таким образом, использование слабых ссылок позволяет не держать в памяти ненужные больше объекты и экономить память.
Кроме того, в WeakMap
в качестве ключей могут использоваться только объекты, а не примитивные значения. А в WeakSet
хранятся только объекты, без ключей.
Реализация приватных полей с помощью WeakMap в JavaScript