45 вопросов по JavaScript с собеседований | В паутине

45 вопросов по JavaScript с собеседований

Основы языка

Значения и типы

Значения (values) в JavaScript имеют типы, а переменные, в которых лежат эти значения - не имеют.
Встроенные типы данных:

  • string - строки
  • number - числа
  • boolean - логические значения
  • null и undefined
  • object - объекты
  • symbol - символы, появились в ES6

Оператор typeof

Оператор typeof получает некоторое значение и определяет его тип:

let a;
typeof a; // "undefined"

a = "hello world";
typeof a; // "string"

a = 42;
typeof a; // "number"

a = true;
typeof a; // "boolean"

a = null;
typeof a; // "object" - известный баг JS

a = undefined;
typeof a;// "undefined"

a = { b: "c" };
typeof a; // "object"

Приведение типов (coercion)

Конвертация встроенных типов данных друг в друга в 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() { } (любая функция)

Равенство (equality)

В JavaScript есть два вида сравнения:

var a = "42";
var b = 42;

a == b; // true - абстрактное, строка привелась к числу
a === b; // false - строгое

Сравнение осуществляется по некоторым простым правилам:

  • Если значения (стороны сравнения) могут принимать значения true или false, избегайте нестрогого сравнения и используйте ===;
  • Если в сравнении участвуют специфические значения (0, "", [] - пустой массив), избегайте нестрогого сравнения.
  • В остальных случаях можно использовать ==. Это безопасно и во многих случаях делает код более читаемым.

null и undefined

JavaScript (и TypeScript) имеют два специальных типа данных - null и undefined. Они предназначены для обозначения разных вещей (концепций):

  • undefined - значение не инициализировано
  • null - значение в данный момент недоступно

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

Переменные, объявленные с помощью ключевого слова let, имеют блочную область видимости. Это значит, что они недоступны снаружи блока {...}

Строгий режим (strict mode) и конструкция "use strict"

Строгий режим - фича стандарта ECMAScript 5. Размещая строку "use strict" в начале программы или функции, вы даете указание интерпретатору исполнять код в "строгом" контексте.

// не строгий режим

(function(){
  "use strict";

  // строгий режим
})();

// не строгий режим

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

function doSomething(val) {
  "use strict"; 
  x = val + 10; // ошибка x is not defined
}

function doSomething(val) {
  "use strict"; 
  let x = val + 10; // нет ошибки
}

Полифиллы и шимы (shim)

Шим - это любой фрагмент кода, который перехватывает обращение к API и добавляет уровень абстракции в приложение. Шимы существуют не только в вебе.

Полифилл - это шим для веба и браузерных API - специальный код (или плагин), который позволяет добавить некоторую функциональность в среду, которая эту функциональность по умолчанию не поддерживает (например, добавить новые функции JS в старые браузеры). Полифиллы не являются частью стандарта HTML5.

Транспилирование

Транспилирование, транспиляция - преобразование кода, написанного в новом стандарте в его эквивалент в старом стиле (ES6 в ES5).

Разница между ES5 и ES6

  • ECMAScript 5 (ES5) - 5-е издание ECMAScript, стандартизированное в 2009 году. Поддерживается современными браузерами практически полностью.
  • ECMAScript 6 (ECMAScript 2016, ES6) - 6-е издание ECMAScript, стандартизированное в 2015 году. Частично поддерживается большинством современных браузеров.

