Skip to content

Latest commit

 

History

History
2656 lines (1947 loc) · 79.4 KB

tasks.md

File metadata and controls

2656 lines (1947 loc) · 79.4 KB

Задачи разного уровня сложности

На главную

Задача № 1

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

Возможное решение

const name = prompt('Введите ваше имя:')
alert(`Привет, ${name}!`)

Задача № 2

Чему будут равны переменные a, b, c и d в примере ниже?

let a = 1
let b = 1

const c = ++a // ?
const d = b++ // ?

Ответ

a = 2
b = 2
c = 2
d = 1

Задача № 3

Какой результат будет у выражений ниже?

'' + 1 + 0
'' - 1 + 0
true + false
6 / '3'
'2' * '3'
4 + 5 + 'px'
'$' + 4 + 5
'4' - 2
'4px' - 2
7 / 0
'  -9  ' + 5
'  -9  ' - 5
null + 1
undefined + 1
' \\t \\n' - 2

Ответ

'' + 1 + 0 // '10' (1)
'' - 1 + 0 // -1 (2)
true + false // 1
6 / '3' // 2
'2' * '3' // 6
4 + 5 + 'px' // '9px'
'$' + 4 + 5 // '$45'
'4' - 2 // 2
'4px' - 2 // NaN
7 / 0 = Infinity
'  -9  ' + 5 // '  -9  5' (3)
'  -9  ' - 5 // -14 (4)
null + 1 // 1 (5)
undefined + 1 // NaN (6)
' \\t \\n' - 2 // -2 (7)
  • Сложение со строкой "" + 1 преобразует 1 к строке: "" + 1 = "1", и в следующем случае "1" + 0 работает то же самое правило.
  • Вычитание - (как и большинство математических операторов) работает только с числами, пустая строка "" приводится к 0.
  • Сложение со строкой превращает число 5 в строку и добавляет к строке.
  • Вычитание всегда преобразует к числу, значит строка " -9 " становится числом -9 (пробелы по краям обрезаются).
  • null становится 0 после численного преобразования.
  • undefined становится NaN после численного преобразования.
  • Пробельные символы, такие как \t и \n, по краям строки игнорируются при преобразовании в число, так что строка " \t \n", аналогично пустой строке, становится 0 после численного преобразования.

Задача № 4

Каким будет результат этих выражений?

5 > 4
'ананас' > 'яблоко'
'2' > '12'
undefined == null
undefined === null
null == '\\n0\\n'
null === +'\\n0\\n'

Ответ

5 > 4 // true
'ананас' > 'яблоко' // false
'2' > '12' // true
undefined == null // true
undefined === null // false
null == '\\n0\\n' // false
null === +'\\n0\\n' // false
  • Очевидно, true.
  • Используется посимвольное сравнение, поэтому false. "а" меньше, чем "я".
  • Снова посимвольное сравнение. Первый символ первой строки "2" больше, чем первый символ второй "1".
  • Специальный случай. Значения null и undefined равны только друг другу при нестрогом сравнении.
  • Строгое сравнение разных типов, поэтому false.
  • Аналогично (4), null равен только undefined.
  • Строгое сравнение разных типов.

Задача № 5

Перепишите конструкцию "if" с использованием условного оператора "?".

let result

if (a + b < 4) {
  result = 'Мало'
} else {
  result = 'Много'
}

Возможное решение

const result = a + b < 4 ? 'Мало' : 'Много'

Задача № 6

Перепишите "if-else" с использованием нескольких операторов "?".

let message

if (login == 'Сотрудник') {
  message = 'Привет'
} else if (login == 'Директор') {
  message = 'Здравствуйте'
} else if (login == '') {
  message = 'Нет логина'
} else {
  message = ''
}

Возможное решение

const message = login === 'Сотрудник'
  ? 'Привет'
  : login === 'Директор'
    ? 'Здравствуйте'
    : login === ''
      ? 'Нет логина'
      : ''

Задача № 7

Какие из перечисленных ниже "console.log()" выполнятся? Какие конкретно значения будут результатами выражений в условиях "if"?

if (-1 || 0) console.log('первый')
if (-1 && 0) console.log('второй')
if (null || (-1 && 1)) console.log('третий')

Ответ

Первое и третье выполнятся.

// Выполнится
// Результат -1 || 0 = -1, в логическом контексте true
if (-1 || 0) console.log('первый')

// Не выполнится
// -1 && 0 = 0,  в логическом контексте false
if (-1 && 0) console.log('второй')

// Выполнится
// оператор && имеет больший приоритет, чем ||
// так что -1 && 1 выполнится раньше
// вычисления: null || -1 && 1  ->  null || 1  ->  1
if (null || (-1 && 1)) console.log('третий')

Задача № 8

При помощи цикла "for" выведите четные числа от 2 до 10.

Возможное решение

for (let i = 2; i <= 10; i++) {
  if (i % 2 === 0) console.log(i)
}

Задача № 9

Напишите цикл, который предлагает (prompt) ввести число, большее 100. Если пользователь ввел другое число – попросить ввести еще раз, и так далее. Цикл должен спрашивать число до тех пор, пока либо пользователь не введет число, большее 100, либо не нажмет кнопку "Отмена" (ESC). Предполагается, что пользователь вводит только числа. Предусматривать обработку нечисловых строк не обязательно.

Возможное решение

let num

do {
  num = prompt('Введите число, большее 100', 0)
} while (num <= 100 && num)

Цикл "do-while" повторяется, пока верны две проверки:

  • Проверка "num <= 100" – то есть, введенное число все еще меньше 100.
  • Проверка "&& num" вычисляется в false, когда num имеет значение null или пустая строка (""). В этом случае цикл "while" тоже нужно прекратить.

Кстати, сравнение "num <= 100" при вводе null даст true, так что вторая проверка необходима.

Задача № 10

Следующая функция возвращает true, если параметр "age" больше 18. В ином случае она задает вопрос (confirm) и возвращает его результат.

function checkAge(age) {
  if (age > 18) {
    return true
  } else {
    return confirm('Родители разрешили?')
  }
}

Перепишите функцию, чтобы она делала то же самое, но без "if", в одну строку. Сделайте два варианта функции "checkAge":

  • Используя оператор "?"
  • Используя оператор "||"

Возможное решение

// оператор ?
const checkAge = (age) => (age > 18 ? true : confirm('Родители разрешили?'))

// оператор ||
const checkAge = (age) => age > 18 || confirm('Родители разрешили?')

Задача № 11

Напишите функцию "min(a, b)"", которая возвращает меньшее из чисел "a" и "b". Пример вызовов:

min(2, 5) // 2
min(3, -1) // -1
min(1, 1) // 1

Возможное решение

function min(a, b) {
  if (a < b) return a
  return b
}

// или
const min = (a, b) => (a < b ? a : b)

Задача № 12

Напишите функцию "pow(x,n)"", которая возвращает "x" в степени "n". Иначе говоря, умножает "x" на себя "n" раз и возвращает результат.

pow(3, 2) // 3 * 3 = 9
pow(3, 3) // 3 * 3 * 3 = 27

Возможное решение

