Золотое серебро ③ ④ Доля Один дал мне посадить на двух лицах лица лица

опрос

предисловие

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

Я также прочитал много вопросов интервью и статей, тем более отвратительным является вид代码片段巨长или牵扯到的知识点巨广или与各种复杂的数学公式相关的И есть过于底层Тип вопроса: как то, что написано от рукиPromise A+Спецификация, рука разрыва红黑树, что достигается формулойCanvas xx特效, Как браузер реализуетxxx API, что делать, если браузер не предоставляет этот API模仿实现该 API, алгоритм сравнения Vue3 и алгоритм Reactdiff 算法有什么区别, вы можете написать их от руки?结合版 diff 算法, рукописныйReact 的时间切片...

Я считаю, что большинство людей похожи на меня.Каждый раз, когда я читаю такую ​​статью, у меня не хватает терпения продолжать ее читать, или я не могу не перейти в область комментариев, чтобы прочитать комментарии на полпути, и затем нажмите «Мне нравится», «Подпишитесь» и добавьте в избранное одним щелчком, в избранном накопились тысячи статей.Хотя статьи такого рода очень технические, они слишком скучны или требуют слишком много знаний.Если какой-либо из них Если вы не ознакомившись с очками знаний, вы не сможете понять последующее содержание. Это как на уроке математики. Сначала я невнимательно слушал, пропустил определенный момент знания и не услышал его. Когда я пришел в себя, я обнаружил, что не могу этого понять.

Например, я однажды прочитал статью, чтобы добиться чего-то очень крутогоCanvasВнезапно появились спецэффекты, тригонометрические функции, хотя я их тоже выучил в средней школе, но за столько лет я почти забыл значения символов sin, cos и tan, да еще и ленивый. хочу открыть браузер и поискать какое-то время, просто продолжайте смотреть вниз и посмотрите, какой алгоритм матрицы появляется. Я на самом деле выучил его, когда учился в колледже. Короче говоря, очень здорово видеть конечный эффект, но как его достичь еще в темноте. Если вам это действительно не нужно в вашей работе, вы будете внимательно читать статью, чтобы изучить ее.Даже если вы не будете использовать ее в своей работе и внимательно изучите, вы, как правило, скоро ее забудете.

Другой вид статьи очень непосредственный: то есть описанные точки знаний не сложны, но я никогда не думал, что его можно использовать так раньше, что эквивалентно идее или API, которого я раньше не знал. , и он очень прост в использовании.Удобство. Такая статья не будет выглядеть особо скучной, да и читать все равно интересно, вздох: Ее еще можно так использовать! Почему ты не додумался до этого раньше?

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

Воспроизведение видео, когда оно неактивно в течение определенного периода времени

В то время я только входил в индустрию, и у меня был относительно небольшой опыт, поэтому у меня не было идей для каких-то очень распространенных нужд. То, что он сделал, былоElectronПроект отображается на стене в древнем городе Янмин. Требование состоит в том, что если пользователь не использует интерфейс в течение десяти минут, рекламный ролик древнего города Янмин будет воспроизводиться автоматически. В то время его разум, казалось, застрял : Как мы можем знать, что у пользователя десять минут ничего не работает? С этой целью я тоже специально пошел узнать, есть ли такой API, потом увидел статью, которая заставила меня воскликнуть, что это прекрасно! 👍

Принцип также очень прост, то есть установить переменную на десять минут на странице:

let minute = 10

Затем установите таймер -1 каждую минуту:

setInterval(() => {
  minute--
  if (minute <= 0) {
    // 播放视频
  }
 }, 1000 * 60)

Когда происходит событие, это означает, что пользователь работает, и переменную нужно восстановить:

window.addEventListener('click', () => minute = 10)

также может контролироватьmousemoveИли клавиатура и другие события, но этот элемент касается большого экрана, мыши или клавиатуры нет, поэтому достаточно прослушивания событий кликов.

Всего несколько строк кода решили мою насущную потребность.Конечно, это было блюдо в то время.Я не думал о таком простом требовании, но кто не подошёл шаг за шагом от Xiaobai? Именно с помощью этих статей можно шаг за шагом расширять идеи для быстрого прогресса.

Оптимизация производительности Vue

Я видел продукт, произведенный г-ном Хуаном.«Демистификация девяти советов по оптимизации производительности для Vue.js»

просто знай этоcomputedФункция внутри состоит в том, чтобы получитьthisПараметры:

computed: {
  a () { return 1 },
  b ({ a }) {
      return a + 10
  }
}

