Вопросы и ответы на собеседование: CORE JS

Вопросы и ответы на собеседование: CORE JS

Front-end 26.06.2019

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

Поделиться

Отправить ответ

avatar
  Subscribe  
Notify of