function pow(x, n) {
  if (n <= 0) return false
  if (n === 1) return x

  let result = x

  for (let i = 1; i < n; i++) {
    result *= x
  }

  return result
}

// или с помощью рекурсии
const pow = (x, n) => {
  if (n <= 0) return false
  if (n === 1) return x
  return x * pow(x, n - 1)
}

// или в одну строку
const pow = (x, n) => (n <= 0 ? false : n === 1 ? x : x * pow(x, n - 1))

Задача № 13

Напишите функцию "isEmpty(obj)", которая возвращает true, если у объекта нет свойств, иначе false. Должно работать так:

const schedule = {}

console.log(isEmpty(schedule)) // true

schedule['8:30'] = 'Подъем!'

console.log(isEmpty(schedule)) // false

Возможное решение

function isEmpty(obj) {
  for (const key in obj) {
    return false
  }
  return true
}

// или
const isEmpty = (obj) => (Object.keys(obj).length ? false : true)

Задача № 14

У нас есть объект, в котором хранятся зарплаты нашей команды:

const salaries = {
  John: 100,
  Jane: 200,
  Bob: 300,
  Alice: 400,
}

Напишите код для суммирования всех зарплат и сохраните результат в переменной "sum". Должно получиться 1000. Если объект "salaries" пуст, то результат должен быть равен 0.

Возможное решение

let sum = 0
  for (let key in salaries) {
  sum += salaries[key]
}

// или
function sum(obj) {
  let sum = 0
  for (let salary of Object.values(salaries)) {
    sum += salary
  }

  return sum
}

// или
const sum = (obj) => (Object.keys(obj).length ? Object.values(obj).reduce((x, y) => x + y) : 0)

Задача № 15

Создайте функцию "multiplyNumeric(obj)", которая умножает все числовые свойства объекта "obj" на 2. Например:

// до вызова функции
const menu = {
  width: 200,
  height: 300,
  title: 'My menu',
}

multiplyNumeric(menu)

// после вызова функции
menu = {
  width: 400,
  height: 600,
  title: 'My menu',
}

Обратите внимание, что "multiplyNumeric()" не нужно ничего возвращать. Следует напрямую изменять объект.

Возможное решение

function multiplyNumeric(obj) {
  for (const key in obj) {
    if (typeof obj[key] === 'number') {
      obj[key] *= 2
    }
  }
}

Задача № 16

Создайте объект "calculator" (калькулятор) с тремя методами:

  • read() (читать) запрашивает два значения и сохраняет их как свойства объекта
  • sum() (суммировать) возвращает сумму сохраненных значений
  • mul() (умножить) перемножает сохраненные значения и возвращает результат

Возможное решение

const calculator = {
  sum() {
    return this.a + this.b
  },

  mul() {
    return this.a * this.b
  },

  read() {
    this.a = +prompt('a?', 0)
    this.b = +prompt('b?', 0)
  },
}

calculator.read()
console.log(calculator.sum())
console.log(calculator.mul())

Задача № 17

Это "ladder" (лестница) – объект, который позволяет подниматься вверх и спускаться:

const ladder = {
  step: 0,
  up() {
    this.step++
  },
  down() {
    this.step--
  },
  showStep: function () {
    // показывает текущую ступеньку
    console.log(this.step)
  },
}

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

ladder.up()
ladder.up()
ladder.down()
ladder.showStep() // 1

Измените код методов "up", "down" и "showStep" таким образом, чтобы их вызов можно было сделать по цепочке, например так:

ladder.up().up().down().showStep() // 1

Такой подход широко используется в библиотеках JavaScript.

Возможное решение

Решением является возврат самого объекта в каждом методе.

const ladder = {
  step: 0,
  up() {
    this.step++
    return this
  },
  down() {
    this.step--
    return this
  },
  showStep() {
    console.log(this.step)
    return this
  },
}

ladder.up().up().down().up().down().showStep() // 1

Задача № 18

Создайте функцию-конструктор "Calculator", который создает объекты с тремя методами:

  • read() запрашивает два значения при помощи "prompt()" и сохраняет их значение в свойствах объекта
  • sum() возвращает сумму введенных свойств
  • mul() возвращает произведение введенных свойств

Возможное решение

function Calculator() {
  this.read = function () {
    this.a = +prompt('a?', 0)
    this.b = +prompt('b?', 0)
  }

  this.sum = function () {
    return this.a + this.b
  }

  this.mul = function () {
    return this.a * this.b
  }
}

const calculator = new Calculator()
calculator.read()

console.log(calculator.sum())
console.log(calculator.mul())

Задача № 19

Напишите функцию-конструктор "Accumulator(initialValue)". Объект, который она создает, должен уметь следующее:

  • Хранить «текущее значение» в свойстве "value". Начальное значение устанавливается в аргументе конструктора "initialValue"
  • Метод "read" использует "prompt()" для получения числа и прибавляет его к свойству "value"

Возможное решение

function Accumulator(initialValue) {
this.value = initialValue

this.read = function () {
  this.value += +prompt('Сколько нужно добавить?', 0)
}
}

const accumulator = new Accumulator(1)
accumulator.read()
accumulator.read()
console.log(accumulator.value)

Задача № 20

Напишите функцию "randomInteger(min, max)", которая генерирует случайное целое (integer) число от min до max (включительно). Любое число из интервала "min-max" должно появляться с одинаковой вероятностью.

Возможное решение

function randomInteger(min, max) {
  // случайное число от min до (max + 1)
  let rand = min + Math.random() * (max + 1 - min)
  return Math.floor(rand)
}

// или
const randomInteger = (min, max) => ~~(min + Math.random() * (max + 1 - min))

Задача № 21

Напишите функцию "ucFirst(str)", возвращающую строку "str" с заглавным первым символом. Например:

ucFirst('ванька') === 'Ванька'

Возможное решение

function ucFirst(str) {
  return str[0].toUpperCase() + str.slice(1)
}

// или
const ucFirst = (str, trimmed = str.trim()) => `${trimmed[0].toUpperCase()}${trimmed.slice(1)}`

console.log(ucFirst('   ванька')) // Ванька

Задача № 22

Создайте функцию "truncate(str, maxlength)", которая проверяет длину строки "str" и, если она превосходит "maxlength", заменяет конец "str" на "…", так, чтобы ее длина стала равна "maxlength". Результатом функции должна быть та же строка, если усечение не требуется, либо, соответственно, усеченная строка.

Возможное решение

function truncate(str, maxlength) {
  return str.length > maxlength ? str.slice(0, maxlength - 1) + '…' : str
}

// или
const truncate = (str, max) => (str.length > max ? `${str.slice(0, max - 1)}...` : str)

const str = 'Очень длинная строка'

console.log(truncate(str, 6)) // Очень...

Задача № 23

Давайте произведем 5 операций с массивом.

  • Создайте массив "styles" с элементами "Джаз" и "Блюз".
  • Добавьте "Рок-н-ролл" в конец.
  • Замените значение в середине на "Классика". Ваш код для поиска значения в середине должен работать для массивов с любой длиной.
  • Удалите первый элемент массива и покажите его.
  • Вставьте "Рэп" и "Регги" в начало массива.

Массив по ходу выполнения операций:

