Вопросы и ответы на собеседование: CORE JS
1. Context (bind / call / apply)
this — это контекст вызова функции.
В глобальном контексте выполнения, за пределом функций this
ссылается на глобальный объект, а в пределах функции this
зависит от того, каким методом была вызвана функция. Рассмотрим несколько примеров:
- В браузерах глобальным является объект window
console.log(this)
// Window
- Вызов функции в нестрогом режиме
function demoFunc() {
return this;
}
demoFunc();
// Window
При данном вызове функции мы не изменяем значение this
и оно остается глобальным.
- Вызов функции в Strict mode
function demoStrictFunt() {
"use strict";
return this;
}
demoStrictFunt();
// undefined
В строгом режиме значение this
остается тем самым, которое было установлено в контексте выполнения, если оно не было установлено — по умолчанию будет undefined.
- Arrrow functions
В стрелочных функциях this
привязан к окружению в котором была вызвана функция, рассмотрим 2 примера.
let user = {
name: 'Missy',
age: '3',
sayHello: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
user.sayHello();
// Hello, my name is Missy
Здесь у нас в функцию sayHello передается this
с внешнего объекта и мы получаем строку «Hello, my name is Missy», теперь попробуем sayHello
заменить на стрелочную функцию:
let user = {
name: 'Missy',
age: '3',
sayHello: () => console.log(`Hello, my name is ${this.name}`)
};
user.sayHello();
// Hello, my name is
При подобном вызове стрелочная функция не получит внешний объект this
и this.name
будет равно undefined
, если значение не передать, к примеру через bind
:
let user = {
name: 'Missy',
age: '3',
sayHello: (x) => console.log(`Hello, my name is ${x.name}`),
};
user.sayHello = (user.sayHello).bind(this, user);
user.sayHello();
// Hello, my name is Missy
И вот мы уже вплотную добрались до методов bind / call / apply.
call и apply позволяют выполнять косвенный вызов функции так если бы она была методом другого объекта. Первым аргументом в оба метода передается объект, относительно которого вызывается функция, этот объект определяет контекст вызова и становится значением ключевого слова this
функции. Остальные аргументы, которые передаются методам call / apply — отправляются в функцию в качестве аргументов.
function func(y) {
return this.x + y
}
func.call({x: 5}, 2); // => 7
Отличием методов call / apply является способ передачи аргументов — в apply они передаются в виде массива.
function func(a, b) {
return a + b
}
func.call(this, 1, 2); // => 3
func.apply(this, [4, 2]); // => 6
После вызова этих методов функция немедленно выполняется, в отличие от метода bind.
Основное назначение метода bind — связать функцию с объектом. Если вызвать метод bind у функции — он вернет новую функцию, которую после, в нужный момент, можно будет вызвать. Рассмотрим на примере
function func() {
return this.name;
}
console.log(func()); // ''
funcBinded = func.bind({name: 'Missy'});
console.log(funcBinded()); // "Missy"
console.log(func()); // ''
Помимо значения this
оригинальной функции будут переданы и все аргументы, которые передавались методу bind
. Частичное применение называют каррингом (currying):
var sum = function(x,y) {return x + y};
var sumBinded = sum.bind(null, 1);
console.log(sumBinded(2)); // 3
console.log(sumBinded(4)); // 5
В данном примере в качестве первого аргумента в bind
передан null
, так как я не меняла контекст вызова функции.
2. Scope
В JS существуют глобальные и локальные переменные. Глобальные переменные — это те, объявление которых не находится внутри какой-то функции. Они являются свойствами глобального объекта (global object), в браузере это window
.
var demoVar = 'test global variable';
console.log(window.demoVar);
// 'test global variable'
Все переменные которые объявляются внутри функции это свойства объекта LocalEnvironment и доступны только внутри этой функции.
console.log(demo); // Uncaught ReferenceError: demo is not defined
function test() {
console.log(demo); // undefined
var demo = 3;
console.log(demo); // 3
}
test();
console.log(demo); // Uncaught ReferenceError: demo is not defined
Переменная demo
видна только в теле функции так как была там объявлена, при чем до присвоения ей значения она тоже существует, но со значением undefined (актуально для ES5), так как в ES5 объявление функций поднимается (hoisting). В данном случае LexicalEnvironment = {demo: 3}
.
Каждый раз когда интерпретатор JS вызывает функцию, он создает новый объект для хранения локальных переменных этой функции и добавляет его в цепочку области видимости. В конце выполнения функции объект с переменными удаляется из цепочки вызовов. Если нет вложенных функций и ссылок на объект — он будет утилизирован сборщиком мусора.
var scope = 'global';
function checkScope() {
var scope = 'local';
function f() { return scope }
return f;
}
checkScope()(); // 'local'
В JS лексическая область видимости область видимости, а это значит, что при выполнении функций действует область видимости, существовавшая на момент определения функций, а не на момент вызова.
Комбинация объекта функции и ее области видимости называется замыканием.
Вложенные функции имеют доступ к внешним функциям.
function counter() {
var i = 0;
return function () {
return i++;
}
}
var demo1 = counter();
demo1() // => 0
demo1() // => 1
demo1() // => 2
var demo2 = counter();
demo2() // => 0
demo2() // => 1
demo1() // => 3
В данном примере внутри функции counter()
создается другая функция и возвращается в качестве результата, таким образом мы получили два независимых счетчика demo1 и demo2, которые хранят свои результаты в переменной i, а эта переменная у каждого счетчика своя.
- В строке
var demo1 = counter()
запускается функцияcounter()
и ей создается LexicalEnvironment со свойствомi = undefined
- Далее в строке
var i = 0;
свойствуi
присваивается начальное значениеi = 0
, а в строкеreturn function () {...}
создается новая функция, которая получает внутреннее свойство Scope и ссылку на текущий LexicalEnvironment. - После вызов counter() завершается, а внутренняя функция возвращается и сохраняется во внешней переменной demo1
3. Hoisting
Hoisting (поднятие) — это механизм, когда переменные и объявления функций поднимаются по своей области видимости перед тем, как код будет выполнен.
Рассмотрим простой пример объявления переменной:
var a = 1;
...
a += 10;
На самом деле в JavaScript эта операция выглядит так:
var a;
// объявление (declaration): a = undefined
...
a = 1; // инициализация (initialisation)
...
a += 10; // использование (usage)
В данном случае объявление переменной a
поднимается области видимости, но изначально эта переменная обладает значением undefined
. В ES5 область видимости объявляется со словом var
, а переменные не объявлены зарезервированным словом получат глобальную область видимости.
function demo() {
globalVar = 'msg'; // неявно объявлена глобальная переменная
var localVar = 'ms2';
}
demo();
conssole.log(window.globalVar); // => 'msg'
// просто проверили, что она все же глобальная
use strict mode
В строгом режиме нельзя упустить объявление переменной — это вызовет ReferenceError
.
'use strict'
console.log(demoVar); // ReferenceError: demoVar is not defined
demoVar = 1;
ES6: let, const
let
и const
нельзя использовать до их объявления:
console.log(demoVar); // Uncaught ReferenceError: Cannot access 'demoVar' before initialization
let demoVar = 1;
4. var, let & const
Основные отличия let от var
- область видимости
let
— блок
let demo = 1;
if(true) {
let demo = '2';
console.log(demo); // 2
}
console.log(demo); // 1
let
видна только после объявления
let
нельзя повторно объявлять в этом же блоке
let a;
let a; // SyntaxError: Identifier 'a' has already been declared
- При использовании в цикле, для каждой итерации создаётся своя переменная
Рассмотрим пример с циклом for и функцией setTimeout:
for (var i = 0; i<10; i++) { setTimeout(() => {
console.log(i);
}, 1000)
}
В случае с var мы получим 10 раз выведенное число 10.
for (let i = 0; i<10; i++) { setTimeout(() => {
console.log(i);
}, 1000)
}
При объявлении переменной i
через let
, мы получим ожидаемый результат — выведутся числа от 0 до 9, так как для каждого цикла создается своя переменная.
Основные отличия const
В отличие отlet
, объявлениеconst
задаёт константу, то есть переменную, которую нельзя менять
const PI = 3.14;
PI = 3.142;
// TypeError: Assignment to constant variable.
При попытке создать const
без значения тоже возникнет ошибка:
const MSG;
// SyntaxError: Missing initializer in const declaration
5. Promises. Promise chain
Promises существуют как способ организации асинхронного кода.
Promise — это объект, который содержит свое состояние. На этапе формирования Промиса он находится в процессе ожидания (pending) и после перейдет в состояние Выполнено (fulfilled) или Отклонено (rejected). При смене состояния промиса на fulfilled / rejected выполнится соответствующий обработчик.
var demoPromise = new Promise (function (resolve, reject) {
// Эта функция может делать что-то полезное
// Но я просто вызову ее состояние "Выполлнено"
resolve('success');
});
demoPromise.then((response) => {console.log(response)});
demoPromise.catch((error) => {console.error(error)});
then
может принимать 2 callback-функции — на успешное выполнение (onFulfiled) и на ошибку (onRejected) или один из них, но функция на успешное выполнение должна быть первой:
demoPromise.then(onFulfiled, onRejected);
demoPromise.then(onFulfiled);
demoPromise.then(null, onRejected);
На практике для обработки ошибки лучше использовать метод catch
:
demoPromise.catch(onRejected);
Промисы можно использовать с setTimeot()
, например:
var demoPromise = new Promise (function (resolve, reject) {
setTimeout(() => {
resolve('success');
}, 3000)
});
demoPromise.then((response) => {console.log(response)});
Promise chain — это цепочки промисов, когда в каждый следующий then
переходит результат от предыдущего.
// Получим данные о книге и узнаем имя владельца
httpGet('http://demo/book/1')
// Передадим полученный объект в первый then и достанем значение нужного свойства owner (владелец книги)
.then((resp) => {
var book = JSON.parse(resp);
return book.ownerId;
};
// Так как теперь у нас есть id владельца книги - мы можем получить его данные
.then((id) => httpGet('http://demo/users/' + id)
// После того как мы получили объект с данными владельца - мы легко можем достать его имя
.then((owner) => {
console.info(owner.name);
}
Методы Промисов
Promise.resolve(value) — создает успешно выполненный Promise с результатом value
.
Promise.resolve(url) // начать с этого значения
.then(httpGet) // вызвать для него httpGet
.then(alert) // и вывести результат
Promise.reject(error) — создает Promise с ошибкой error
.
Promise.reject(new Error("Custom Error"))
.catch(alert)
Promise.all([…]) — получает массив промисов и возвращает промис который ждет выполнение всех переданных промисов и переходит в состояние «Выполнено» с массивом их результатов.
Promise.all([
httpGet('/article/promise/user.json'),
httpGet('/article/promise/guest.json'),
])
.then(resp => console.log(resp))
.catch(err => console.log(err));
Promise.race([…]) работает аналогично Promise.all
, но результатом будет только первый успешно выполненный промис из списка, остальные игнорируются.
5. Event Loop
Вызов любой функции создает контекст выполнения. При вызове вложенной функции создается новый контекст выполнения, а старый сохраняется в специальной структуре — так формируется Стек (Stack) контекстов.
function f(y) {
var z = 5;
return y + z;
}
function g(x) {
var a = 4;
return f(x*a)
}
g(1);
При вызове функции g
, создается контекст выполнения функции g
. Когда управление передается функции f
— создается контекст выполнения функции f
, который будет находиться в стеке выше, чем предыдущий. После того как функция f
завершит свое выполнение и передаст значение в g
— контекст выполнения функции f
удалится из стека, а после завершения выполнения g
— удалится и контекст выполнения функции g
.
Среда выполнения JS содержит Очередь (Queue) событий — это список событий, подлежащих обработке, где каждое событие ассоциируется с функцией. Когда на стеке освобождается место — событие извлекается из очереди.
Объекты размещаются в куче (Heap) — это область памяти, где хранятся объекты когда мы их определяем.
В браузере события добавляются как только произошли и если на них есть обработчик событий. В случае, когда обработчика нет, к примеру, на событие click
— оно будет утеряно.
setTimeout
добавит событие в конец очереди по пришествию указанного времени, таким образом время, указанное при вызове setTimeout
является минимальным временем, через которое выполнится callback
функция.
Поскольку console.log
с setTimeout
будет помещено в конец стека, то в примере выведение чисел будут не последовательны:
(function () {
console.log('1');
setTimeout(function () {
console.log('2');
});
console.log('3');
setTimeout(function () {
console.log('4');
}, 0);
console.log('5');
})();
// 1, 3, 5, 2, 4
Источники:
«Javascript. Подробное руководство» Дэвид Флэнаган
Современный учебник JavaScript
Веб-документация MDN
Medium