Middle Level

Расскажите о пирамиде тестирования.

Пирамида тестирования - это концепция организации автоматизированных тестов, которая представляет собой модель, где тесты делятся на три уровня:

  1. Unit тесты: На этом уровне находится большинство тестов. Они тестируют отдельные функции и методы кода. Они быстрые, их много, и они управляются разработчиками.
  2. Integration тесты: Эти тесты обеспечивают тесную связь между разными модулями или слоями приложения. Их меньше, чем модульных или функциональных тестов, но они все еще важны для проверки того, что модули работают вместе, как предполагалось.
  3. End-to-End тесты: На этом уровне тесты имитируют реальное поведение пользователя, работая с системой со стороны клиента. Несмотря на их высокую ценность, они дороги в разработке и поддержке, поэтому их рекомендуется делать относительно немного.

Как запретить браузеру отдавать кэш на HTTP-запрос?

Отправка HTTP-параметра Cache-Control с директивой no-cache или no-store в ответе от сервера предложит браузеру не кешировать данный ответ.

Пример:

Cache-Control: no-cache

Cache-Control: no-store

Если вы хотите установить это на клиенте (например, в JavaScript при использовании fetch), вы можете использовать опцию cache: 'no-store'.

Пример: fetch(url, { cache: 'no-store' })...

Расскажите о паттернах Observer, Pub / Sub. Какая между ними разница?

Observer (наблюдатель) - это поведенческий паттерн проектирования, который создает механизм подписки, позволяющий одним объектам наблюдать и реагировать на события, происходящие в других объектах.

В этом паттерне существует "субъект" и множество "наблюдателей". Субъект уведомляет наблюдателей о любых изменениях своего состояния. Паттерн Наблюдатель чаще всего используется в JavaScript, когда один объект должен наблюдать за изменениями в другом объекте.

Примеры использования Observer включают в себя EventEmitter в Node.js и API событий в браузере как часть модели DOM, в которой обработчики событий (наблюдатели) могут быть зарегистрированы для событий DOM.

document.querySelector('button').addEventListener('click', () => {
  console.log('The button was clicked!');
});

Publish/Subscribe (Издатель/Подписчик), или Pub/Sub, является вариацией паттерна Observer. Он использует "топик" или "канал" как промежуточное звено между издателями и подписчиками. Этот дополнительный уровень абстракции означает, что издатели могут "публиковать" (отправлять) сообщения в определенные темы, а подписчики могут "подписываться" на прослушивание сообщений из этих тем.

Pub/Sub предлагает более гибкую организацию, поскольку издатели и подписчики не должны прямо друг друга знать. Это хорошо для расширяемости и модульности систем.

Пример Pub/Sub на клиенте - API postMessage в браузере, которое позволяет безопасно проводить асинхронное, междоменное общение. Более общий пример - работы с WebSocket, системы управления очередями сообщений типа RabbitMQ и MQTT.

// Издатель
window.postMessage('Hello World!', 'http://example.org');
// Подписчик
window.addEventListener('message', event => {
  console.log(event.data); // logs 'Hello World!'
});

Существуют и другие JavaScript-библиотеки, такие как Redux и RxJS, которые используют эти паттерны в своих основных архитектурах.

С какой целью может быть использован event listener события fetch self.addEventListener ( 'fetch', event => )?

Этот прослушиватель события fetch может использоваться в сервис-воркерах (Service Workers) для перехвата и обслуживания HTTP-запросов, отправленных вашим веб-приложением.

Сервис-воркеры могут применяться для различных целей:

  1. Офлайн кэширование: Сервис-воркеры могут кэшировать ответы на некоторые или все сетевые запросы, что позволяет приложению работать офлайн или на недостаточно стабильном соединении. Так при запросе ресурса сначала смотрится наличие этого ресурса в кэше, и если ресурс найден, то он возвращается из кэша.
  2. Перехват запросов: C помощью fetch, сервис-воркер может изменять запросы или ответы. Это может быть полезно для преобразования запросов, вставки специфических заголовков или выполнения других операций на запрос/ответ.
  3. Push Notifications: Сервис-воркеры могут прослушивать push-события из бэкенд-сервисов и даже активировать показ уведомлений при закрытом приложении.
  4. Background sync: Сервис-воркер на основе событий может обслуживать запросы в фоновом режиме или запускать синхронизацию данных при появлении интернет-соединения.

Пример использования fetch в сервис-воркере:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        if (response) {
          // возвращаем из кэша, если найдено
          return response;
        }
        // если нет в кэше, делаем запрос на сервер
        return fetch(event.request);
      })
  );
});

Что такое Event loop?

Движок браузера выполняет JavaScript в одном потоке. Для потока выделяется область памяти — стэк, где хранятся фреймы (аргументы, локальные переменные) вызываемых функций.

Список событий, подлежащих обработке формируют очередь событий. Когда стек освобождается, движок может обрабатывать событие из очереди. Координирование этого процесса и происходит в event loop.

Это по сути бесконечный цикл, в котором выполняются многочисленные обработчики событий. Если очередь пустая — движок браузера ждет, когда поступит событие. Если непустая — первое в ней событие извлекается и его обработчик начинает выполняться. И так до бесконечности.