Джаз, Блюз
Джаз, Блюз, Рок - н - ролл
Джаз, Классика, Рок - н - ролл
Классика, Рок - н - ролл
Рэп, Регги, Классика, Рок - н - ролл

Ответ

const styles = ['Джаз', 'Блюз']
styles.push('Рок-н-ролл')
styles[~~((styles.length - 1) / 2)] = 'Классика'
console.log(styles.shift())
styles.unshift('Рэп', 'Регги')

Задача № 24

На входе массив чисел, например: arr = [1, -2, 3, 4, -9, 6]. Задача: найти непрерывный подмассив в "arr", сумма элементов в котором максимальна. Функция "getMaxSubSum(arr)" должна возвращать эту сумму. Например:

getMaxSubSum([-1, 2, 3, -9]) // 5
getMaxSubSum([2, -1, 2, 3, -9]) // 5
getMaxSubSum([-1, 2, 3, -9, 11]) // 11
getMaxSubSum([-2, -1, 1, 2]) // 3
getMaxSubSum([100, -9, 2, -3, 5]) // 100
getMaxSubSum([1, 2, 3]) // 6

Если все элементы отрицательные – ничего не берем (подмассив пустой) и сумма равна 0:

getMaxSubSum([-1, -2, -3]) // 0

Возможное решение

function getMaxSubSum(arr) {
  let maxSum = 0
  let partialSum = 0

  for (const item of arr) {
    partialSum += item
    maxSum = Math.max(maxSum, partialSum)
    if (partialSum < 0) partialSum = 0
  }

  return maxSum
}

// или
const getMaxSubSum = (arr, max = 0) => {
  arr.reduce((x, y) => {
    y < 0 ? (x = 0) : (x += y)
    if (x > max) max = x
    return x
  }, 0)
  return max
}

Задача № 25

Напишите функцию "camelize(str)", которая преобразует строки вида "my-short-string" в "myShortString". То есть дефисы удаляются, а все слова после них получают заглавную букву. Примеры:

camelize('background-color') === 'backgroundColor'
camelize('list-style-image') === 'listStyleImage'

Возможное решение

function camelize(str) {
  return str
    .split('-')
    .map((word, index) => (index == 0 ? word : word[0].toUpperCase() + word.slice(1)))
    .join('')
}

// или
const camelize = (str, trimmed = str.trim()) =>
trimmed
  .split('-')
  .map((word, i) => (i === 0 ? word : `${word[0].toUpperCase()}${word.slice(1)}`))
  .join('')

console.log(camelize('   my-long-word')) // myLongWord

Задача № 26

Отсортировать элементы массива по убыванию. Пример:

const arr = [5, 2, 1, -10, 8]

// ...

console.log(arr) // 8, 5, 2, 1, -10

Возможное решение

const arr = [5, 2, 1, -10, 8]

arr.sort((a, b) => b - a)

console.log(arr) // 8, 5, 2, 1, -10

Задача № 27

Создайте функцию конструктор "Calculator", которая создает "расширяемые" объекты калькулятора. Задание состоит из двух частей.

  • Во-первых, реализуйте метод "calculate(str)", который принимает строку типа "1 + 2" в формате «ЧИСЛО оператор ЧИСЛО» (разделено пробелами) и возвращает результат. Метод должен понимать плюс (+) и минус (-). Пример использования:
const calc = new Calculator()

console.log(calc.calculate('3 + 7')) // 10
  • Затем добавьте метод "addMethod(name, func)", который добавляет в калькулятор новые операции. Он принимает оператор "name" и функцию с двумя аргументами "func(a,b)", которая описывает его. Например, давайте добавим умножение *, деление / и возведение в степень **:
const powerCalc = new Calculator()
powerCalc.addMethod('*', (a, b) => a * b)
powerCalc.addMethod('/', (a, b) => a / b)
powerCalc.addMethod('**', (a, b) => a ** b)

const result = powerCalc.calculate('2 ** 3')
console.log(result) // 8
  • Для этой задачи не нужны скобки или сложные выражения.
  • Числа и оператор разделены ровно одним пробелом.
  • Не лишним будет добавить обработку ошибок.

Возможное решение

Обратите внимание, как хранятся методы. Они просто добавляются к внутреннему объекту. Все тесты и числовые преобразования выполняются в методе "calculate". В будущем он может быть расширен для поддержки более сложных выражений.

function Calculator() {
  this.methods = {
    '-': (a, b) => a - b,
    '+': (a, b) => a + b,
  }

  this.calculate = function (str) {
    const split = str.split(' ')
    const a = +split[0]
    const op = split[1]
    const b = +split[2]

    if (!this.methods[op] || isNaN(a) || isNaN(b)) return NaN

    return this.methods[op](a, b)
  }

  this.addMethod = function (name, func) {
    this.methods[name] = func
  }
}

Задача № 28

У вас есть массив объектов "users", и у каждого из объектов есть firstName, lastName и id. Напишите код, который создаст еще один массив объектов с параметрами "id" и "fullName", где fullName – состоит из firstName и lastName. Например:

const vasya = { firstName: 'Вася', lastName: 'Пупкин', id: 1 }
const petya = { firstName: 'Петя', lastName: 'Иванов', id: 2 }
const masha = { firstName: 'Маша', lastName: 'Петрова', id: 3 }

const users = [vasya, petya, masha]

// const usersMapped = ...

/*
usersMapped = [
  { fullName: "Вася Пупкин", id: 1 },
  { fullName: "Петя Иванов", id: 2 },
  { fullName: "Маша Петрова", id: 3 }
]
*/

console.log(usersMapped[0].id) // 1
console.log(usersMapped[0].fullName) // Вася Пупкин

Возможное решение

const usersMapped = users.map((user) => ({
  fullName: `${user.firstName} ${user.lastName}`,
  id: user.id,
}))

// или
const usersMapped = users.map(({ firstName, lastName, id }) => ({
  fullName: `${firstName} ${lastName}`,
  id,
}))

Задача № 29

Напишите функцию "shuffle(array)", которая перемешивает (переупорядочивает случайным образом) элементы массива. Многократные прогоны через "shuffle" могут привести к разным последовательностям элементов. Например:

const arr = [1, 2, 3]

shuffle(arr)
// arr = [3, 2, 1]

shuffle(arr)
// arr = [2, 1, 3]

shuffle(arr)
// arr = [3, 1, 2]

Все последовательности элементов должны иметь одинаковую вероятность. Например, [1, 2, 3] может быть переупорядочено как [1, 2, 3] или [1, 3, 2], или [3, 1, 2] и т.д., с равной вероятностью каждого случая.

Возможное решение

// Тасование Фишера-Йетса
const shuffle = (arr) => {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = ~~(Math.random() * (i + 1)) // случайный индекс от 0 до i

    // меняем элементы местами
    // аналогично let t = arr[i]; arr[i] = arr[j]; arr[j] = t
    ;[arr[i], arr[j]] = [arr[j], arr[i]]
  }
  return arr
}

Задача № 30

Напишите функцию "getAverageAge(users)", которая принимает массив объектов со свойством "age" и возвращает средний возраст. Формула вычисления среднего арифметического значения: (age1 + age2 + ... + ageN) / N. Например:

const users = [
  { name: 'Вася', age: 25 },
  { name: 'Петя', age: 30 },
  { name: 'Маша', age: 29 },
]

console.log(getAverageAge(users)) // (25 + 30 + 29) / 3 = 28

Возможное решение

const getAverageAge = (users) => users.reduce((acc, cur) => acc + cur.age) / users.length

Задача № 31

Пусть "arr" – массив строк. Напишите функцию "unique(arr)", которая возвращает массив, содержащий только уникальные элементы "arr". Например:

const arr = ['раз', 'раз', 'раз', 'два', 'три', 'проверка', 'проверка']

console.log(unique(arr)) // раз, два, три, проверка

Возможное решение

// без использования Set
const unique = (arr, result = []) => {
  arr.forEach((str) => {
    !result.includes(str) && result.push(str)
  })
  return result
}

// с использованием Set
const unique = (arr) => [...new Set(arr)]

Задача № 32

Анаграммы – это слова, у которых те же буквы в том же количестве, но они располагаются в другом порядке. Например:

nap - pan
ear - are - era
cheaters - hectares - teachers

Напишите функцию "aclean(arr)", которая возвращает массив слов, очищенный от анаграмм. Например:

const arr = ['nap', 'teachers', 'cheaters', 'PAN', 'ear', 'era', 'hectares']

alert(aclean(arr)) // "nap,teachers,ear" или "PAN,cheaters,era"

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

Возможное решение

function aclean(arr) {
  const obj = {}

  for (let i = 0; i < arr.length; i++) {
    const sorted = arr[i].toLowerCase().split('').sort().join('')
    obj[sorted] = arr[i]
  }

  return Object.values(obj)
}

const arr = ['nap', 'teachers', 'cheaters', 'PAN', 'ear', 'era', 'hectares']

console.log(aclean(arr))

// или
const aclean = (arr, obj = {}) => {
  for (const word of arr) {
    const sorted = word.toLowerCase().split('').sort().join('')
    obj[sorted] = word
  }
  return Object.values(obj)
}

Задача № 33

Есть массив сообщений:

const messages = [
  { text: 'Hello', from: 'John' },
  { text: 'How are you?', from: 'Jane' },
  { text: 'See you soon', from: 'John' },
]
  • У вас есть к ним доступ, но управление этим массивом происходит где-то еще. Добавляются новые сообщения и удаляются старые, и вы не знаете, в какой момент это может произойти. Имея такую вводную информацию, решите, какую структуру данных вы могли бы использовать для ответа на вопрос "было ли сообщение прочитано?". Структура должна быть подходящей, чтобы можно было однозначно сказать, было ли прочитано это сообщение для каждого объекта сообщения.
  • Когда сообщение удаляется из массива "messages", оно должно также исчезать из структуры данных. Нам не следует модифицировать сами объекты сообщений, добавлять туда свойства. Если сообщения принадлежат какому-то другому коду, то это может привести к плохим последствиям.

Возможное решение

Можно хранить прочитанные сообщения в WeakSet:

const readMessages = new WeakSet()

readMessages.add(messages[0])
readMessages.add(messages[1])
// readMessages содержит 2 элемента

readMessages.add(messages[0])
// readMessages до сих пор содержит 2 элемента

// было ли сообщение message[0] прочитано?
console.log(`Read message 0: ${readMessages.has(messages[0])}`) // true

messages.shift()
// теперь readMessages содержит 1 элемент (хотя технически память может быть очищена позже)

WeakSet позволяет хранить набор сообщений и легко проверять наличие сообщения в нем. Он очищается автоматически. Минус в том, что мы не можем перебрать его содержимое, не можем получить "все прочитанные сообщения" напрямую. Но мы можем сделать это, перебирая все сообщения и фильтруя те, которые находятся в WeakSet. Альтернативным решением может быть добавление свойства вида "message.isRead = true" к сообщению после его прочтения. Так как сообщения принадлежат чужому коду, то это не очень хорошо, но если использовать свойство-символ, то вероятность конфликтов будет небольшой.

Задача № 34

Есть массив сообщений:

const messages = [
  { text: 'Hello', from: 'John' },
  { text: 'How are you?', from: 'Jane' },
  { text: 'See you soon', from: 'John' },
]

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

Возможное решение

Для хранения даты мы можем использовать WeakMap:

const readMap = new WeakMap()

readMap.set(messages[0], new Date().toLocaleDateString())

Задача № 35

Напишите функцию "count(obj)", которая возвращает количество свойств объекта:

const user = {
  name: 'John',
  age: 30,
}

console.log(count(user)) // 2

Возможное решение

const count = (obj) => Object.keys(obj).length

Задача № 36

У нас есть объект "salaries" с зарплатами:

const salaries = {
  John: 100,
  Jane: 200,
  Bob: 300,
  Alice: 400,
}

Создайте функцию "topSalary(salaries)", которая возвращает имя самого высокооплачиваемого сотрудника.

  • Если объект "salaries" пустой, то нужно вернуть null.
  • Если несколько высокооплачиваемых сотрудников, можно вернуть любого из них.

Возможное решение

function topSalary(salaries) {
  let max = 0
  let maxName = null

  for (const [name, salary] of Object.entries(salaries)) {
    if (max < salary) {
      max = salary
      maxName = name
    }
  }

  return maxName
}

// или
const topSalary = (obj, keys = Object.keys(obj), values = Object.values(obj)) => (keys.length ? keys[values.indexOf(Math.max(...values))] : null)

Задача № 37

В простых случаях циклических ссылок мы можем исключить свойство, из-за которого они возникают, из сериализации по его имени. Но иногда мы не можем использовать имя, так как могут быть и другие, нужные свойства с этим именем во вложенных объектах. Поэтому можно проверять свойство по значению. Напишите функцию "replacer" для JSON-преобразования, которая удалит свойства, ссылающиеся на "meetup":

const room = {
  number: 23,
}

const meetup = {
  title: 'Совещание',
  occupiedBy: [{ name: 'Иванов' }, { name: 'Петров' }],
  place: room,
}

// цикличные ссылки
room.occupiedBy = meetup
meetup.self = meetup

console.log(
  JSON.stringify(meetup, function replacer(key, value) {
    // ...
  })
)

/* в результате должно быть:
{
  "title":"Совещание",
  "occupiedBy":[{"name":"Иванов"},{"name":"Петров"}],
  "place":{"number":23}
}
*/

Возможное решение

console.log(
  JSON.stringify(meetup, function replacer(key, value) {
    return key !== '' && value === meetup ? undefined : value
  })
)

Задача № 38

Факториал натурального числа – это число, умноженное на "себя минус один", затем на "себя минус два", и так далее до 1. Факториал n обозначается как "n!" Определение факториала можно записать как:

n! = n * (n - 1) * (n - 2) * ...*1

Примеры значений для разных n:

1! = 1
2! = 2 * 1 = 2
3! = 3 * 2 * 1 = 6
4! = 4 * 3 * 2 * 1 = 24
5! = 5 * 4 * 3 * 2 * 1 = 120

Задача – написать функцию "factorial(n)", которая возвращает "n!", используя рекурсию.

console.log(factorial(5)) // 120

Возможное решение

