JavaScript-функции, объекты и типы данных: подготовка к интервью

Мы собрали самые важные и популярные темы по JavaScript, которые попадаются на собеседованиях, и коротко, но емко рассказываем про JavaScript-функции, области видимости и асинхронный код.

Типы и приведение типов

В JavaScript имеется 7 встроенных типов: null, undefined , boolean, number, string, object и symbol (ES6). Все эти типы зовутся примитивами, кроме object.

typeof 0              // number
typeof true           // boolean
typeof 'Hello'        // string
typeof Math           // object
typeof null           // object  !!
typeof Symbol('Hi')   // symbol (New ES6)

Null и Undefined

Undefined – отсутствие определения объекта/переменной. Функция вернет undefined, когда возвращаемый объект не указан явно.

Null – отсутствие значения. Это значение можно присвоить переменной.

Неявное приведение типов

var name = 'Joey';
if (name) {
  console.log(name + " doesn't share food!")  // Joey doesn’t share food!
}

В этом примере строковая переменная name приводится к true.

К true будут приводится все неявно ложные значения, которые приводятся к boolean.

Явно ложные значения: "", 0, null, undefined, NaN, false.

Boolean(null)         // false
Boolean('hello')      // true 
Boolean('0')          // true 
Boolean(' ')          // true 
Boolean([])           // true 
Boolean(function(){}) // true

Строки и числовое преобразование

Осторожно работайте с оператором «+», так как этот оператор работает как со строками, так и с числами, в то время, как «*», «/» и «-» – исключительно для чисел. Если применить эти операторы к строке, она будет преобразована в числовой тип.

1 + "2" = "12"
"" + 1 + 0 = "10"
"" - 1 + 0 = -1
"-9\n" + 5 = "-9\n5"
"-9\n" - 5 = -14
"2" * "3" = 6
4 + 5 + "px" = "9px"
"$" + 4 + 5 = "$45"
"4" - 2 = 2
"4px" - 2 = NaN
null + 1 = 1
undefined + 1 = NaN

== и ===

Многие привыкли считать, что оператор == проверяет равенство значений, а === – равенство типов и значений. На деле, это работает немного иначе.

  • == проверяет равенство с приведением типов
  • === проверяет равенство без приведения типов
2 == '2'            // True
2 === '2'           // False
undefined == null   // True
undefined === null  // False

Приведение может оказаться обманчивым:

let a = '0';
console.log(Boolean(a)); // True
let b = false;
console.log(Boolean(b)); // False

Если сравнить переменные из примера, то console.log(a == b) вернет true. Так происходит потому, что строковая переменная a, приведенная к boolean будет одним из явно ложных значений (0), так же, как и b (false).

В операции с использованием оператора === console.log вернет false, так как переменная a не будет приведена к логическому типу.

Вот еще несколько примеров обманчивых сравнений:

false == ""  // true
false == []  // true
false == {}  // false
"" == 0      // true
"" == []     // true
"" == {}     // false
0 == []      // true
0 == {}      // false
0 == null    // false

Значения и ссылки

Составные значения всегда создают копию ссылки на назначение: объекты (в том, числе массивы) и функции.

var a = 2;        
var b = a;        // 'b' копия значения 'a'
b++;
console.log(a);   // 2
console.log(b);   // 3
var c = [1,2,3];
var d = c;        // 'd' ссылается на общее значение
d.push( 4 );      // изменение объекта
console.log(c);   // [1,2,3,4]
console.log(d);   // [1,2,3,4]
/* значения по ссылке равны */
var e = [1,2,3,4];
console.log(c === d);  // true
console.log(c === e);  // false

Чтобы скопировать составное значение как значение, необходимо создать его копию. Таким образом, не будет ссылки на оригинальное значение.

const copy = c.slice()    // 'copy' отсылается к новому значению
console.log(c);           // [1,2,3,4]
console.log(copy);        // [1,2,3,4]
console.log(c === copy);  // false

Области видимости

Область видимости отсылает к исполняемому контексту. Она определяет доступность переменных и функций в коде.

Глобальная область видимости включает в себя все переменные, объявленные за пределами JavaScript-функции. В браузере глобальной областью будет объект window.

Локальная область видимости – это область внутри границ JavaScript-функции. Переменные, объявленные в этой области будут доступны внутри самой функции и из ее вложений.