function clicked() {
	console.log('clicked')
}
 
console.log('start')
console.log('start2')
  1. У нас в Web Apis региструется наша функция и хранится там
  2. в Call Stack у нас попадет 2 консоль лога, start, start2
  3. Мы можем нажать на кнопку, чтоб сработал clicked, и это событие попадет в callback queue, или очередь, откуда это событие выполнится только если стэк будет пустым

Micro и Macro tasks

Есть микротаски, есть макротаски. EventLoop берет в порядке важности эти задачи, промисы это микротаски, таймауты это макротаски. В приоритете микротаски, они выполняются все, потом уже идут МАКРОтаски

Что такое МИКРОтаски?

  • промисы
  • queueMicrotask
  • mutationObserver (метод для слежки за нодами в DOM

Что такое МАКРОтаски?

  • таймеры (timeout, interval)
  • события (клик, например, загрузка картинок)
  • браузерные штуки (рендер, input/ouput, короче все от копота браузера)

Какие приоритеты у микро и макро тасок?

Выполнит сначала все микротаски, и если у нас очередь микротасок пуста, то берется ОДНА задача из макротасок

Как работает boxing / unboxing в JavaScript?

Boxing заключается в оборачивании примитивного типа данных в соответствующий объект-обертку, пример оборачивания строки:

let str = "Hello, world";
let boxedStr = new String(str); // <String: "Hello, world">

Unboxing - это процесс преобразования объекта обертки обратно в его соответствующий примитивный тип. JavaScript автоматически производит unboxing, когда методы примитивного типа вызываются на объектах-обертках.

let boxedStr = new String("Hello, world");
let unboxedStr = boxedStr.valueOf(); // "Hello, world"

В чем разница между оператором in и методом hasOwnProperty?

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

let obj = { property: 'value' };
 
console.log('property' in obj); // true
console.log(obj.hasOwnProperty('property')); // true
 
let obj2 = Object.create({ prototypeProperty: 'value' });
 
console.log('prototypeProperty' in obj2); // true
console.log(obj2.hasOwnProperty('prototypeProperty')); // false

с помощью чего в JS реализуются ООП-парадигмы?

Инкапсуляция: Это один из основных принципов ООП, который прячет внутренние детали реализации и показывает только то, что необходимо. Это можно выполнить с помощью функций-конструкторов, прототипного наследования, классов (ES6+) и замыканий.

class Circle {
  constructor(radius) {
    this.radius = radius; // публичное свойство
    let defaultLocation = { x: 0, y: 0 }; // приватное свойство
    this.getDefaultLocation = function() { // приватный метод
      return defaultLocation;
    }
  }
 
  draw() {
    console.log('draw');
  }
}
 
const circle = new Circle(10);
 

Полиморфизм: Полиморфизм - это способность объекта использовать методы производного класса, которые ему были унаследованы или переопределены. Это можно выполнить с помощью прототипного наследования или классов ES6+.

class Animal {
  makeSound() {
    // ...
  }
}
 
class Dog extends Animal {
  makeSound() {
    console.log('Bark');
  }
}
 
class Cat extends Animal {
  makeSound() {
    console.log('Meow');
  }
}
 
let pet = new Dog();
pet.makeSound(); // Bark
pet = new Cat();
pet.makeSound(); // Meow

Абстракция: Абстракция скрывает сложные детали и показывает только важную информацию. Эта концепция также используется для создания абстрактных классов или методов, которые должны быть реализованы каждым похожим классом. В JavaScript абстрактные классы можно создать с помощью классов ES6+.

class AbstractClass {
  constructor() {
    if (new.target === AbstractClass) {
      throw new TypeError("Cannot construct AbstractClass instances directly");
    }
  }
 
  abstractMethod() {
    throw new Error("You have to implement the method doSomething!");
  }
}
 
class ConcreteClass extends AbstractClass {
  abstractMethod() {
    console.log("Implements abstract method!");
  }
}

Наследование - объекты могут наследовать свойства и методы от прототипных объектов. В JavaScript это можно сделать через прототипное наследование:

Пример наследования на объектах:

В JavaScript наследование между объектами основано на прототипах. Краткий пример:

let animal = {
  eats: true,
  walk() {
    console.log('Animal walk');
  }
};
 
let dog = Object.create(animal); // dog refers to animal as its prototype
dog.barks = true;
dog.walk(); // output: 'Animal walk'
console.log(dog.eats); // output: true

В этом примере объект dog инстанцируется с использованием Object.create(animal), что создает новый объект dog, имеющий animal в качестве своего прототипа. Поэтому dog унаследовал свойства и методы animal.

С появлением ES6 в JavaScript ввели ключевое слово class для определения классов. Ключевое слово extends используется для того, чтобы сделать наследование классов более явным и более простым для понимания:

class Animal {
  constructor() {
    this.eats = true;
  }
 
  walk() {
    console.log('Animal walk');
  }
}
 
class Dog extends Animal {
  bark() {
    console.log('Barks');
  }
}
 
let dog = new Dog();
dog.walk(); // output: 'Animal walk'
console.log(dog.eats); // output: true
dog.bark(); // output: 'Barks'

В этом примере Dog является подклассом (или производным классом) Animal и наследует все его свойства и методы. Кроме того, Dog добавляет свой метод - bark().

Какая разница между композицией и наследованием?

Наследование

class Vehicle {
  constructor(name) {
    this.name = name;
  }
 
  drive() {
    return `${this.name} drives forward`;
  }
}
 
class Car extends Vehicle {
  beep() {
    return `${this.name} beeps`;
  }
}
 
let myCar = new Car("Thunder");
console.log(myCar.drive()); // "Thunder drives forward"
console.log(myCar.beep());  // "Thunder beeps"

В этом примере класс Car наследует от класса Vehicle. Он наследует метод drive и добавляет новый метод beep.

Композиция

const canDrive = {
  drive() {
    return `${this.name} drives forward`;
  }
};
 
const canBeep = {
  beep() {
    return `${this.name} beeps`;
  }
};
 
function createCar(name) {
  let car = Object.assign({name: name}, canDrive, canBeep);
  return car;
}
 
let myCar = createCar("Thunder");
console.log(myCar.drive()); // "Thunder drives forward"
console.log(myCar.beep());  // "Thunder beeps"

В этом примере вместо того, чтобы наследовать свойства и методы от класса Vehicle, мы создаем объект myCar, который состоит из отдельных объектов, содержащих особенности или функции. Здесь у нас есть canDrive и canBeep — объекты, отвечающие за определенные функциональности. Мы соединяем их вместе, чтобы создать myCar. Если когда-то мы захотим изменить поведение myCar, мы можем просто добавить или удалить компоненты.

Почему не стоит использовать конструкторы примитивов?

Примитивные типы данных в JavaScript, такие как String, Number, Boolean, имеют соответствующие объектные обертки - String(), Number(), Boolean(), которые экземплярируются с использованием ключевого слова new. Использование таких конструкторов может привести к непредвиденным ошибкам и путанице, поскольку они создают объект, а не примитивное значение.

let primStr = "I am a primitive string";
let objStr = new String("I am an object string");
 
console.log(typeof primStr); // "string"
console.log(typeof objStr);  // "object"

Такой код может делать некоторые проверки усложненными и непредсказуемыми:

if (primStr === "I am a primitive string") {
  console.log('Works fine'); // 'Works fine'
}
 
if (objStr === "I am an object string") {
  console.log('Works fine');
} else {
  console.log('Doesn\'t work'); // 'Doesn't work'
}

Здесь ветка if (objStr === "I am an object string") не выполняется, потому что objStr это объект, а не примитивная строка. Это может вызвать путаницу и ошибки. Это одна из причин избегать использования new String, new Number и new Boolean.

Какие различия в поведении ES5 функции-конструктора и ES2015 класса?

В ECMAScript (ES) 5 и ES2015 (или ES6), функции и классы можно использовать для создания объектов и организации кода, но есть некоторые ключевые различия в применении и функциональности.

1. Конструкторы и классы: ES5 использует функции-конструкторы для создания объектов.

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}