const factorial = (n) => (n ? n * factorial(n - 1) : 1)

// мемоизация
const memo = (fn, cache = Object.create(null)) => (val) => cache[val] || (cache[val] = fn(val))

const fact = (n) => (n < 0 ? new Error() : n === 0 || n === 1 ? 1 : n * fact(n - 1))

const memoFact = memo(fact)

console.time('t1')
console.log(memoFact(100))
console.timeEnd('t1') // 0.15
console.time('t2')
console.log(memoFact(101))
console.timeEnd('t2') // 0.05
console.time('t3')
console.log(memoFact(102))
console.timeEnd('t3') // 0.05

Задача № 39

Последовательность чисел Фибоначчи определяется формулой "Fn = Fn - 1 + Fn - 2". То есть, следующее число получается как сумма двух предыдущих. Первые два числа равны 1, затем 2(1+1), 3(1+2), 5(2+3) и так далее: 1, 1, 2, 3, 5, 8, 13, 21.... Числа Фибоначчи тесно связаны с золотым сечением и множеством природных явлений вокруг нас. Напишите функцию "fib(n)" которая возвращает n-е число Фибоначчи. Пример работы:

console.log(fib(3)) // 2
console.log(fib(7)) // 13
console.log(fib(77)) // 5527939700884757

Возможное решение

// медленно
const fib = (n) => (n <= 1 ? n : fib(n - 1) + fib(n - 2))

// быстро
const fib = (n, a = 1, b = 1) => {
  for (let i = 3; i <= n; i++) {
    let c = a + b
    a = b
    b = c
  }
  return b
}

Задача № 40

Допустим, у нас есть такой односвязный список:

const list = {
  value: 1,
  next: {
    value: 2,
    next: {
      value: 3,
      next: {
        value: 4,
        next: null,
      },
    },
  },
}

Напишите функцию "printList(list)", которая выводит элементы списка по одному, и другую функцию "printReverseList(list)", которая делает тоже самое, но в обратном порядке.

Возможное решение

// с использованием цикла
const printList = (list, tmp = list) => {
  while (tmp) {
    console.log(tmp.value)
    tmp = tmp.next
  }
}

const printReverseList = (list, tmp = list, arr = []) => {
  while (tmp) {
    arr.push(tmp.value)
    tmp = tmp.next
  }

  for (let i = arr.length - 1; i >= 0; i--) {
    console.log(arr[i])
  }
}

// с использованием рекурсии
const printList = (list) => {
  console.log(list.value)
  if (list.next) {
    printList(list.next)
  }
}

const printReverseList = (list) => {
  if (list.next) {
    printReverseList(list.next)
  }
  console.log(list.value)
}

Задача № 41

Напишите функцию "sum", которая работает таким образом: sum(a)(b) = a + b. Да, именно таким образом, используя двойные круглые скобки (не опечатка). Например:

sum(1)(2) = 3
sum(5)(-1) = 4

Возможное решение

// это называется каррированием (currying)
function sum(a) {
  return function (b) {
    return a + b
  }
}

// или
const sum = (a) => (b) => a + b

Задача № 42

У нас есть встроенный метод "arr.filter(f)" для массивов. Он фильтрует все элементы с помощью функции "f". Если она возвращает true, то элемент добавится в возвращаемый массив. Сделайте набор "готовых к употреблению" фильтров:

  • inBetween(a, b) – между a и b (включительно).
  • inArray([...]) – находится в данном массиве.

Они должны использоваться таким образом:

  • arr.filter(inBetween(3, 6)) – выбирает только значения между 3 и 6 (включительно).
  • arr.filter(inArray([1, 2, 3])) – выбирает только элементы, совпадающие с одним из элементов массива

Например:

const arr = [1, 2, 3, 4, 5, 6, 7]

console.log(arr.filter(inBetween(3, 6))) // 3, 4, 5, 6

console.log(arr.filter(inArray([1, 2, 10]))) // 1, 2

Возможное решение

function inBetween(a, b) {
  return function (x) {
    return x >= a && x <= b
  }
}

function inArray(arr) {
  return function (x) {
    return arr.includes(x)
  }
}

// или
const inBetween = (a, b) => (x) => x >= a && x <= b

const inArray = (arr) => (x) => arr.includes(x)

Задача № 43

У нас есть массив объектов, который нужно отсортировать:

const users = [
  { name: 'John', age: 20, surname: 'Johnson' },
  { name: 'Pete', age: 18, surname: 'Peterson' },
  { name: 'Ann', age: 19, surname: 'Hathaway' },
]

Обычный способ был бы таким:

// по имени (Ann, John, Pete)
users.sort((a, b) => (a.name > b.name ? 1 : -1))

// по возрасту (Pete, Ann, John)
users.sort((a, b) => (a.age > b.age ? 1 : -1))

Можем ли мы сделать его короче, скажем, вот таким?

users.sort(byField('name'))
users.sort(byField('age'))

То есть, чтобы вместо функции, мы просто писали "byField(fieldName)". Напишите функцию "byField", которая может быть использована для этого.

Возможное решение

const byField = (field) => (a, b) => (a[field] > b[field] ? 1 : -1)

Задача № 44

Напишите функцию "printNumbers(from, to)", которая выводит число каждую секунду, начиная от "from" и заканчивая "to". Сделайте два варианта решения.

  • Используя setInterval.
  • Используя рекурсивный setTimeout.

Возможное решение

// setInterval
const printNumbers = (from, to) => {
  const timerId = setInterval(() => {
    console.log(from)
    from === to ? clearInterval(timerId) : from++
  }, 1000)
}

// setTimeout
function printNumbers(from, to) {
  setTimeout(function go() {
    console.log(from)
    if (from < to) setTimeout(go, 1000)
    from++
  }, 1000)
}

Задача № 45

Создайте декоратор "spy(func)", который должен возвращать обертку, которая сохраняет все вызовы функции в своем свойстве "calls". Каждый вызов должен сохраняться как массив аргументов. Например:

function work(a, b) {
  console.log(a + b) // произвольная функция или метод
}

work = spy(work)

work(1, 2) // 3
work(4, 5) // 9

for (const args of work.calls) {
  console.log(`call:${args.join()}`) // "call:1,2", "call:4,5"
}

Возможное решение

Здесь мы можем использовать call.push(args) для хранения всех аргументов в списке и f.apply(this, args) для переадресации вызова.

function spy(fn) {
  function wrap(...args) {
    wrap.calls.push(args)
    return fn.apply(this, arguments)
  }

  wrap.calls = []

  return wrap
}

// без this
const spy = (fn) => {
  const wrap = (...rest) => {
    wrap.calls.push(rest)
    return fn(...rest)
  }
  wrap.calls = []
  return wrap
}

Задача № 46

Создайте декоратор "delay(f, ms)", который задерживает каждый вызов "f" на "ms" миллисекунд. Например:

function f(x) {
  console.log(x)
}

// создаем обертки
let f1000 = delay(f, 1000)
let f1500 = delay(f, 1500)

f1000('test') // показывает "test" после 1000 мс
f1500('test') // показывает "test" после 1500 мс