Несколько ключевых отличий двух стандартов:

  • Стрелочные функции и интерполяция в строках.
    const greetings = (name) => {
          return `hello ${name}`;
    }

    и даже так:

    const greetings = name => `hello ${name}`;
  • Ключевое слово const.Константы в JavaScript отличаются от констант в других языках программирования. Они сохраняют неизменной только ссылку на значение. Таким образом, вы можете добавлять, удалять и изменять свойства объявленного константным объекта, но не можете перезаписать текущую переменную, в которой лежит этот объект.
    const NAMES = [];
    NAMES.push("Jim");
    console.log(NAMES.length === 1); // true
    NAMES = ["Steve", "John"]; // error
  • Блочная видимость.Переменные, объявленные с помощью новых ключевых слов let и const имеют блочную область видимости, то есть недоступны за пределами {}-блоков. Кроме того, они не поднимаются, как var-переменные.
  • Параметры по умолчанию.Теперь функцию можно инициализировать с дефолтным значением параметров. Оно будет использовано, если параметр не будет передан при вызове.
    // Basic syntax
    function multiply (a, b = 2) {
         return a * b;
    }
    multiply(5); // 10
  • Классы и наследование.Новый стандарт ввел в язык поддержку привычного синтаксиса классов (class), конструкторы (constructor) и ключевое слово extend для оформления наследования.
  • Оператор for-of для перебора итерируемых объектов в цикле.
  • spread-оператор, который удобно использовать для слияния объектов и еще во многих случаях.
    const obj1 = { a: 1, b: 2 }
    const obj2 = { a: 2, c: 3, d: 4}
    const obj3 = {...obj1, ...obj2}
  • Обещания (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')
     })
  • Модули.Способ разбития кода на отдельные модули, которые можно импортировать при необходимости.
    import myModule from './myModule';

    Синтаксис экспорта позволяет выделить функциональность модуля:

    const myModule = { x: 1, y: () => { console.log('This is ES5') }}
    export default myModule;

Примитивные значения

Является ли число целым (integer)?

Очень простой способ проверить, имеет ли число дробную часть - разделить его на единицу и проверить остаток.

function isInt(num) {
  return num % 1 === 0;
}

console.log(isInt(4)); // true
console.log(isInt(12.2)); // false
console.log(isInt(0.3)); // false

Почему 0.1 + 0.2 === 0.3 - это false?

Действительно, в JavaScript 0.1 + 0.2 на самом деле равно 0.30000000000000004. Дело в том, что все числа в языке (даже целые) представлены в формате с плавающей запятой (float). В двоичной системе счисления эти числа - бесконечные дроби. Для их хранения выделяется ограниченный объем памяти, поэтому возникают подобные неточности.

Рекурсивная функция, возвращающая двоичное представление числа

Например, получив на вход число 4, эта функция должна вернуть 100.

decimalToBinary(3); // 11
decimalToBinary(8); // 1000
decimalToBinary(1000); // 1111101000

function decimalToBinary(digit) {
  // последовательно получаем каждый разряд двоичного числа
  if(digit >= 1) {
    // если число не делится на 2 без остатка, 
    // прибавляем к результату справа единицу (младший разряд)
    // если делится, прибавляем 0
    // делим число на 2 и продолжаем рекурсию

    if (digit % 2) {
      return decimalToBinary((digit - 1) / 2) + 1;
    } else {
      // Recursively return proceeding binary digits
      return decimalToBinary(digit / 2) + 0;
    }
  } else {
    // выход из рекурсии, если число меньше 1
    return '';
  }
}

Является ли число степенью двойки?

Обратите внимание, что 0 не является степенью двойки.
Решение очень простое, основано на побитовом умножении (И, &). Этот оператор ставит 1 на бит результата, для которого соответствующие биты операндов равны 1.
Возьмем для примера 4 & 3. Двоичное представление четверки - 100, тройки - 011. Ни в одном бите операнды не совпадают, поэтому результат будет 000.
Другой пример: 5 & 4. В двоичном виде это 101 & 100 = 100.

Суть решения в том, что любая степень двойки - это единственная единица и нули (2 -10, 4 - 100, 8 - 1000). Если вычесть из него единицу, полученное число будет состоять из одних единиц (1 - 1, 3 - 11, 7 - 111). То есть у самого числа (степень двойки) и этого же числа, уменьшенного на 1 все биты разные, значит, побитовое умножение в результате даст 0.

isPowerOfTwo(4); // true
isPowerOfTwo(64); // true
isPowerOfTwo(1); // true
isPowerOfTwo(0); // false
isPowerOfTwo(-1); // false

