Нужна помощь в понимании вызова функции в JavaScript

Мне сложно понять немного кода примера из книги JavaScript Allongé (бесплатно в онлайн-версии).

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

(
 (diameter) =>
  ((PI) => diameter * PI)(3.14159265)
)(2);
// calculates circumference given diameter 2

Далее говорится:

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

(
 ((PI) =>
   (diameter) => diameter * PI
 )(3.14159265)
)(2);

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

Ответ 1

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

Разбейте примеры

Первый пример

Структура

var calculateCircumference = (diameter) => (
    (PI) => diameter * PI)(3.14159265)
);

calculateCircumference(2); // 6.2831853

Астрологически, вот что происходит, если вы вызываете этот код

  • Вы передаете диаметр (например, 2)
  • Создается новая функция, которая принимает PI как параметр и использует ее для вычисления окружности. Эта функция немедленно вызывается
  • Функция использует обе переменные, присутствующие для вычисления

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

Второй пример

При каррировании

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

//non-curried
function add(a, b) { // or, in ES6: (a, b) => a + b;
    return a + b;
}

//curried
function curryAdd(a) { //or in ES6: (a) => (b) => a + b;
    return function(b) {
        return a + b;
    }
}

//invocation
add(2, 3); // 5
curryAdd(2)(3); // 5

Я не буду вдаваться в детали, но, по существу, функция в карри, которая принимает несколько параметров, может быть передана меньше, и она вернет новую функцию, которая может взять все остальное. Когда все параметры будут выполнены, вы получите результат - в формальной нотации функция curryAdd будет выражаться как curryAdd :: Number -> Number -> Number - это функция, которая принимает число и возвращает другую функцию, которая принимает число, которое, наконец, возвращает другое номер. Для чего вы хотели бы это сделать, вот пример - это тривиально, но он получает точку:

//add5:: Number -> Number
add5 = curryAdd(5);

add5(3); // 8
add5(10); // 15
[1, 2, 3].map(add5); // [6, 7, 8]

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

Структура

С учетом сказанного рассмотрим второй пример:

//curryMultiply :: Float -> Float -> Float
(PI) => (diameter) => diameter * PI
//another way to write it:
//(a) => (b) => a * b

Надеюсь, это пояснит, что происходит. Я переписал остальную часть примера в то, что на самом деле происходит:

// calculateCircumference :: Float -> Float
var calculateCircumference = curryMultiply(3.14159265);

calculateCircumference(2); //6.2831853

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

Ответ 2

Вам следует взглянуть на выражение с мгновенным вызовом функции (IIFE); что шаблон дизайна...

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

// The way we're confident...
function logFoo() { console.log(1, 'FOO'); }
logFoo();

// Using and IIFE
(function() { console.log(2, 'FOO'); }());
// OR for better readability
(function() { console.log(2, 'FOO'); })();

Ответ 3

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

В конце концов, что действительно имеет значение с точки зрения производительности, это то, что механизм JavaScript действительно делает с этими функциями в любом случае.

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

Возможно, вы захотите выполнить больше итераций, но учтите, что это, по-видимому, чрезвычайно медленное на Edge.

// This is a warmup to make sure that both methods are passed through
// Just In Time (JIT) compilation, for browsers doing it that way.
test1(1E5);
test2(1E5);

// Perform actual test
console.log('Method #1: ' + test1(1E6).toFixed(2) + 'ms');
console.log('Method #2: ' + test2(1E6).toFixed(2) + 'ms');

function test1(iter) {
  var res, n, ts = performance.now();

  for(n = 0; n < iter; n++) {
    res = (
      (diameter) => ((PI) => diameter * PI)(3.14159265)
    )(Math.random() * 10);
  }
  return performance.now() - ts;
}

function test2(iter) {
  var res, n, ts = performance.now();

  for(n = 0; n < iter; n++) {
    res = (
      ((PI) => (diameter) => diameter * PI)(3.14159265)
    )(Math.random() * 10);
  }
  return performance.now() - ts;
}

Ответ 4

Я считаю, что акцент делается на фразе "Каждый раз, когда мы вызываем внешнюю функцию...", что действительно запутывает, поскольку внешняя функция используется только один раз в примере (как IEFE). В этом примере лучше понять разницу:

const circumference = (diameter) => 
  ((PI) =>
    diameter * PI
  )(3.14159265);
console.log(circumference(2));
console.log(circumference(5));

const circumference = ((PI) =>
  (diameter) =>
    diameter * PI
)(3.14159265);
console.log(circumference(2));
console.log(circumference(5));

Но, видимо, автор не хочет вводить здесь объявления переменных, поэтому, возможно, это будет написано

((circumference) => {
  console.log(circumference(2));
  console.log(circumference(5));
})(((PI) =>
  (diameter) =>
    diameter * PI
)(3.14159265));

к тому же эффекту: -)