function outer() {
  let a = 1;
  function inner() {
    let b = 2;
    function innermost() {
      let c = 3;
      console.log(a, b, c);   // 1 2 3
    }
    innermost();
    console.log(a, b);        // 1 2 — 'c' не определена
  }
  inner();
  console.log(a);             // 1 — 'b' и 'c' не определены
}
outer();

Поднятие

«Движение» объявлений переменных и функций наверх соответствующей области видимости во время компиляции называется поднятием.

JavaScript-функции по умолчанию подняты: это значит, что функцию можно вызвать еще до ее объявления.

console.log(toSquare(3));  // 9

function toSquare(n){
  return n*n;
}

Переменные поднимаются иначе. Для var будет поднято только объявление, но не значение. let и const не будут подняты.

{  /* оригинальный код */
  console.log(i);  // undefined
  var i = 10
  console.log(i);  // 10
}

{  /* этап компиляции */
  var i;
  console.log(i);  // undefined
  i = 10
  console.log(i);  // 10
}
// ES6 let & const
{
  console.log(i);  // ReferenceError: i is not defined
  const i = 10
  console.log(i);  // 10
}
{
  console.log(i);  // ReferenceError: i is not defined
  let i = 10
  console.log(i);  // 10
}

Выражение и объявление JavaScript-функции

Выражение функции – это «запись» JavaScript-функции в переменную, такое описание функции не будет поднято.

var sum = function(a, b) {
  return a + b;
}

Объявление функции – это привычное описание тела функции с ключевого слова function. Такая функция будет исполняться в любом месте.

function sum(a, b) {
  return a + b;
}

Переменные: var, let и const

До ES6 в JavaScript было возможно использовать только var. Переменные и функции, созданные внутри функции будут доступны только в области видимости JavaScript-функции. Переменные, объявленные в блоках типа if/for могут быть доступны в любой области видимости. Если условие или цикл объявлены в глобальной области – переменные возможно будет менять из любого участка кода.

Необъявленные переменные (без присвоения var, let или const) будут автоматически объявляться в глобальной области видимости.

function greeting() {
  console.log(s) // undefined
  if(true) {
    var s = 'Hi';
    undeclaredVar = 'I am automatically created in global scope';
  }
  console.log(s) // 'Hi'
}
console.log(s);  // Error — ReferenceError: s is not defined
greeting();
console.log(undeclaredVar) // 'I am automatically created in global scope'

Const и let не поднимаются и могут быть доступны внутри блока кода, ограниченного фигурными скобками.

let g1 = 'global 1'
let g2 = 'global 2'
{   /* создание области видимости блока */
  g1 = 'new global 1'
  let g2 = 'local global 2'
  console.log(g1)   // 'new global 1'
  console.log(g2)   // 'local global 2'
  console.log(g3)   // ReferenceError: g3 is not defined
  let g3 = 'I am not hoisted';
}
console.log(g1)    // 'new global 1'
console.log(g2)    // 'global 2'

Const не меняется, то есть нельзя присвоить новые значения. Но вот свойства меняться могут.

const tryMe = 'initial assignment';
tryMe = 'this has been reassigned';  // TypeError: Assignment to constant variable.
// You cannot reassign but you can change it…
const array = ['Ted', 'is', 'awesome!'];
array[0] = 'Barney';
array[3] = 'Suit up!';
console.log(array);     // [“Barney”, “is”, “awesome!”, “Suit up!”]
const airplane = {};
airplane.wings = 2;
airplane.passengers = 200;
console.log(airplane);   // {passengers: 200, wings: 2}

Замыкания

Замыкание – это функция и ее лексическое окружение. Замыкания позволяют функции иметь доступ к переменным из вложенных областей видимости – окружения.

function sayHi(name){
  var message = `Hi ${name}!`;
  function greeting() {
    console.log(message)
  }
  return greeting
}
var sayHiToJon = sayHi('Jon');
console.log(sayHiToJon)     // ƒ() { console.log(message) }
console.log(sayHiToJon())   // 'Hi Jon!'

Одно из основных преимуществ  замыканий – инкапсуляция данных. Это означает, что данные не могут быть доступны открыто, как в примере ниже.

function SpringfieldSchool() {
  let staff = ['Seymour Skinner', 'Edna Krabappel'];
  return {
    getStaff: function() { console.log(staff) },
    addStaff: function(name) { staff.push(name) }
  }
}

let elementary = SpringfieldSchool()
console.log(elementary)        // { getStaff: ƒ, addStaff: ƒ }
console.log(staff)             // ReferenceError: staff is not defined
/* Closure allows access to the staff variable */
elementary.getStaff()          // ["Seymour Skinner", "Edna Krabappel"]
elementary.addStaff('Otto Mann')
elementary.getStaff()          // ["Seymour Skinner", "Edna Krabappel", "Otto Mann"]