function isPowerOfTwo(number) {
  return number & (number - 1) === 0;
}

Развернуть все слова в полученной строке

Решим задачу с помощью одной простой функции в два этапа: сначала развернем все предложение целиком, затем вернем исходный порядок слов.

let string = "Welcome to this Javascript Guide!";

// развернуть все предложение целиком
// вывод: !ediuG tpircsavaJ siht ot emocleW
let reverseEntireSentence = reverseBySeparator(string, "");

// сохранить исходный порядок слов
// вывод: emocleW ot siht tpircsavaJ !ediuG
let reverseEachWord = reverseBySeparator(reverseEntireSentence, " ");

function reverseBySeparator(string, separator) {
  return string.split(separator).reverse().join(separator);
}

Строки-анаграммы

Определить, являются ли две строки анаграммами друг друга.

let firstWord = "Mary";
let secondWord = "Army";

isAnagram(firstWord, secondWord); // true

function isAnagram(first, second) {
  // сначала приводим обе строки к нижнему регистру
  let a = first.toLowerCase();
  let b = second.toLowerCase();

  // разбиваем строку по символам, сортируем их и снова объединяем в строку
  // результаты сравниваем
  a = a.split("").sort().join("");
  b = b.split("").sort().join("");

  return a === b;
}

Строки-палиндромы

Палиндром - это слово, фраза, число или другая последовательность символов, которая читается одинаково слева направо и справа налево. Важно учитывать, что в строке могут быть пробелы и символы в разном регистре.

isPalindrome("racecar"); // true
isPalindrome("race Car"); // true

function isPalindrome(word) {
  // убрать все пробельные символы, привести к нижнему регистру
  let lettersOnly = word.toLowerCase().replace(/\s/g, "");

  // сравнить с перевернутой версией
  return lettersOnly === lettersOnly.split("").reverse().join("");
}

А вот этот вариант работает в 25 раз быстрее (если, конечно, вы сможете в нем разобраться):

function isPalindrome(s,i) {
   return (i=i||0)<0||i>=s.length>>1||s[i]==s[s.length-1-i]&&isPalindrome(s,++i);
}

Изоморфные строки

Изоморфными называются строки, между символами которых можно установить однозначное соответствие. Например, paper и title изоморфны (p = t, a = i, e = l, r = e). Количество и порядок символов при этом необходимо учитывать.

  1. egg и sad - неизоморфны,
  2. dgg и add - изоморфны.
isIsomorphic("egg", 'add'); // true
isIsomorphic("paper", 'title'); // true
isIsomorphic("kick", 'side'); // false

function isIsomorphic(firstString, secondString) {

  // проверка длины строк
  if (firstString.length !== secondString.length) return false

  let letterMap = {};

  for (let i = 0; i < firstString.length; i++) {
    let letterA = firstString[i],
        letterB = secondString[i];

    // если такой буквы еще не было, сохранить ее в объекте
    // и поставить в соответствие с соответствующей буквой второго слова
    if (letterMap[letterA] === undefined) {
      letterMap[letterA] = letterB;
    } else if (letterMap[letterA] !== letterB) {
      // если соответствие для буквы уже установлено
      // и буква второго слова не соответствует
      // то строки не изоморфны
      return false;
    }
  }

  return true;
}

Объекты и массивы

Объекты

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

let obj = {
  a: "hello world", // свойство a со значением "hello world"
  b: 42,
  c: true
};

obj.a;		// "hello world", получение через точку (doted-notation)
obj.b;		// 42
obj.c;		// true

obj["a"];	// "hello world", получение через скобки (bracket-notation)
obj["b"];	// 42
obj["c"];	// true

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

let obj = {
  a: "hello world",
  b: 42
};

let b = "a";

obj[b];			// "hello world"
obj["b"];		// 42

Сравнение объектов

Непримитивные значения в JavaScript (объекты) передаются по ссылке, а операторы сравнения (== и ===) сравнивают именно ссылки. Поэтому два разных объекта с идентичными свойствами не будут равны друг другу.

