问题 需要帮助理解JavaScript中的函数调用


我很难理解本书中的一些示例代码 JavaScriptAllongé (在线版免费)。

示例代码是用于计算给定直径的周长的函数。它显示了使用名称绑定值的不同方法。根据这本书,一种方法是:

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

它进一步指出:

好吧,与此相关的皱纹通常是,调用函数比评估表达式要昂贵得多。每次调用外部函数时,我们都会调用内部函数。我们可以通过写作解决这个问题

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

我无法理解如何通过调用两个函数来解决这种情况,两个函数中是不是只有两个函数调用? 他们如何彼此不同?


5540
2017-08-07 11:52


起源

我可能会弄错,但据我所知,他们只是改变了功能的顺序,实际上并没有在性能方面保存任何东西。 - Madara Uchiha♦
说一个人比另一个人“相当昂贵”,这有点误导。除了一小部分例外之外,它根本没有实际意义。 - JJJ
我不相信一本作者似乎不知道存在的书 Math.PI... - Niet the Dark Absol


答案:


这可能看起来有点令人困惑,因为我认为它没有得到很好的解释。或者,我认为它不是以典型的JavaScript方式解释的。

让我们分解一下这些例子

第一个例子

分解

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

calculateCircumference(2); // 6.2831853

安排如下,如果您调用此代码,会发生以下情况

  1. 你通过直径(例如,2)
  2. 一个  创建的函数 PI 作为参数并使用它来计算周长。立即调用此函数
  3. 该函数使用两个变量来进行计算

除了浪费计算方式(两次调用)之外,这个例子也没有充分的理由进行复杂化。内在的功能是毫无意义的,并没有获得任何东西。这可能是这个例子失去了很多清晰度的地方 - 似乎是让这个例子按原样工作的唯一原因,就是设置第二个例子。

第二个例子

在currying上

在解决这个例子之前,似乎这本书可能没有提到它究竟是如何工作的。第二个例子利用了一种称为的技术 curry 它在函数式编程中使用 - 它不是特定于JavaScript,但它仍然被广泛称为JavaScript世界中的名称。关于currying的简要概述

//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

我不会详细介绍,但基本上,一个带有多个参数的curried函数可以传递得更少,它将返回一个可以接受其余部分的新函数。当满足所有参数时,您将获得结果 - 以正式表示法,即 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 功能,你只是在评估  功能。


6
2017-08-07 12:47



我不认为这本书试图在那里做currying,而是以Scheme方式解释名称绑定。否则,+1 - Bergi
@Bergi我也不认为它试图做currying,但这是有效的事情。我认为理解currying会导致理解代码,因为它所做的只是评估从其他函数返回的一系列函数。 - vlaz
@vid非常好解释非常感谢。你们是对的这个例子是一个用来表示使用const的情况的设置,它继续在后面的例子中使用const作为PI值。 - userid1765


你应该看看 立即调用的函数表达式 (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'); })();

如您所见,我们使用括号来包装/执行表达式 (...) 和括号作为 function call operator。这意味着: 评估该表达式并调用它返回的内容

当然,因为我们正在使用函数,所以我们可以传递它们的参数:

function log(what) { console.log(3, what); }
log('Foo');

// IIFE
(function(what) { console.log(4, what); })('Foo');

你可能已经知道的最后一件事是 Arrow Function,介绍 ECMAScript中6

(what => console.log(what))('Foo');

最后,你正在与IIFE的嵌套往返战斗。


3
2017-08-07 13:06



谢谢你很好地解释 - userid1765


我相信重点是“每次我们调用外部函数时......“,这确实令人困惑,因为外部函数仅在示例中被调用一次(作为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));

达到同样的效果:-)


2
2017-08-07 13:16



精彩回答!我花了一段时间才得到这个,但我明白了最后会发生什么。谢谢! - userid1765


本书可能暗示的是JavaScript编译器更有可能 一致 第二种方法中的PI函数。但是,如果我们用不同的动态直径多次调用这些方法,这才有意义。否则,编译器也可能同样内联直径函数。

在一天结束时,从性能角度来看真正重要的是JavaScript引擎是什么  无论如何都要做这些功能。

以下是一项测试,表明两种方法之间几乎没有差异。至少在我的盒子上。

您可能希望执行更多迭代,但请注意,这显然非常慢 边缘

// 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;
}


2
2017-08-07 12:47



非常感谢你今天学到了一些关于Inline扩展的新知识 - userid1765