Таким образом, он не будет повторно извлекаться при обновлении компонента.getterНу, я никогда не замечал этого раньше.

Чистый CSS для достижения эффекта перетаскивания

В прошлом мы в основном использовали его, когда перетаскивали.JSЭто очень хлопотно реализовать, но я читал работу команды фронтенда.«Чистый CSS также позволяет добиться эффекта перетаскивания».Актерский состав из пяти человек, которым я восхищаюсь:

В традиционной сети прокрутка страницы является очень распространенным взаимодействием.Операция заключается в использовании колеса мыши или непосредственном перетаскивании полосы прокрутки. Однако мобильный терминал уже не тот, вы можете прокручивать страницу напрямую, перетаскивая ее пальцем. Обычно страница прокручивается либо по вертикали, либо по горизонтали, а что, если можно прокручивать в обоих направлениях? Например:

.dragbox {
  width: 300px;
  height: 300px;
  overflow: auto
}
.dragcon {
  width: 500px;
  height: 500px;
}

Пока ширина и высота внутреннего элемента больше контейнера, можно добиться прокрутки в обоих направлениях (не забудьте установить overflow:auto), как показано ниже:

WX20210313-180448@2x.png

В обычных условиях колесико мыши может одновременно прокручиваться только в одном направлении (удерживайте нажатой клавишу Shift, чтобы прокручивать в другом направлении), но мобильный терминал может напрямую перетаскивать содержимое для произвольной прокрутки, как показано ниже:

Теперь добавьте элемент в середине содержимого для прокрутки области содержимого:

Затем скройте следующий текст:

Пахнет немного перетянутым? Принцип так прост!

Новый синтаксис для Vue3

ищи сейчасVue3выходи илиComposition APIлибо新的响应式原理, об этих вещах сложнее говорить, и все игнорируют многие другие моменты, например, много раз мы хотимCSSТакже может быть реактивным, как синтаксис когда-то фантазии:

<template>
  <h1>{{ color }}</h1>
</template>

<script>
export default {
  data () {
    return {
      color: 'red'
    }
  }
}
</script>

<style>
h1 {
  color: this.color;
}
</style>

Однако из-заCSSа такжеJSотносящийся к другому контексту, это трудно сделать, но, читая это«Супер забавная новая функция Vue: введение переменных JS в CSS»Я только что узнал, что это можно написать так:

<template>
  <h1>{{ color }}</h1>
</template>

<script>
export default {
  data () {
    return {
      color: 'yellow'
    }
  }
}
</script>

<style>
h1 {
  color: v-bind(color)
}
</style>

когдаthis.colorкогда происходят изменения,cssтакже ответим вместе.

И есть«Супер забавная новая функция Vue: портал DOM», эти маленькие хитрости могут быть очень удобны для повышения эффективности нашей разработки, но сегодняшниеVue3Есть очень мало связанных статей, которые упоминают об этом.

Вопросы интервью Jiugongge

Этот вопрос для интервью содержит небольшое количество кода, но очень немногие люди могут сделать это правильно.«Не стоит недооценивать Цзюгунге, один вопрос может раскрыть истинную форму кандидата! 》Это дает нам хорошую идею, потому что при выполнении этого вида цзюгунге:

Многие думают, что им нужно только добавить границу к каждой сетке, но на самом деле, если они это сделают, это будет выглядеть так:

Потому что после добавления границы к каждому блоку две соседние границы будут совмещены, и невооруженным глазом будет выглядеть граница в два раза толще. а также«Не стоит недооценивать Цзюгунге, один вопрос может раскрыть истинную форму кандидата! 》Эта проблема легко решается с помощью отрицательных полей:

Отрицательные значения CSS, о которых вы не знали

Упоминание об отрицательных полях напоминает мне об этом«Навыки отрицательного значения CSS и детали, о которых вы не знаете»:

<template>
  <div></div>
</template>

<style>
div {
  width: 200px;
  height: 200px;
  outline: 20px solid #000;
  outline-offset: -118px;
}
</style>

На самом деле не ожидал, что это возможно.

тема

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

Конечно, нельзя сказать, что те статьи с большим количеством кода и сложным кодом плохие, на самом деле эти статьи очень технические, но ведь у большинства людей не хватает духу так внимательно изучать различные сложные алгоритмы. , но если вы собираетесь字节跳动,百度,阿里,腾讯Если такая большая компания идет на собеседование, все равно очень нужно изучать эти сложные статьи.

Реконструкция сцены

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

Конечно, я не хочу использовать свой телефон