let a = [1, 2, 3];
let b = [1, 2, 3];

a == b // false
a === b // false

Есть одна маленькая хитрость, которой можно воспользоваться для сравнения простых структур. Если сравнить объект с примитивом (например, строкой), он будет приведет к строковому представлению. Для массива, например, строковое представление - это строка со списком элементов, разделенных запятыми. Благодаря этому можно сделать так:

let a = [1, 2, 3];
let b = [1, 2, 3];
let c = "1,2,3";

a == c // true
a.toString() == b.toString() // true

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

Вместо приведения к строке также можно использовать сериализацию (JSON.stringify(obj)), но этот способ тоже работает не во всех случаях.

Для глубокого сравнения нужно использовать специальные библиотеки, например, deep-equal, или рекурсивный алгоритм.

Вот здесь можно увидеть реализацию более академичного способа сравнения объектов.

Массивы

Массивы - это объекты, которые хранят значения не в именованных свойствах, а в нумерованных.

let arr = [
  "hello world",
  42,
  true
];

arr[0];	 // "hello world"
arr[1];	 // 42
arr[2];	 // true
arr.length; // 3

typeof arr; // "object"

Как очистить массив?

Способ 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();
}

Довольно громоздко, но может быть полезно, если вы хотите перед удалением что-нибудь еще сделать с элементами.

Является ли объект массивом?

Лучший способ узнать, является ли объект инстансом определенного класса, - использовать метод Object.prototype.toString().

let arr = [1, 2, 3];
let obj = {'a': 1, 'b': 2};
let str = "hello";
let foo = function() {};

console.log(Object.prototype.toString.call(arr)); // [object Array]
console.log(Object.prototype.toString.call(obj)); // [object Object]
console.log(Object.prototype.toString.call(str)); // [object String]
console.log(Object.prototype.toString.call(foo)); // [object Function]

Таким образом, проверить, является ли полученное значение массивом можно с помощью конструкции:

function checkArray(value) {
  if(Object.prototype.toString.call(value) === '[object Array]') {
    console.log('Array!');
  } else {
    console.log('Not array');
  }
}

В jQuery есть специальный метод $.isArray(value), который под капотом использует ту же самую проверку.

Современные браузеры (Chrome 5, Firefox 4.0, IE 9, Opera 10.5 and Safari 5) поддерживают метод Array.isArray(value).

Зачем вообще проверять, является ли значение массивом? Это очень полезно для перегрузки методов в зависимости от полученных параметров. Например, вы можете работать в одном и том же методе и с отдельной строкой, и с массивом строк. В каждом конкретном случае проверка на тип параметра может быть разной. Например, в приведенном примере проще будет воспользоваться оператором typeof:

function foo(param) {
  if(typeof param === 'string') {}
  else {}
}

Но проверка на массив тоже может потребоваться, ведь в функцию foo может быть передан, например, обычный объект, с которым она не умеет работать.

В массиве чисел найти максимальную разницу между двумя элементами, причем большее число должно обязательно стоять после меньшего

Обязательно разберитесь в условии прежде чем приступать к решению.

Если у нас есть массив:

let array = [7, 8, 4, 9, 9, 15, 3, 1, 10];

то в ответе ожидается значение 11 (15 - 5 = 11), но не 14 (15 - 1 = 14), так как большее число должно находиться после меньшего.

Решение выглядит так:

findLargestDifference(array);

function findLargestDifference(array) {
  // если в массиве всего один элемент, возвращаем -1
  if (array.length <= 1) return -1;

  // в currentMin сохраняем текущее минимальное значение
  let currentMin = array[0];
  let currentMaxDifference = 0;

  // перебираем массив и сохраняем максимальную разницу в currentMaxDifference
  // параллельно отслеживаем минимальный элемент

  for (var i = 1; i < array.length; i++) {
    if (array[i] > currentMin && (array[i] - currentMin > currentMaxDifference)) {
      currentMaxDifference = array[i] - currentMin;
    } else if (array[i] <= currentMin) {
      currentMin = array[i];
    }
  }

  // If negative or 0, there is no largest difference
  if (currentMaxDifference <= 0) return -1;

  return currentMaxDifference;
}

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