В ES6 для создания объектов вводятся классы.

class Car {
    constructor(make, model, year) {
      this.make = make;
      this.model = model;
      this.year = year;
    }
  }

Классы ES6 обеспечивают более чистый и удобный синтаксис для создания конструкторов и организации кода.

2. Наследование: В ES5 прототипное наследование осуществляется с помощью комбинации функций конструктора и Object.create().

function Sedan(make, model, year) {
  Car.call(this, make, model, year);
  this.type = "Sedan";
}
 
Sedan.prototype = Object.create(Car.prototype);

В ES6 вводится ключевое слово extends для упрощения наследования.

class Sedan extends Car {
  constructor(make, model, year) {
    super(make, model, year);
    this.type = "Sedan";
  }
}

3. Строгая проверка: Классы ES6 всегда работают в строгом режиме("strict mode"), в то время как в ES5 строгий режим нужно указывать явно.

4. Hoisting: Функции-конструкторы в ES5 подвергаются "hoisting" (перемещение объявления функции в начало области видимости), в то время как классы ES6 такой возможности не предоставляют, они не "hoisted".

5. Методы: В ES5 в прототип добавляются методы после определения функции-конструктора. В ES6 методы можно добавить непосредственно в тело класса.

ES5:

Car.prototype.getAge = function() {
  const currentYear = new Date().getFullYear();
  return currentYear - this.year;
};

ES6:

class Car {
  //...
  getAge() {
    const currentYear = new Date().getFullYear();
    return currentYear - this.year;
  }
}

6. Статические методы: Ввод статических методов в ES6 классах становится более прямым, в отличие от ES5, где требуется добавление методов к самому конструктору.

ES5:

Car.getCompany = function() {
  return 'Tesla';
};

ES6:

class Car {
  // ...
  static getCompany() {
    return 'Tesla';
  }
}

Таким образом, основное различие между ES5 функциями-конструкторами и ES6 классами заключается в улучшенном синтаксисе и возможностях последних, которые позволяют более легко и понятно создавать объекты и реализовывать наследование.