Другими словами, "delay(f, ms)" возвращает вариант "f" с "задержкой на ms мс". В приведенном выше коде f – функция с одним аргументом, но ваше решение должно передавать все аргументы и контекст this.

Возможное решение

Здесь мы можем использовать call.push(args) для хранения всех аргументов в списке и f.apply(this, args) для переадресации вызова.

function delay(f, ms) {
  return function () {
    setTimeout(() => f.apply(this, arguments), ms)
  }
}

// без this
const delay = (fn, ms) => (...rest) => setTimeout(() => fn(...rest), ms)

Задача № 47

Что выведет функция?

function f() {
  console.log(this) // ?
}

const user = {
  g: f.bind(null),
}

user.g()

Ответ

Функция выведет null.

function f() {
  console.log(this) // null
}

const user = {
  g: f.bind(null),
}

user.g()

Контекст связанной функции жестко фиксирован. Изменить однажды привязанный контекст уже нельзя. Так что хоть мы и вызываем "user.g()", внутри исходная функция будет вызвана с this = null. Однако, функции "g" совершенно без разницы, какой this она получила. Ее единственное предназначение – это передать вызов в "f" вместе с аргументами и ранее указанным контекстом null, что она и делает. Таким образом, когда мы запускаем "user.g()", исходная функция вызывается с this = null.

Задача № 48

Можем ли мы изменить this дополнительным связыванием? Что выведет этот код?

function f() {
  console.log(this.name)
}

f = f.bind({ name: 'Вася' }).bind({ name: 'Петя' })

f() // ?

Ответ

Функция выведет "Вася".

function f() {
  console.log(this.name)
}

f = f.bind({ name: 'Вася' }).bind({ name: 'Петя' })

f() // Вася

Экзотический объект bound function, возвращаемый при первом вызове "f.bind()", запоминает контекст (и аргументы, если они были переданы) только во время создания. Следующий вызов "bind" будет устанавливать контекст уже для этого объекта. Это ни на что не повлияет. Можно сделать новую привязку, но нельзя изменить существующую.

Задача № 49

Вызов "askPassword()" в приведённом ниже коде должен проверить пароль и затем вызвать "user.loginOk/loginFail" в зависимости от ответа. Однако, его вызов приводит к ошибке. Почему? Исправьте последнюю строку кода, чтобы все работало (других строк изменять не надо).

function askPassword(ok, fail) {
  const password = prompt('Password?', '')
  if (password == 'rockstar') ok()
  else fail()
}

const user = {
  name: 'John',

  loginOk() {
    console.log(`${this.name} logged in`)
  },

  loginFail() {
    console.log(`${this.name} failed to log in`)
  },
}

askPassword(user.loginOk, user.loginFail)

Возможное решение

Ошибка происходит потому, что "askPassword" получает функции "loginOk/loginFail" без контекста. Когда они вызываются, то, естественно, this = undefined. Используем "bind", чтобы передать в "askPassword" функции "loginOk/loginFail" с привязанным контекстом:

function askPassword(ok, fail) {
  const password = prompt('Password?', '')
  if (password == 'rockstar') ok()
  else fail()
}

const user = {
  name: 'John',

  loginOk() {
    console.log(`${this.name} logged in`)
  },

  loginFail() {
    console.log(`${this.name} failed to log in`)
  },
}

askPassword(user.loginOkюиштв(user), user.loginFail.bind(user))

Теперь все работает корректно. Альтернативное решение – сделать функции-обертки над "user.loginOk/loginFail":

askPassword(
  () => user.loginOk(),
  () => user.loginFail()
)

Обычно, это также работает и хорошо выглядит. Но может не сработать в более сложных ситуациях, а именно – когда значение переменной "user" меняется между вызовом "askPassword" и выполнением "() => user.loginOk()".

Задача № 50

Это задание является немного усложненным вариантом предыдущего. Объект "user" был изменен. Теперь вместо двух функций "loginOk/loginFail" у него есть только одна – user.login(true/false). Что нужно передать в вызов функции "askPassword" в коде ниже, чтобы она могла вызывать функцию "user.login(true)" как "ok" и функцию "user.login(false)" как "fail"?

function askPassword(ok, fail) {
  const password = prompt('Password?', '')
  if (password == 'rockstar') ok()
  else fail()
}

const user = {
  name: 'John',

  login(result) {
    console.log(this.name + (result ? ' logged in' : ' failed to log in'))
  },
}

askPassword() // ?

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

Возможное решение

Можно использовать стрелочную функцию-обертку:

askPassword(
  () => user.login(true),
  () => user.login(false)
)

Или же можно создать частично примененную функцию на основе "user.login", которая использует объект "user" в качестве контекста и получает соответствующий первый аргумент:

askPassword(user.login.bind(user, true), user.login.bind(user, false))

Задача № 51

Объект rabbit наследует от объекта animal. Какой объект получит свойство full при вызове rabbit.eat(): animal или rabbit?

const animal = {
  eat() {
    this.full = true
  }
}

const rabbit = {
  __proto__: animal
}

rabbit.eat()

Ответ

Поскольку this – это объект, который стоит перед точкой, rabbit.eat() изменяет объект rabbit. Поиск свойства и исполнение кода – два разных процесса. Сначала осуществляется поиск метода rabbit.eat в прототипе, а затем этот метод выполняется с this=rabbit.

Задача № 52

У нас есть два хомяка: шустрый (speedy) и ленивый (lazy); оба наследуют от общего объекта hamster. Когда мы кормим одного хомяка, второй тоже наедается. Почему? Как это исправить?

const hamster = {
  stomach: [],

  eat(food) {
    this.stomach.push(food)
  }
}

const speedy = {
  __proto__: hamster
}

const lazy = {
  __proto__: hamster
}

speedy.eat("apple")
console.log( speedy.stomach ) // apple

console.log( lazy.stomach ) // apple

Ответ

Давайте внимательно посмотрим, что происходит при вызове speedy.eat("apple").

  • Сначала в прототипе (=hamster) находится метод speedy.eat, а затем он выполняется с this=speedy (объект перед точкой).
  • Затем в this.stomach.push() нужно найти свойство stomach и вызвать для него push. Движок ищет stomach в this (=speedy), но ничего не находит.
  • Он идёт по цепочке прототипов и находит stomach в hamster.
  • И вызывает для него push, добавляя еду в живот прототипа.

Получается, что у хомяков один живот на двоих! И при lazy.stomach.push(...) и при speedy.stomach.push(), свойство stomach берётся из прототипа (так как его нет в самом объекте), затем в него добавляются данные. Обратите внимание, что этого не происходит при простом присваивании this.stomach=:

const hamster = {
  stomach: [],

  eat(food) {
    this.stomach = [food]
  }
}

const speedy = {
  __proto__: hamster
}

const lazy = {
  __proto__: hamster
}

speedy.eat("apple")
console.log( speedy.stomach ) // apple

console.log( lazy.stomach ) // пусто

Теперь всё работает правильно, потому что this.stomach= не ищет свойство stomach. Значение записывается непосредственно в объект this. Также мы можем полностью избежать проблемы, если у каждого хомяка будет собственный живот:

const hamster = {
  stomach: [],

  eat(food) {
    this.stomach.push(food)
  }
}