Известно, что в неотсортированном массиве содержится (n-1) из n последовательных чисел (границы этого диапазона известны). Найдите пропущенное число за время O(n)

Тут важно разобраться в условии задачи. Если что-то непонятно, не стесняйтесь спрашивать.

У вас есть диапазон чисел от num1 до num2 включительно (обе границы известны). Длина диапазона - n.
Также есть массив длины n-1, в котором содержатся все числа из диапазона, кроме одного. Вам нужно найти это единственное пропущенное число.

Так как лишних чисел в массиве нет, недостающее можно найти, подсчитав реальную и полную суммы элементов.

// ожидаемый ответ функции - 8

// исходный неотсортированный массив
var arrayOfIntegers = [2, 5, 1, 4, 9, 6, 3, 7];
var upperBound = 9; // верхняя граница диапазона
var lowerBound = 1; // нижняя граница диапазона

findMissingNumber(arrayOfIntegers, upperBound, lowerBound); // 8

function findMissingNumber(arrayOfIntegers, upperBound, lowerBound) {

  // находим сумму всех элементов массива
  let sumOfIntegers = 0;
  for (var i = 0; i < arrayOfIntegers.length; i++) {
    sumOfIntegers += arrayOfIntegers[i];
  }

  // Рассчитываем сумму всех чисел в заданном диапазоне
  // используем формулу Гаусса для нахождения суммы всех чисел от 1 до заданного
  // n * (n+1) : 2

  let upperLimitSum = (upperBound * (upperBound + 1)) / 2;
  let lowerLimitSum = (lowerBound * (lowerBound - 1)) / 2;

  let theoreticalSum = upperLimitSum - lowerLimitSum;

  return theoreticalSum - sumOfIntegers;
}

Время работы функции составляет O(n) так как каждый элемент перебирается всего 1 раз.

Удалить из массива повторяющиеся значения, вернуть только уникальные элементы

Эту задачу очень просто решить, используя возможности стандарта ES6 (сеты - коллекции с уникальными элементами):

let array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]

Но и на ES5 ее вполне можно решить (и даже нужно!). Для этого используем обычный объект - и устанавливаем ему свойства с именами, равными элементам массива.

let array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

uniqueArray(array); // [1, 2, 3, 5, 9, 8]

function uniqueArray(array) {
  let hashmap = {};
  let unique = [];

  for(var i = 0; i < array.length; i++) {
    // если в объекта еще нет ключа, равного значению элемента, добавляем
    if(!hashmap.hasOwnProperty(array[i])) {
      hashmap[array[i]] = 1;
      unique.push(array[i]);
    }
  }

  return unique;
}

Реализуйте добавление в очередь и удаление из очереди с помощью двух стеков

Очереди элементов работают по принципу первый пришел - первый ушел.
Если мы добавляем элементы очереди в массив, чтобы получить самый первый, нужно воспользоваться методом unshift, но это довольно тяжелая операция, так как она сдвигает индексы всех оставшихся элементов массива.

В то же время существует более легкий метод pop, который берет последний элемент массива и не меняет порядок индексации.

Если у нас есть два массива, можно сделать так:

var inputStack = []; // первый стек
var outputStack = []; // второй стек

// добавление в очередь
// просто добавляем элемент в первый стек
function enqueue(stackInput, item) {
  return stackInput.push(item);
}

// удаление из очереди
function dequeue(stackInput, stackOutput) {

  // развернем стек входящих элементов (input)
  // в output-стеке все элементы окажутся в обратном порядке
  // первый элемент в input-стеке (первый пришедший) станет последним в output-стеке
  // так его проще будет получить, не нарушая нумерацию элементов 

  if (stackOutput.length <= 0) {
    while(stackInput.length > 0) {
      let elementToOutput = stackInput.pop(); // последний из входящих
      stackOutput.push(elementToOutput);
    }
  }

  return stackOutput.pop(); // вернуть последний элемент из output-стека
}