Работа немного похожа на экзамен в средней школе: множественный выбор + заполнение пропусков + большие вопросы.

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

Одна из тем:写一个函数,这个函数会返回一个数组,数组里面是 2 ~ 32 之间的随机整数(不能重复),这个函数还可以传入一个参数,参数是几,返回的数组长度就是几

так:

WX20210313-201455@2x.png

Когда я впервые увидел заголовок, я подумал, как это сложно, написать это! Сначала сгенерируем случайные числа от 2 до 32... Как сгенерировать случайные числа от 2 до 32?Math.random() * 32, но это генерирует от 0 до 32, вот! Сначала сгенерируйте случайное число от 0 до 30, а затем добавьте к нему 2:

const fn = num => {
  let arr = []

  for (let i = num; i-- > 0;) {
    arr.push(Math.round(Math.random() * 30 + 2))
  }
  
  return arr
}

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

QQ20210313-202503@2x.png

В это время я подумал о новой структуре данных ES6 Set, которая может гарантировать отсутствие повторяющихся значений, а также может использоваться с...Оператор легко конвертируется в массив, поэтому продолжаем писать:

const fn = num => {
  let arr = []

  for (let i = num; i-- > 0;) {
    arr.push(Math.round(Math.random() * 30 + 2))
  }

  arr = [...new Set(arr)]

  return arr
}

Хотя такой способ записи решает проблему повторяющихся значений, он приносит новые проблемы: если повторяющихся значений несколько, длина массива будет меньше, например:

WX20210313-203320@2x.png

Мои мысли в то время были такими:fn(10)传的参数是 10,如果最终出来的数组不为 10,那就用 10 减去数组的长度,就是相差的位数了。 Напримерfn(10)Привести кarr.length = 8,Так10 - 8Это означает, что нужно сгенерировать только два случайных числа, но эти два случайных числа также могут перекрываться с существующим 8-битным массивом, поэтому нам нужно соединить случайно сгенерированное двузначное число с исходным 8-битным массивом. затем используйтеSetДля дедупликации структуры данных используйтеwhileОценка цикла, если параметры переданы10вычесть длину массиваarr.lengthне равно0Если у вас все еще есть повтор, то продолжайте перегенерировать случайное число, чтобы повторять шаги только сейчас, пока не будут автоматически выскакивать массивы, которые не повторяются по всем числам.whileloop, затем верните этот массив:

const fn = num => {
  let arr = []

  for (let i = num; i-- > 0;) {
    arr.push(Math.round(Math.random() * 30 + 2))
  }

  arr = [...new Set(arr)]
  
  let len = arr.length
      
  while (num - len > 0) {
    arr = [...new Set(arr.concat(fn(num - len)))]
    len = arr.length
  }

  return arr
}

результат операции:

WX20210313-211222@2x.png

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

Сложные вопросы с обеих сторон

Что касается второй стороны(Другие заданные вопросы), интервьюер сказал, что, хотя вы задали правильный вопрос, на самом деле это было похоже на взлом грубой силы, и эффективность была очень низкой. Например, я передаю функцию30,от2 ~ 32Всего чисел всего 30. Подумайте, сколько раз будет запускаться этот метод генерации случайных чисел.Если вам повезет, функция будет сгенерирована при первом запуске функции.29Если у вас есть массив с разными цифрами, то осталась только одна цифра, насколько вероятно, что последняя цифра будет повторяться? не так ли30分之29? Повторите это один раз, чтобы запустить его снова, повторите это снова и снова запустите его... Каждый раз, когда вам нужно создать массив, а затем создать новыйSetПреобразование обратно в массив очень дорого. У вас есть какие-то моменты, которые вы хотите оптимизировать?

В этот момент я подумал, чтоSet

fn(10)Math.random10

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

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

Вернувшись домой, я включил компьютер и начал писать код. Сначала я проверил свое исходное решение, чтобы убедиться, что производительность так плоха, как он сказал:

WX20210313-214527@2x.png

0,1 миллисекунды - это нормально! Может быть, число маленькое.Если это случайное число от 0 до 10000, он должен вылететь, верно? Немного изменим функцию:

const fn = num => {
  let arr = []

  for (let i = num; i-- > 0;) {
    arr.push(Math.round(Math.random() * 10000))
  }

  arr = [...new Set(arr)]

  let len = arr.length

  while (num - len > 0) {
    arr = [...new Set(arr.concat(fn(num - len)))]
    len = arr.length
  }

  return arr
}

результат операции:

WX20210313-215155@2x.png

В этот раз я действительно почувствовал замирание, на получение результата ушло больше двух секунд, а в компьютерных расчетах две тысячи миллисекунд — это уже астрономическое число. Затем попробуйте два массива, которые он сказал: я понимаю, что сначала нужно определить целое число в диапазоне от 2 до 32 заранее, а затем определить пустой массив для хранения результата в длине массива (length) и использовать сгенерированное целое число в качестве нижнего индекса для извлечения чисел из этого массива и помещения их в пустой массив, так что даже если сгенерированные случайные числа имеют дубликаты, это не имеет значения, поскольку два массива не идентичны. будут дубликаты:

const fn = num => {
  const allNums = Array.from({ length: 31 }, (_, i) => i + 2)
  const result = []

  for (let i = num; i-- > 0;) {
    result.push(allNums.splice(Math.floor(Math.random() * allNums.length), 1)[0])
  }

  return result
}

Попробуйте еще раз на этот раз:

WX20210313-220008@2x.png

Нет проблем, а как насчет производительности? Давайте проверим:

QQ20210313-220450@2x.png

Это действительно намного быстрее, чем раньше, давайте попробуем случайное число от 0 до 10000:

const fn = num => {
  const allNums = Array.from({ length: 10001 }, (_, i) => i)
  const result = []

  for (let i = num; i-- > 0;) {
    result.push(allNums.splice(Math.floor(Math.random() * allNums.length), 1)[0])
  }

  return result
}

результат операции:

WX20210313-220823@2x.png

На этот раз разрыв особенно очевиден: у одного более 2300 миллисекунд, что может заставить людей почувствовать зависание, а другому нужно всего семь миллисекунд, и человеческое чувство состоит в том, чтобы нажать Enter, чтобы получить результат.

На самом деле инкапсуляция этой функции недостаточно тщательная, так как внутри функции полностью прописана генерация случайных чисел от нескольких до нескольких.Если вы не хотите генерировать случайные числа от 2 до 32, вам нужно изменить код внутри функции, который, очевидно, не соответствует принципу открытости и закрытости и недостаточно гибок для использования. Давайте снова инкапсулируем его, чтобы сделать его более общей функцией:

const fn = (len, from = 0, to = 100) => {
  const allNums = Array.from({ length: to - from }, (_, i) => i + from)
  const result = []

  for (let i = len; i-- > 0;) {
    result.push(allNums.splice(Math.floor(Math.random() * allNums.length), 1)[0])
  }

  return result
}

результат операции:

WX20210313-222144@2x.png

Вы можете видеть, что он работает отлично и дает нужные нам результаты, но почему нет числа 12 при генерации десяти цифр от 2 до 12? Получается, что мы инкапсулировали эту функцию навстречу программе左闭右开Негласные правила, внимательные ученики давно должны были обнаружить, что в программе左闭右开такое явление, например, мы используемsubstringметод например:

'0123456'.substring(1, 5)

результат операции:

QQ20210313-222546@2x.png

Вы можете видеть, что мы передали параметры от 1 до 5, но конечный результат содержит 1 вместо 5. Когда мы учились в средней школе, мы узнали понятия открытого интервала и закрытого интервала.不包括это число, а закрытый интервал равен包括.想当年数学老师就一直反复强调过这个概念,所以程序中的左闭右开Он также должен быть спроектирован так, чтобы максимально соответствовать правилам математики!

Но я не думаю, что можно сказать, что последнее реализовано.随机数生成器Он лучше предыдущего, и это тоже зависит от ситуации.На крайний случай: если случайным образом сгенерировать 10 неповторяющихся чисел от 0 до 10000, вероятность повторения в таком большом диапазоне очень мала?уже? Таким образом, первая схема, скорее всего, должна сгенерировать только десять случайных чисел для удовлетворения потребностей. Но если это второе решение: сначала сгенерируйте массив от 0 до 10000. Этот массив слишком велик, но их нужно всего 10. Это немного похоже на зенитные орудия, чтобы убивать комаров, и разделочные ножи, чтобы убивать кур. Кажется, что вторая функция больше подходит только тогда, когда доля диапазона больше. Если вы хотите инкапсулировать немного более разумно, вы можете дать вам идею:

Разделите to-from на значение len, чтобы получить соотношение. Например, получение 10 чисел от 0 до 10000 эквивалентно 10/10000, что составляет одну тысячную.В этом случае используйте первую функцию для получения случайных чисел, и если вы получите 3000 от 0 до 10000Число является отношением три десятых.В этот раз уместнее использовать вторую функцию:

const fn = (len, from = 0, to = 100) => {
  const ratio = (to - from) / len
  let result = []
  
  if (ratio > 0.3) {
    const allNums = Array.from({ length: to - from }, (_, i) => i + from)

    for (let i = len; i-- > 0;) {
      result.push(allNums.splice(Math.floor(Math.random() * allNums.length), 1)[0])
    }
  } else {
    for (let i = len; i-- > 0;) {
      result.push(Math.round(Math.random() * to + from))
    }

    result = [...new Set(result)]

    let length = result.length

    while (len - length > 0) {
      result = [...new Set(result.concat(fn(len - length, from, to)))]
      length = result.length
    }
  }
  

  return result
}

Конечно, этой функции по-прежнему не хватает здравого смысла: например, когдаfromСравниватьtoЧто делать, когда вы старше? проходить负数Что я должен делать? проходить小数Что делать?bigintа такжеnumberЧто мне делать, когда он перепутался?lenСравниватьto - fromЧто делать, когда вы старше? Они не будут потрачены впустую здесь, чтобы инкапсулировать один за другим.Если вам интересно, вы можете инкапсулировать его самостоятельно.

полезность

Считаете ли вы, что эта функция не имеет другого применения в вопросах интервью? Это действительно не обязательно! После этого вопроса я сразу же думаю об этом.[Банк Циндао] Таблица культурного опытаГлавный модуль вопросов и ответов времени:

этоТаблица культурного опытаНа самом деле, это делается для того, чтобы друзья, которые приходят в Циндаоский банк для ведения бизнеса, не были такими скучными в процессе ожидания, особенно те клиенты, которые приходят с детьми, продвигая культуру Шаньдун, они будут вставлять две рекламы, чтобы заработать дополнительные деньги. (они зарабатывают, а не я), в начале этого модуля бекенд попросил меня подобрать двадцать-тридцать вопросов из Аналектов и отправить ему, он занес их в базу, а потом я случайным образом получил 10 вопросов по запросу. Позже он был слишком занят и сказал, что вопросов все равно не так много, поэтому позвольте мне написать их все на фронтенде и получить их случайным образом самостоятельно!

Итак, я случайным образом нашел несколько Аналектов Конфуция и вставил их в него, затем следующий шаг — алгоритм.Пользователь определенно не хочет каждый раз вводить одни и те же десять вопросов, по крайней мере, это должно быть немного случайным, верно? В то время я торопился, поэтому не написал алгоритм должным образом, поэтому написал только упрощенную версию:

const arr = ['题', '题', '题', ...'题']

const result = arr.filter(() => Math.random() > 0.5)

result.length = 10

Этот способ написания соответствует требованиям, но не является строгим. В результате более сложные вопросы в массиве появляются чаще, а более поздние вопросы появляются реже. Давайте проверим это:

const arr = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]

const fn = num => {
  const result = arr.filter(() => Math.random() > 0.5)
  result.length = num
  return result
}

результат операции:

Видно, что чем ниже вероятность появления цифр, тем меньше вероятность появления первых нескольких цифр.0(Первый вопрос) или 1(вопрос 2) появляется почти каждый раз, а последний вопрос в большинстве случаев даже не появляется.

Однако, поскольку я работал сверхурочно каждый день до раннего утра, я так устал, что у меня не было сил думать об алгоритме, поэтому я искал библиотеку, напримерLodash,UnderscoreВ библиотеке нет метода, который может добиться подобных функций, поэтому давайте сначала сделаем это, а потом изменим, когда тест скажет, что есть проблема! Первым приоритетом является выпуск программного обеспечения. Но об этом потом никто не узнал, об этом знала только я(Теперь и ты это знаешь!), Бог знает это, вы это знаете, и я это знаю, не ходите в банк Циндао и не рассказывайте другим!

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

Если вы спросите меня, почему бы мне не написать алгоритм в банке Циндао?Таблица культурного опытаАлгоритм замены? Потому что ты уже уволился! У меня нет полномочий снова касаться этого проекта, и все коллеги в Циндао также отказались. Контракт между компанией и Банком Циндао также закончился. Лидеры Стороны А также были удовлетворены принятием и сделали не предлагать никаких исправлений мнения, так что этот проект пришел к успешному завершению...

Я могу только похоронить недостатки этого алгоритма годами и поговорить с вами здесь.

Эта статья была впервые опубликована в публичном аккаунте:«Фронтальное обучение не движется»

Замечательные статьи в прошлом