const speedy = {
  __proto__: hamster,
  stomach: []
}

const lazy = {
  __proto__: hamster,
  stomach: []
}

speedy.eat("apple")
console.log( speedy.stomach ) // apple

console.log( lazy.stomach ) // пусто

Все свойства, описывающие состояние объекта (как свойство stomach в примере выше), рекомендуется записывать в сам этот объект. Это позволяет избежать подобных проблем.

Задача № 53

Представьте, что у нас имеется некий объект obj, созданный функцией-конструктором – мы не знаем какой именно, но хотелось бы создать ещё один объект такого же типа. Можем ли мы сделать так?

const obj2 = new obj.constructor()

Приведите пример функции-конструктора для объекта obj, с которой такой вызов корректно сработает. И пример функции-конструктора, с которой такой код поведёт себя неправильно.

Ответ

Мы можем использовать такой способ, если мы уверены в том, что свойство "constructor" существующего объекта имеет корректное значение. Например, если мы не меняли "prototype", используемый по умолчанию, то код ниже, без сомнений, сработает:

function User(name) {
  this.name = name
}

const user = new User('John')
const user2 = new user.constructor('Jane')

console.log( user2.name ) // Jane

Всё получилось, потому что User.prototype.constructor == User. Но если кто-то перезапишет User.prototype и забудет заново назначить свойство "constructor", чтобы оно указывало на User, то ничего не выйдет. Например:

function User(name) {
  this.name = name
}
User.prototype = {} // (*)

const user = new User('John')
const user2 = new user.constructor('Jane')

console.log( user2.name ) // undefined

Почему user2.name приняло значение undefined? Рассмотрим, как отработал вызов new user.constructor('Jane'):

  • Сначала ищется свойство constructor в объекте user. Не нашлось.
  • Потом задействуется поиск по цепочке прототипов. Прототип объекта user – это User.prototype, и там тоже нет искомого свойства.
  • Значение User.prototype – это пустой объект {}, чей прототип – Object.prototype. Object.prototype.constructor === Object. Таким образом, свойство constructor всё-таки найдено.

Наконец, срабатывает const user2 = new Object('Jane'), но конструктор Object игнорирует аргументы, он всегда создаёт пустой объект: const user2 = {} – это как раз то, чему равен user2 в итоге.

Задача № 54

Добавьте всем функциям в прототип метод defer(ms), который возвращает обёртку, откладывающую вызов функции на ms миллисекунд. Например, должно работать так:

function f(a, b) {
  console.log( a + b )
}

f.defer(1000)(1, 2) // 3 через 1 секунду

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

Возможное решение

Мы можем использовать такой способ, если мы уверены в том, что свойство "constructor" существующего объекта имеет корректное значение. Например, если мы не меняли "prototype", используемый по умолчанию, то код ниже, без сомнений, сработает:

Function.prototype.defer = function(ms) {
  let f = this
  return function(...args) {
    setTimeout(() => f.apply(this, args), ms)
  }
}

Задача № 55

Имеется объект dictionary, созданный с помощью Object.create(null) для хранения любых пар ключ/значение. Добавьте ему метод dictionary.toString(), который должен возвращать список ключей, разделённых запятой. Ваш toString не должен выводиться при итерации объекта с помощью цикла for..in. Вот так это должно работать:

const dictionary = Object.create(null)

// ...

dictionary.apple = "apple"
dictionary.banana = "banana"

for(const key in dictionary) {
  console.log(key) // apple banana
}

alert(dictionary) // apple,banana

Возможное решение

В методе можно получить все перечисляемые ключи с помощью Object.keys и вывести их список. Чтобы сделать toString неперечисляемым, давайте определим его, используя дескриптор свойства. Синтаксис Object.create позволяет нам добавить в объект дескрипторы свойств как второй аргумент.

const dictionary = Object.create(null, {
  toString: {
    value() {
      return Object.keys(this).join()
    }
  }
})

// или
const dictionary = Object.create(null)

Object.defineProperty(dictionary, 'toString', {
  value: () => Object.keys(dictionary).join()
})

Задача № 56

Класс Clock написан в функциональном стиле:

function Clock({ template }) {
  let timer

  function render() {
    const date = new Date()

    const hours = date.getHours()
    if (hours < 10) hours = '0' + hours

    const mins = date.getMinutes()
    if (mins < 10) mins = '0' + mins

    const secs = date.getSeconds()
    if (secs < 10) secs = '0' + secs

    const output = template
      .replace('h', hours)
      .replace('m', mins)
      .replace('s', secs)

    console.log(output)
  }

  this.stop = function () {
    clearInterval(timer)
  }

  this.start = function () {
    render()
    timer = setInterval(render, 1000)
  }
}

const clock = new Clock({ template: 'h:m:s' })
clock.start()

Перепишите его, используя современный синтаксис классов.

Возможное решение

class Clock {
  constructor({ template }) {
    this.template = template
  }

  render() {
    const date = new Date()

    const hours = date.getHours()
    if (hours < 10) hours = '0' + hours

    const mins = date.getMinutes()
    if (mins < 10) mins = '0' + mins

    const secs = date.getSeconds()
    if (secs < 10) secs = '0' + secs

    const output = this.template
      .replace('h', hours)
      .replace('m', mins)
      .replace('s', secs)

    console.log(output)
  }

  stop() {
    clearInterval(this.timer)
  }

  start() {
    this.render()
    this.timer = setInterval(() => this.render(), 1000)
  }
}

const clock = new Clock({ template: 'h:m:s' })
clock.start()

Задача № 57

У нас есть класс Clock. Сейчас он выводит время каждую секунду:

class Clock {
  constructor({ template }) {
    this.template = template
  }

  render() {
    const date = new Date()

    const hours = date.getHours()
    if (hours < 10) hours = '0' + hours

    const mins = date.getMinutes()
    if (mins < 10) mins = '0' + mins

    const secs = date.getSeconds()
    if (secs < 10) secs = '0' + secs

    const output = this.template
      .replace('h', hours)
      .replace('m', mins)
      .replace('s', secs)

    console.log(output)
  }

  stop() {
    clearInterval(this.timer)
  }

  start() {
    this.render()
    this.timer = setInterval(() => this.render(), 1000)
  }
}

Создайте новый класс ExtendedClock, который будет наследоваться от Clock и добавьте параметр precision – количество миллисекунд между «тиками». Установите значение в 1000 (1 секунда) по умолчанию. Не изменяйте класс Clock. Расширьте его.

Возможное решение

class ExtendedClock extends Clock {
  constructor(options) {
    super(options)
    const { precision = 1000 } = options
    this.precision = precision
  }

  start() {
    this.render()
    this.timer = setInterval(() => this.render(), this.precision)
  }
}

Задача № 58

Почему instanceof в примере ниже возвращает true? Мы же видим, что a не создан с помощью B().

function A() {}
function B() {}

A.prototype = B.prototype = {}

const a = new A()

console.log( a instanceof B ) // true
clock.start()

Ответ

Да, действительно, выглядит странно. Но instanceof не учитывает саму функцию при проверке, а только prototype, который проверяется на совпадения в прототипной цепочке. И в данном примере a.proto == B.prototype, так что instanceof возвращает true. Таким образом, по логике instanceof, именно prototype в действительности определяет тип, а не функция-конструктор.

