1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/wizardforcel-eloquent-js-3e-zh

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
5.md 26 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 29.11.2024 02:25 369bafd

Высшие функции

Цзы-ли и Цзы-сы хвастались размерами своих последних программ. «Двести тысяч строк, — сказал Цзы-ли, — не считая комментариев!» Цзы-сы ответил: «Пфф, моя уже почти миллион строк». Мастер Юань-Ма сказал: «Моя лучшая программа состоит из пятисот строк». Услышав это, Цзы-ли и Цзы-сы просветлели. Мастер Юань-ма, «Книга программирования»

Есть два способа создания программного обеспечения: один способ сделать его настолько простым, что в нём явно нет недостатков, а другой способ сделать его таким сложным, чтобы явных недостатков не было. C.A.R. Хоар, лекция премии Тьюринга 1980 года

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

Давайте кратко рассмотрим примеры из введения. Первая программа содержит 6 строк кода и может быть запущена напрямую.

let total = 0, count = 1;
while (count <= 10) {
  total += count;
  count += 1;
}
console.log(total);

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

console.log(sum(range(1, 10)));

Какая программа более вероятно содержит баг?

Если учесть количество строк кода для функций sum и range, очевидно, что вторая программа имеет больше кода. Однако я всё ещё думаю, что вероятность наличия бага во второй программе ниже.

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

Функции sum и range определяют операции, которые, конечно, будут включать циклы, подсчёт и другие операции. Но по сравнению с написанием этого кода вместе, этот способ выражения проще и легче избежать ошибок.

Абстракция

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

Рассмотрим, например, эти два рецепта бобового супа:

Положите обезвоженные бобы в контейнер по одной чашке на человека. Залейте водой до тех пор, пока бобы не будут покрыты водой, затем замочите бобы минимум на 12 часов. Слейте бобы и положите их в кастрюлю по четыре чашки воды на человека. Заполните кастрюлю ингредиентами до краёв и варите на медленном огне в течение двух часов. Добавьте по половине луковицы на человека, нарежьте ножом и добавьте в бобы. Добавьте по одному стеблю сельдерея на человека, нарежьте ножом и добавьте к бобам. Добавьте по одному корню моркови на человека, нарежьте ножом и добавьте к бобам. Наконец, варите вместе более 10 минут.

Второй рецепт:

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

Замочите бобы на 12 часов. Положите по четыре чашки воды на человека в кастрюлю, затем варите на слабом огне в течение двух часов. Добавить нарезанные овощи и варить вместе более 10 минут.

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

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

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

Повторная абстракция

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

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

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

Можем ли мы превратить «выполнение чего-либо N раз» в функцию? Написать функцию, которая вызывает console.log N раз, легко.

function repeatLog(n) {
  for (let i = 0; i < n; i++) {
    console.log(i);
  }
}

Но что, если мы хотим выполнить операцию, отличную от печати числа? Мы можем использовать функции для определения операции, которую мы хотим выполнить, а функция также является значением, поэтому мы можем передать ожидаемую операцию в качестве параметра.

function repeat(n, action) {
  for (let i = 0; i < n; i++) {
    action(i);
  }
}
repeat(3, console.log);
// → 0
// → 1
// → 2

Вам не нужно передавать предопределённую функцию в repeat. Обычно вы хотите создать функцию на месте.

let labels = [];
repeat(5, i => {
  labels.push(`Unit ${i + 1}`);
});
console.log(labels);
// → ["Unit 1", "Unit 2", "Unit 3", "Unit 4", "Unit 5"]

Эта структура немного похожа на цикл for — она сначала описывает цикл, а затем предоставляет тело. Однако тело теперь записано как значение функции, заключённое в скобки вызова repeat. Вот почему оно должно быть закрыто правой круглой скобкой и правой фигурной скобкой. В этом примере тело представляет собой однострочное выражение, вы также можете опустить фигурные скобки и записать цикл в виде одной строки.

Высшие функции

Если функция работает с другими функциями, то есть принимает другие функции в качестве параметров или возвращает функции в качестве значений, мы можем назвать её высшей функцией. Поскольку мы видели, что функция — это обычное значение, концепция высших функций не является чем-то необычным. Термин «высший» происходит из математики, где понятия функции и значения строго различаются.

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

function greaterThan(n) {
  return m => m > n;
}
let greaterThan10 = greaterThan(10);
console.log(greaterThan10(11));
// → true

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