С момента создания elementary внешняя функция была возвращена. Таким образом, переменная staff существует только внутри замыкания и не доступна ниоткуда больше.

Мгновенно выполняемые функции (Immediate Invoked Function Expression – IIFE)

IIFE – означает функцию, которая будет выполнена сразу после объявления.

  • (Окружающие скобки) препятствуют рассмотрению описываемой IIFE-функции в качестве объявления обычной.
  • Завершающие скобки() – исполняющее выражение.
var result = [];
for (var i=0; i < 5; i++) {
  result.push( function() { return i } );
}
console.log( result[1]() ); // 5
console.log( result[3]() ); // 5
result = [];
for (var i=0; i < 5; i++) {
  (function () {
    var j = i; // copy current value of i
    result.push( function() { return j } );
  })();
}
console.log( result[1]() ); // 1
console.log( result[3]() ); // 3

При использовании IIFE:

  • Можно вкладывать закрытые данные в функцию.
  • Обновить окружение.
  • Избежать использования глобального пространства имен.

Контекст

Не путайте контекст и область видимости. Контекст определяет как функция была вызвана. К примеру, внутри контекста вызванной функции к ней можно обращаться через this. Область видимости определяет видимость переменных.

Вызов функций: call, apply и bind

Три этих метода используются, чтобы прикрепить контекст this к функции и различаются методами вызова функции.

  • .call() вызывает функцию немедленно и требует предоставить аргументы списком (один за одним).
  • .apply() вызывает функцию немедленно и позволяет передать аргументы массивом.

call() и apply() практически одинаковы и используются для вызова метода объекта. Выбор конкретного способа определяется методом передачи аргументов.

const Snow = {surename: 'Snow'}
const char = {
  surename: 'Stark',
  knows: function(arg, name) {
    console.log(`You know ${arg}, ${name} ${this.surename}`);
  }
}
char.knows('something', 'Bran');              // You know something, Bran Stark
char.knows.call(Snow, 'nothing', 'Jon');      // You know nothing, Jon Snow
char.knows.apply(Snow, ['nothing', 'Jon']);   // You know nothing, Jon Snow
  • .bind() вернет новую функцию с точным контекстом и параметрами. Обычно это требуется для вызова функции в определенный момент и с определенном контексте. Этот метод требует передавать аргументы по одному, через запятую.
const Snow = {surename: 'Snow'}
const char = {
  surename: 'Stark',
  knows: function(arg, name) {
    console.log(`You know ${arg}, ${name} ${this.surename}`);}
  }
const whoKnowsNothing = char.knows.bind(Snow, 'nothing');
whoKnowsNothing('Jon');  // You know nothing, Jon Snow

Ключевое слово this

Значение this обычно определяется контекстом выполнения функции. Контекст выполнения обычно подразумевает как функция была вызвана.

Ниже приведен список значений this, которые могут быть в зависимости от контекста.

  • Привязка через new: здесь this будет относиться к созданному объекту.
function Person(name, age) {
  this.name = name;
  this.age =age;
  console.log(this);
}
const Rachel = new Person('Rachel', 30);   // { age: 30, name: 'Rachel' }
  • Явное привязывание: при вызове call или apply this будет означать объект, отправленный в качестве аргумента.
  • Обратите внимание: .bind() работает немного иначе. Метод создает новую функцию, которая вызывает оригинальную с привязанным к ней объектом.
function fn() {
  console.log(this);
}
var agent = {id: '007'};
fn.call(agent);    // { id: '007' }
fn.apply(agent);   // { id: '007' }
var boundFn = fn.bind(agent);
boundFn();         // { id: '007' }
  • Неявное привязывание: когда функция вызывается с контекстом, this будет являться объектом, методом которого является эта функция.
var building = {
  floors: 5,
  printThis: function() {
    console.log(this);
  }
}
building.printThis();  // { floors: 5, printThis: function() {…} }
  • Привязка по умолчанию: если ни один из вышеперечисленных случаев не подходит, this будет являться глобальным объектом (в браузере – объект window). Функция, не являющаяся методом автоматически становится глобальным объектом.
function printWindow() {
  console.log(this)
}
printWindow();  // window object
  • Лексическое this: когда функция вызвана с помощью стрелочной функции =>, this получит значение окружающей области видимости во время создания.