Задача № 59

Создайте класс FormatError, который наследует от встроенного класса SyntaxError. Класс должен поддерживать свойства message, name и stack. Пример использования:

const err = new FormatError("ошибка форматирования")

console.log( err.message ) // ошибка форматирования
console.log( err.name ) // FormatError
console.log( err.stack ) // stack

console.log( err instanceof FormatError ) // true
console.log( err instanceof SyntaxError ) // true

Возможное решение

class FormatError extends SyntaxError {
  constructor(message) {
    super(message)
    this.name = "FormatError"
  }
}

Задача № 60

Что выведет код ниже?

const promise = new Promise(function(resolve, reject) {
  resolve(1)

  setTimeout(() => resolve(2), 1000)
})

promise.then(console.log)

Ответ

Вывод будет: 1. Второй вызов resolve будет проигнорирован, поскольку учитывается только первый вызов reject/resolve. Все последующие вызовы – игнорируются.

Задача № 61

Встроенная функция setTimeout использует колбэк-функции. Создайте альтернативу, использующую промисы. Функция delay(ms) должна возвращать промис, который перейдёт в состояние «выполнен» через ms миллисекунд, так чтобы мы могли добавить к нему .then:

function delay(ms) {
  // ...
}

delay(3000).then(() => console.log('выполнилось через 3 секунды'))

Возможное решение

const delay = (ms) =>
  new Promise(resolve => setTimeout(resolve, ms))

Заметьте, что resolve вызывается без аргументов. Мы не возвращаем из delay ничего, просто гарантируем задержку.

Задача № 62

Что вы думаете? Выполнится ли .catch? Поясните свой ответ.

new Promise(function(resolve, reject) {
  setTimeout(() => {
    throw new Error("Whoops!")
  }, 1000)
}).catch(console.log)

Ответ

Нет, не выполнится. Здесь присутствует "скрытый try..catch" вокруг кода функции. Поэтому обрабатываются все синхронные ошибки. В данном примере ошибка генерируется не по ходу выполнения кода, а позже. Поэтому промис не может обработать её.

Задача № 63

Перепишите пример, используя "async/await":

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json()
      } else {
        throw new Error(response.status)
      }
    })
}

Возможное решение

async function loadJson(url) {
  const response = await fetch(url)

  const {status} = response

  if (status === 200) {
    const json = await response.json()
    return json
  }

  throw new Error(status)
}

Задача № 64

Перепишите пример, используя "async/await":

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`)
    this.name = 'HttpError'
    this.response = response
  }
}

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json()
      } else {
        throw new HttpError(response)
      }
    })
}

function demoGithubUser() {
  let name = prompt("Введите логин?", "harryheman")

  return loadJson(`https://api.github.com/users/${name}`)
    .then(user => {
      alert(`Полное имя: ${user.name}.`)
      return user
    })
    .catch(err => {
      if (err instanceof HttpError && err.response.status == 404) {
        alert("Такого пользователя не существует, пожалуйста, повторите ввод.")
        return demoGithubUser()
      } else {
        throw err
      }
    })
}

demoGithubUser()

Возможное решение

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`)
    this.name = 'HttpError'
    this.response = response
  }
}

async function loadJson(url) {
  let response = await fetch(url)
  if (response.status == 200) {
    return response.json()
  } else {
    throw new HttpError(response)
  }
}

async function demoGithubUser() {
  let user
  while(true) {
    const name = prompt("Введите логин?", "harryheman")

    try {
      user = await loadJson(`https://api.github.com/users/${name}`)
      break
    } catch(err) {
      if (err instanceof HttpError && err.response.status === 404) {
        alert("Такого пользователя не существует, пожалуйста, повторите ввод.")
      } else {
        throw err
      }
    }
  }

  alert(`Полное имя: ${user.name}.`)
  return user
}

demoGithubUser()

Задача № 65

Обычно при чтении несуществующего свойства из объекта возвращается undefined. Создайте прокси, который генерирует ошибку при попытке прочитать несуществующее свойство. Это может помочь обнаружить программные ошибки пораньше. Напишите функцию wrap(target), которая берёт объект target и возвращает прокси, добавляющий в него этот аспект функциональности. Вот как это должно работать:

const user = {
  name: "John"
}

function wrap(target) {
  return new Proxy(target, {
      // ...
  })
}

user = wrap(user)

console.log(user.name) // John
console.log(user.age) // Ошибка: такого свойства не существует

Возможное решение

function wrap(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      if (prop in target) {
        return Reflect.get(target, prop, receiver)
      } else {
        throw new ReferenceError(`Свойства "${prop}" не существует`)
      }
    }
  })
}

Задача № 66

Создайте функцию makeObservable(target), которая делает объект «наблюдаемым», возвращая прокси. Вот как это должно работать:

function makeObservable(target) {
  // ...
}

const user = {}
user = makeObservable(user)

user.observe((key, value) => {
  console.log(`SET ${key}=${value}`)
})

user.name = "John" // SET name=John

Другими словами, возвращаемый makeObservable объект аналогичен исходному, но также имеет метод observe(handler), который позволяет запускать handler при любом изменении свойств. При изменении любого свойства вызывается handler(key, value) с именем и значением свойства.

Возможное решение

Решение состоит из двух частей:

  • При вызове .observe(handler) нам нужно где-то сохранить обработчик, чтобы вызвать его позже. Можно хранить обработчики прямо в объекте, создав в нём для этого свой символьный ключ.
  • Нам нужен прокси с ловушкой set, чтобы вызывать обработчики при изменении свойств.
const handlers = Symbol('handlers')

function makeObservable(target) {
  target[handlers] = []

  target.observe = function(handler) {
    this[handlers].push(handler)
  }

  return new Proxy(target, {
    set(target, property, value) {
      const success = Reflect.set(...arguments)
      if (success) {
        target[handlers].forEach(handler => handler(property, value))
      }
      return success
    }
  })
}

Задача № 67

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

Возможное решение

const expr = prompt("Введите арифметическое выражение:", '2*3/2')

const calc = expr => {
  if (!expr) return

  ;[...expr].forEach(char => {
    if (!char.match(/[\d\+\-*/]+/)) {
      throw new Error('wrong symbol')
    }
  })

  return eval(expr)
}

console.log(calc(expr))

Задача № 68

Используя "Intl.Collator", отсортируйте массив:

const animals = ["тигр", "ёж", "енот", "ехидна", "АИСТ", "ЯК"]

// ...

console.log(animals)
// ["АИСТ", "ёж", "енот", "ехидна", "тигр", "ЯК"]

В этом примере порядок сортировки не должен зависеть от регистра. Что касается буквы "ё", то мы следуем обычным правилам сортировки буквы ё, по которым «е» и «ё» считаются одной и той же буквой, за исключением случая, когда два слова отличаются только в позиции буквы «е» / «ё» – тогда слово с «е» ставится первым.

Возможное решение

const collator = new Intl.Collator()

animals.sort((a, b) => collator.compare(a, b))

console.log(animals.sort())
// // ["АИСТ", "ЯК", "енот", "ехидна", "тигр", "ёж"]