function noisy(f) {
  return (...args) => {
    console.log("calling with", args);
    let result = f(...args);
    console.log("called with", args, ", returned", result);
    return result;
  };
}
noisy(Math.min)(3, 2, 1);
// → calling with [3, 2, 1]
// → called with [3, 2, 1], returned 1

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

function unless(test, then) {
  if (!test) then();
}
repeat(3, n => {
  unless(n % 2 == 1, () => {
    console.log(n, "is even");
  });
});
// → 0 is even
// → 2 is even

Существует встроенный метод массива, forEach, который обеспечивает аналогичную функциональность цикла for/of в качестве высшей функции.

["A", "B"].forEach(l => console.log(l));
// → A
// → B
``` В данной главе используется система данных, основанная на сценариях, таких как латинский, кириллический или арабский.

Следует помнить о Unicode, системе, которая присваивает каждому символу в письменном языке числовое значение. Большинство этих символов связаны с определённым сценарием. Стандарт включает 140 различных сценариев — 81 из них всё ещё используются, а 59 являются историческими.

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

*Пример данных включает в себя некоторую информацию о 140 сценариях, определённых в Unicode. Связка «SCRIPTS» в «кодовом ящике» предоставляет набор объектов, каждый из которых описывает сценарий.*

```json
{
  name: "Coptic",
  ranges: [[994, 1008], [11392, 11508], [11513, 11520]],
  direction: "ltr",
  year: -200,
  living: false,
  link: "https://en.wikipedia.org/wiki/Coptic_alphabet"
}

Такой объект сообщает название сценария, диапазон Unicode, направление письма (слева направо «ltr», справа налево «rtl», например, для арабского и иврита, или сверху вниз «ttb», например, для монгольского), приблизительное время начала использования и дополнительную информацию, такую как ссылка. Направление может быть «ltr» слева направо, «rtl» справа налево (для арабского языка и иврита) или «ttb» сверху вниз (для монгольского).

Атрибут «ranges» содержит массив диапазонов Unicode, каждый массив имеет два элемента, включая нижний предел и верхний предел. Любой код символа в пределах этих диапазонов будет назначен сценарию. Нижний предел включён (код 994 является коптским символом), а верхний предел исключён (код 1008 не является).

Фильтрация массива

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

function filter(array, test) {
  let passed = [];
  for (let element of array) {
    if (test(element)) {
      passed.push(element);
    }
  }
  return passed;
}

console.log(filter(SCRIPTS, script => script.living));
// → [{name: "Adlam", …}, …]

Эта функция использует параметр «test» (функция значения) для заполнения промежутка в расчётах — решения о том, какие элементы собирать.

Обратите внимание, что функция «filter» не удаляет элементы из текущего массива, а создаёт новый массив и добавляет в него элементы, удовлетворяющие условию. Эта функция является «чистой функцией», поскольку она не изменяет заданный массив.

Как и «forEach», «filter» также является стандартным методом массива. Функция, определённая в этом примере, служит только для демонстрации внутренней реализации. В дальнейшем мы будем использовать следующие методы для фильтрации данных:

console.log(SCRIPTS.filter(s => s.direction == "ttb"));
// → [{name: "Mongolian", …}, …]

Преобразование массива с помощью функции «map»

Предположим, мы уже отфильтровали массив «SCRIPTS», создав массив информации о сценариях. Однако мы хотим создать массив, содержащий имена, так как это более удобно для проверки.

Метод «map» применяет функцию к каждому элементу массива и использует возвращаемое значение для построения нового массива, реализуя операцию преобразования массива. Новый массив имеет ту же длину, что и входной массив, но его содержимое преобразуется в новую форму с помощью применяемой функции.

function map(array, transform) {
  let mapped = [];
  for (let element of array) {
    mapped.push(transform(element));
  }
  return mapped;
}

let rtlScripts = SCRIPTS.filter(s => s.direction == "rtl");
console.log(map(rtlScripts, s => s.name));
// → ["Adlam", "Arabic", "Imperial Aramaic", …]

Подобно «forEach» и «filter», «map» также является стандартным методом массивов.

Использование «reduce» для суммирования данных

Ещё одна распространённая операция с массивами — вычисление одного значения из них. Пример рекурсии, который мы рассмотрели, суммировал ряд чисел, является примером этого. Другой пример — поиск сценария с наибольшим количеством символов.

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

Функция «reduce» принимает три параметра: массив, функцию объединения и начальное значение. Эта функция не так интуитивно понятна, как «filter» и «map», поэтому давайте рассмотрим её подробнее:

function reduce(array, combine, start) {
  let current = start;
  for (let element of array) {
    current = combine(current, element);
  }
  return current;
}

console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0));
// → 10

Массив имеет стандартный метод «reduce», который соответствует функции, которую мы видели ранее, и может упростить объединение операций. Если ваш массив содержит несколько элементов и вы вызываете метод «reduce» без параметра «start», этот метод будет использовать первый элемент массива в качестве начального значения и начнёт объединять операции со второго элемента.

console.log([1, 2, 3, 4].reduce((a, b) => a + b));
// → 10

Для использования «reduce» (дважды) для поиска сценария с наибольшим количеством символов мы можем написать следующее:

function characterCount(script) {
  return script.ranges.reduce((count, [from, to]) => {
    return count + (to - from);
  }, 0);
}

console.log(SCRIPTS.reduce((a, b) => {
  return characterCount(a) < characterCount(b) ? b : a;
}));
// → {name: "Han", …}

Функция «characterCount» уменьшает размер диапазона, назначенного сценарию. Обратите внимание на использование деструктуризации в списке параметров функции объединения. Второй вызов «reduce» сравнивает два сценария и возвращает больший сценарий, используя его для поиска самого большого сценария.

Стандарт Unicode назначает более 89 000 символов китайскому сценарию, который является самым большим письменным языком в наборе данных. Китайские иероглифы используются для китайского, японского и корейского языков. Эти языки имеют много общих символов, хотя они склонны писать их по-разному. (Американская) организация Unicode решила рассматривать их как единую систему письма для сохранения кодов символов. Это называется унификацией китайских иероглифов, и некоторые люди всё ещё очень злятся из-за этого.

Композиционность

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

let biggest = null;
for (let script of SCRIPTS) {
  if (biggest == null ||
      characterCount(biggest) < characterCount(script)) {
    biggest = script;
  }
}
console.log(biggest);
// → {name: "Han", …}

Этот фрагмент кода добавляет больше привязок, хотя он добавляет две строки кода, логика кода по-прежнему легко понятна.

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

function average(array) {
  return array.reduce((a, b) => a + b) / array.length;
}

console.log(Math.round(average(
  SCRIPTS.filter(s => s.living).map(s => s.year))));
// → 1185
console.log(Math.round(average(
  SCRIPTS.filter(s => !s.living).map(s => s.year))));
// → 209

Таким образом, сценарии мёртвых языков в среднем старше, чем живые. Это не особенно значимая или удивительная статистика. Тем не менее, я надеюсь, вы согласитесь, что код, используемый для его вычисления, легко читается. Вы можете думать об этом как о конвейере: мы начинаем со всех сценариев, фильтруем живые (или мёртвые) сценарии, извлекаем время из этих сценариев, усредняем их, а затем округляем результат до ближайшего целого числа. ``` arrays.reduce((acc, arr) => acc.concat(arr), []);


Напишите **высокоуровневую функцию loop**, которая будет предоставлять функционал, аналогичный циклу `for`. Она принимает значение, функцию проверки, функцию обновления и функцию тела. На каждой итерации она сначала проверяет текущее значение цикла с помощью функции проверки и останавливается, если функция проверки возвращает `false`. Затем она вызывает функцию тела, передавая ей текущее значение. Наконец, она вызывает функцию обновления для создания нового значения и начинает всё заново.

При определении функции можно использовать обычный цикл для выполнения фактической итерации.
```js
// Ваш код здесь.

loop(3, n => n > 0, n => n - 1, console.log);
// → 3
// → 2
// → 1

Функция every

Аналогично методу some, в массивах есть метод every. Этот метод возвращает true, если заданная функция возвращает true для каждого элемента массива. В некотором смысле, some — это версия оператора || для массивов, а every похож на оператор &&.

Реализуйте функцию every, которая принимает массив и предикатную функцию в качестве аргументов. Напишите две версии: одну с использованием цикла, другую — с использованием метода some.

function every(array, test) {
  // Ваш код здесь.
}

console.log(every([1, 3, 5], n => n < 10));
// → true
console.log(every([2, 4, 16], n => n < 10));
// → false
console.log(every([], n => n < 10));
// → true

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/wizardforcel-eloquent-js-3e-zh.git
git@api.gitlife.ru:oschina-mirror/wizardforcel-eloquent-js-3e-zh.git
oschina-mirror
wizardforcel-eloquent-js-3e-zh
wizardforcel-eloquent-js-3e-zh
master