В stackInput элементы добавляются как в обычную очередь. Если нужно получить первый элемент, мы переворачиваем исходную очередь, там что первый элемент в stackInput становится последним в stackOutput. Забрать последний элемент из stackOutput (который, на самом деле, первый в очереди), можно с помощью метода pop.

Обратите внимание, пока в stackOutput есть хоть один элемент, мы ничего не переносим из исходной очереди, чтобы не нарушать порядок. Элементы там могут накапливаться и дальше. Когда output-стек обнулится, мы вновь скопируем в него накопившиеся элементы.

Больше методов массивов в JS вы можете найти здесь.

Найти пересечение массивов

Пересечение - это набор элементов, которые присутствуют в обоих массивах. При этом необходимо отбирать только уникальные элементы.

let firstArray = [2, 2, 4, 1];
let secondArray = [1, 2, 0, 2];

intersection(firstArray, secondArray); // [2, 1]

function intersection(firstArray, secondArray) {

  let hashmap = {};
  let intersectionArray = [];

  firstArray.forEach(function(element) {
    hashmap[element] = 1;
  });

  secondArray.forEach(function(element) {
    if (hashmap[element] === 1) {
      intersectionArray.push(element);
      hashmap[element]++;
    }
  });

  return intersectionArray;
}

Сначала создаем хеш, ключами которого являются значения первого массива. Операция поиска по ключу в хеше имеет сложность O(1).
Затем перебираем элементы второго массива и проверяем, есть ли они в первом.
Общая сложность алгоритма - O(n).

В неотсортированном массиве целых чисел найти наибольшее произведение любых трех элементов

Помните, что максимальным необязательно окажется произведение трех самых больших чисел в массиве. Если минимальные элементы отрицательны, но велики по модулю, возможно искомым окажется произведение min1 * min2 * max1 (двум самых маленьких и одного самого большого).

let unsortedArray = [-10, 7, 29, 30, 5, -10, -70];
computeProduct(unsortedArray); // 21000 (-70 * -10 * 30)

function sortIntegers(a, b) {
  return a - b;
}

function computeProduct(unsorted) {
   // сортируем массив по возрастанию
   let sortedArray = unsorted.sort(sortIntegers),
       product1 = 1, // max1 * max2 * max3
       product2 = 1, // min1 * min2 * max1
       array_n_element = sortedArray.length - 1; // количество элементов

   // находим произведение трех самых больших элементов
   for (var x = array_n_element; x > array_n_element - 3; x--) {
       product1 = product1 * sortedArray[x];
   }

   // находим произведение двух меньших и самого большого элементов
   product2 = sortedArray[0] * sortedArray[1] * sortedArray[array_n_element];

   if (product1 > product2) return product1;

   return product2;
}

Вкратце: бинарный (двоичный) поиск делит отсортированный массив на половины. Подробнее - в Википедии.

function recursiveBinarySearch(array, value, leftPosition, rightPosition) {
  if (leftPosition > rightPosition) return -1;

  var middlePivot = Math.floor((leftPosition + rightPosition) / 2);
  if (array[middlePivot] === value) {
    return middlePivot;
  } else if (array[middlePivot] > value) {
    return recursiveBinarySearch(array, value, leftPosition, middlePivot - 1);
  } else {
    return recursiveBinarySearch(array, value, middlePivot + 1, rightPosition);
  }
}

Сбалансированность скобок

let expression = "{{}}{}{}"
let expressionFalse = "{}{{}";

isBalanced(expression); // true
isBalanced(expressionFalse); // false
isBalanced(""); // true

function isBalanced(expression) {
  let checkString = expression;
  let stack = [];

  // если строка пуста, то считаем скобки сбалансированными
  if (checkString.length <= 0) return true;

  for (let i = 0; i < checkString.length; i++) {
    if(checkString[i] === '{') {
      stack.push(checkString[i]);
    } else if (checkString[i] === '}') {
      // Pop on an empty array is undefined
      if (stack.length > 0) {
        stack.pop();
      } else {
        return false;
      }
    }
  }

  // If the array is not empty, it is not balanced
  if (stack.pop()) return false;
  return true;
}