function Cat(name) {
  this.name = name;
  console.log(this);   // { name: 'Garfield' }
  ( () => console.log(this) )();   // { name: 'Garfield' }
}
var myCat = new Cat('Garfield');

Strict Mode

JavaScript будет исполняться в «строгом» режиме при использовании директивы “use strict”.

Преимущества Strict Mode:

  • Отладка становится проще – ошибки, которые в другом случае будут проигнорированы в этом режиме будут показаны.
  • Предотвращаются случайные глобальные переменные – присвоение значения к необъявленной переменной будет возвращать ошибку.
  • Препятствует недопустимому использованию удаления – попытки удалить переменные, функции и неудаляемые свойства приведут к ошибке.
  • Предотвращается дублирование имен свойств и значений параметров – это вызовет ошибку (справедливо до ES6).
  • Использовать eval() теперь безопасней – переменные и функции, объявленные в eval не будут создаваться в локальной области видимости.
  • Передача в качестве значения this null или undefined не повлияет на глобальный объект.

Ключевое слово new

Функции, вызванные с помощью new, являются функциями-конструкторами. Последовательность действий при вызове функции через new:

  1. Создается новый объект
  2. Прототип объекта устанавливается как прототип функции-конструктора.
  3. Функция-конструктор исполняется с this, как новый объект.
  4. Возвращается созданный объект.
function myNew(constructor, ...arguments) {
  var obj = {}
  Object.setPrototypeOf(obj, constructor.prototype);
  return constructor.apply(obj, arguments) || obj
}

Прототипы и наследование

В JavaScript каждый объект имеет прототип, доступный через метод .__proto__.

Прототипы реализуют в JS механизм наследования.

var animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
  this.__proto__ = animal;
}

var rabbit = new Rabbit("Roger");

alert( rabbit.eats ); // true, так как есть в прототипе

Цепочка прототипов

Прототипы могут образовывать цепочки наследования, каждый из объектов которого будет иметь свойства предыдущего (родительского), вплоть до первого звена.

Асинхронный JavaScript

JavaScript является однопоточным языком программирования. Главный недостаток однопоточности заключается в том, что при исполнении части кода, которая занимает много времени, весь остальной код будет выполнен с задержкой.

Каждая функция в JavaScript будет выполняться в порядке очереди в стеке вызовов: первая зашедшая в стек функция будет выполнена последней, а последняя – первой.

Решением проблемы однопоточности и очередей является использование функций обратного вызова. Асинхронный процесс начинается с размещения асинхронной функции вызова в области памяти. Эту область будем считать менеджером событий.

Цикл событий обрабатывает выполнение множества частей кода с течением времени. Он  наблюдает за стеком вызовов и очередью вызовов.

Рассмотрим код ниже, чтобы понять как это работает:

const first = function () {
  console.log('First message')
}
const second = function () {
  console.log('Second message')
}
const third = function() {
  console.log('Third message')
}

first();
setTimeout(second, 0);
third();
  1. При инициализации браузерная консоль, стек вызовов и менеджер событий пусты.
  2. first() добавлена в стек вызовов.
  3. console.log("First message») добавлена в стек вызовов.
  4. console.log("First message») выполнена и выведена в консоль.
  5. console.log("First message») удалена из стека.
  6. first() удалена из стека.
  7. setTimeout(second, 0) добавлена в стек
  8. setTimeout(second, 0) выполнена и обработана менеджером событий. После 0ms менеджер событий переместит second() в очередь вызовов.
  9. setTimeout(second, 0) выполнена и удалена из стека.
  10. third() добавлена в стек.
  11. console.log("Third message») добавлена в стек.
  12. console.log("Third message») выполнена, результат выведен в консоль.
  13. console.log("Third message») удалена из стека.
  14. third() удалена из стека.
  15. Сейчас стек вызовов пуст и функция second() ждет своей очереди для попадания в стек.
  16. Цикл событий перемещает second() из очереди событий в стек.
  17. console.log("Second message») добавляется в стек.
  18. console.log("Second message») выполняется и выводит сообщение в консоль.
  19. console.log("Second message») удаляется из стека.
  20. second() удаляется из стека.

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

eFusion
01 марта 2020

ТОП-15 книг по JavaScript: от новичка до профессионала

В этом посте мы собрали переведённые на русский язык книги по JavaScript – ...
admin
10 июня 2018

Лайфхак: в какой последовательности изучать JavaScript

Огромный инструментарий JS и тонны материалов по нему. С чего начать? Расск...