Функции и классы

Функция обратного вызова (callback) с простым примером

Коллбэк - это функция, которая передается в другую функцию как аргумент и выполняется после того, как закончатся какие-то другие операции. В примере ниже коллбэк логирует в консоль.

function modifyArray(arr, callback) {
  // некоторые операции с массивом arr
  arr.push(100);
  // после того, как операции закончены, выполняется коллбэк
  callback();
}

let arr = [1, 2, 3, 4, 5];

modifyArray(arr, function() {
  console.log("массив модифицирован", arr);
});

Область видимости (scope)

Каждая функция в JS имеет собственную область видимости (scope). Это коллекция переменных и правила доступа к ним. Если переменная объявлена внутри функции (в ее скоупе), то доступ к ней может получить только код внутри этой функции.

Области видимости могут быть вложены друг в друга, при этом все внутренние скоупы имеют доступ к переменным из внешних.

Замыкания (closures)

В C и большинстве других распространенных языков после возврата функции (оператор return или просто окончание работы) все локальные переменные становятся недоступными, так как фрейм стека уничтожается. В JavaScript они остаются в некотором смысле доступными.

Замыкание - это фрейм стека, который выделяется, когда функция начинает свое работу, и не освобождается после ее возврата (как если бы он был выделен в куче, а не в стеке!). Вы можете думать о переменной одновременно как об указателе на функцию, так и как о скрытом указателе на замыкание.

Говоря простыми словами, замыкание - это функция, созданная и возвращенная из другой функции, которая имеет доступ к родительскому скоупу.

Счетчик с помощью замыкания (closure)

Если мы создаем счетчик, то, скорее всего, не хотим, чтобы внешний код мог повлиять на накопленное значение. Значит, его нужно каким-то образом защитить. Замыкания отлично справляются с этой задачей.

function counter() {
  let _counter = 0; // переменная недоступна извне функции

  // вернем объект с полезными методами счетчика
  // которые смогут увеличивать приватную переменную _counter 
  // и возвращать текущее значение
  return {
    add: function(increment) { _counter += increment; },
    retrieve: function() { return 'The counter is currently at: ' + _counter; }
  }
}

// ошибка: нельзя получить доступ к переменной внутри замыкания
// _counter;

// создаем и увеличиваем счетчик
let c = counter();
c.add(5); 
c.add(9); 

// получаем текущее значение
c.retrieve(); // => The counter is currently at: 14

Каррирование (закрепление аргумента)

Каррирование - преобразование функции от многих аргументов в набор функций, каждая из которых является функцией от одного аргумента. (с) Википедия.

Пример задачи. Напишите функцию createBase, которая будет фиксировать первое слагаемое и добавлять к нему любое число:

var addSix = createBase(6);
addSix(10); // 16
addSix(21); // 27

Решим задачу с помощью замыкания:

function createBase(baseNumber) {
  return function(N) {
    // значение baseNumber сохранится в замыкании функции
    // оно будет "зафиксировано"
    return baseNumber + N;
  }
}

var addSix = createBase(6);
addSix(10);
addSix(21);

Вот еще одна задачка на ту же тему. Нужно написать функцию, которая будет перемножать полученные аргументы, но с необычным синтаксисом:

console.log(mul(2)(3)(4)); // output : 24

Тут все то же самое, просто мы не фиксируем первый (или первый и второй) множители в отдельной переменной.

function mul (x) {
  return function (y) { 
    return function (z) { 
      return x * y * z;
    };
  };
}

Тут нужно понимать две основные концепции JS:

  • Функция является объектом первого класса, то есть ее можно свободно возвращать из другой функции или сохранять в переменной как обычное значение.
  • Замыкания, с помощью которых при необходимости можно зафиксировать первый параметр.

IIFE (Immediately Invoked Function Expression) и паттерн Модуль

IIFE, или немедленно выполняемое функциональное выражение, - это объявление функции с ее немедленным выполнением.

(function IIFE(){
	console.log( "Hello!" );
})();
// "Hello!"

Такой синтаксис позволяет избежать загрязнения глобального пространства имен, ведь все необходимые переменные можно спрятать в замыкании.

С помощью IIFE в JavaScript реализуется паттерн проектирования Модуль - независимый от внешнего кода компонент.

Как создать по-настоящему приватный метод класса и в чем недостатки таких методов?

Создать действительно приватный метод в JS можно, только поместив его в замыкание конструктора.

var Employee = function (name, company, salary) {
  this.name = name || "";       // Публичное свойство
  this.company = company || ""; // Публичное свойство
  this.salary = salary || 5000; // Публичное свойство

  // Приватный метод
  let increaseSalary = function () {
    this.salary = this.salary + 1000;
  };

  // Публичный метод
  this.dispalyIncreasedSalary = function() {
    increaseSalary();
    console.log(this.salary);
  };
};


let emp1 = new Employee("John","Pluto",3000);
let emp2 = new Employee("Merry","Pluto",2000);
let emp3 = new Employee("Ren","Pluto",2500);

Недостатком такого способа является неизбежное дублирование. Функция increaseSalary будет создаваться для каждого экземпляра Employee, хотя в этом нет необходимости.

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

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

Этот Паттерн нечасто используется в классических языках программирования, но в JavaScript на нем полностью построена объектная модель. До ES6 даже привычного синтаксиса классов не было (сейчас это просто синтаксический сахар).

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

Задача по прототипной модели

Что выведет этот код?

let Employee = {
  company: 'xyz'
}
let emp1 = Object.create(Employee);
delete emp1.company
console.log(emp1.company);

Ответ: 'xyz'. Оператор delete удаляет только собственные свойства объекта, а свойство company определено в прототипе.
Проверить, является ли свойство собственным можно с помощью метода emp1.hasOwnProperty('company').
Можно удалить свойство напрямую из прототипа: delete emp1.__proto__.company.

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

new создает новый объект с типом object и устанавливает его внутреннее свойство [[prototype]] (__proto__ - равно свойству prototype функции-конструктора). Указатель this при этом указывает на только что созданный объект. После того как конструктор модифицирует этот объект, он возвращается.

Выглядит это примерно так:

function New(func) {
    let res = {};
    if (func.prototype !== null) {
        res.__proto__ = func.prototype;
    }
    let ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
        return ret;
    }
    return res;
}

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

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

function foo() {
	console.log( this.bar );
}

let obj1 = {
  bar: "obj1",
  foo: foo
};

let obj2 = {
  bar: "obj2"
};

obj1.foo();	    // "obj1"
foo.call( obj2 );   // "obj2"
new foo();	     // undefined

Обратите внимание, при вызове функции с new она выступает как конструктор и возвращает пустой объект, у которого не определено свойство bar.

Если вызвать функцию без контекста (foo()) в строгом режиме, будет ошибка, так как this в этом случае не определен.

Привязка контекста функции - метод 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

Как добавить массивам пользовательский метод?

Так как JavaScript основан на прототипах, чтобы добавить новый метод всем массивам, нужно определить его в прототипе массивов (объект Array.prototype).

Array.prototype.average = function() {
  let sum = this.reduce(function(prev, cur) { return prev + cur; });
  return sum / this.length;
}

let arr = [1, 2, 3, 4, 5];
let avg = arr.average();
console.log(avg); // => 3

Браузерный JS

Всплытие событий и как его остановить

Всплытие событий - это концепция DOM-модели. Когда событие генерируется на элементе, все родительские элементы последовательно о нем оповещаются. Поэтому вы можете повесить обработчик клика на контейнер карточки и таким образом узнать о клике по вложенной кнопке.

Остановить всплытие можно с помощью метода объекта события event.stopPropagation(). В IE < 9 у объекта события есть свойство event.cancelBubble.

Комментарии (0)

Ваш email не будет опубликован. Все поля обязательны