admin管理员组

文章数量:1295065

I am having a hard time understanding a bit of example code from the book JavaScript Allongé (free in the online version).

The example code is a function for calculating the circumference for a given diameter. It shows different ways to bind values with names. One way to go about it, according to the book is this:

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

It further states:

Well, the wrinkle with this is that typically, invoking functions is considerably more expensive than evaluating expressions. Every time we invoke the outer function, we’ll invoke the inner function. We could get around this by writing

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

I cannot understand how it gets around the situation with calling two functions, aren't there exactly two function calls in both the examples? How do they differ from each other?

I am having a hard time understanding a bit of example code from the book JavaScript Allongé (free in the online version).

The example code is a function for calculating the circumference for a given diameter. It shows different ways to bind values with names. One way to go about it, according to the book is this:

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

It further states:

Well, the wrinkle with this is that typically, invoking functions is considerably more expensive than evaluating expressions. Every time we invoke the outer function, we’ll invoke the inner function. We could get around this by writing

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

I cannot understand how it gets around the situation with calling two functions, aren't there exactly two function calls in both the examples? How do they differ from each other?

Share edited Aug 7, 2016 at 16:14 Peter Mortensen 31.6k22 gold badges110 silver badges133 bronze badges asked Aug 7, 2016 at 11:52 userid1765userid1765 1035 bronze badges 3
  • 1 I might be mistaken, but as far as I can tell, they only switched the order of the functions, and aren't actually saving anything in terms of performance. – Madara's Ghost Commented Aug 7, 2016 at 11:59
  • 1 It's somewhat misleading to say that one is "considerably more expensive" than the other. Apart from a very small set of exceptions it makes no practical difference at all. – JJJ Commented Aug 7, 2016 at 12:01
  • 3 I wouldn't trust a book whose author seems unaware of the existence of Math.PI... – Niet the Dark Absol Commented Aug 7, 2016 at 17:30
Add a ment  | 

4 Answers 4

Reset to default 6

This probably looks a bit confusing because I don't think it's explained very well. Or, rather, I don't think it's explained in a typical JavaScript way.

Let's break down the examples

First Example

Breakdown

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

calculateCircumference(2); // 6.2831853

Arranged like this, here is what happens if you call this code

  1. You pass the diameter (e.g., 2)
  2. A new function is created that takes PI as parameter and uses it to calculate the circumference. This function is immediately invoked
  3. The function uses both variables present to do the calculation

Aside from being wasteful putation-wise (two invocation) this example is also convoluted for no good reason. The inner function is pointless and doesn't gain you anything. It's probably where the example loses lots of its clarity - seems like the only reason to have the example work as it is, is to set up for the second example.

Second Example

On currying

Before tackling the example, it seems like the book probably failed to mention how exactly it works. The second example leverages a technique called curry which is used in functional programming - it is not specific to JavaScript but it is still widely known as that name in the JavaScript world. A very brief overview of 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

I will not go into detail but essentially, a curried function that takes multiple parameters, can be passed less and it will return a new function that can take the rest. When all the parameters are satisfied, you will get the result - in a formal notation, the curryAdd function will be expressed as curryAdd :: Number -> Number -> Number - it's a function that takes a number and returns another function that takes a number which finally returns another number. For why you would want to do that, here is an example - it's trivial but it gets the point accross:

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

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

Currying is a bit like partial allocation of functions but the two are not (necessarily) the same thing.

Breakdown

With that said, let's look at the second example:

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

Hopefully that clarifies what is going on a bit. I'll re-write the rest of the example into what is actually happening:

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

calculateCircumference(2); //6.2831853

The second example's code is equivalent to the above. It avoids invoking a function twice because the outer function (which I dubbed curryMultiply) is invoked only once - any time you call the calculateCircumference function, you are only evaluating the inner function.

I believe the emphasis is on the phrase "Every time we invoke the outer function…", which is indeed confusing as the outer function is only invoked once in the example (as an IEFE). One should be able to grasp the difference better with this example:

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));

But apparently the author does not want to introduce variable declarations here, so maybe it would be written

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

to the same effect :-)

You should have a look at immediately-invoked function expression (IIFE); that's a design pattern...

Basically: You declare a function and invoke it immediately... this was sometimes used as an expedient for creating a lexical scope, just in order to avoid global variables...

// 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'); })();

As you can see, we use parenthesis for wrapping/executing an expression (...) and parenthesis as a function call operator. That mean: evaluate that expression and call what it return.

Of course, because we're using functions, we can pass them arguments:

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

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

The last thing you probably already know is the Arrow Function, introduced by ECMAScript 6:

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

Finally, you're fighting with a nested round trip of IIFE(s).

What the book may suggest is that the JavaScript piler is more likely to inline the PI function in the second method. But this would only make sense if we call these methods several times with different dynamic diameters. Otherwise, the piler is likely to inline the diameter function just as well.

At the end of the day, what really matters from a performance perspective is what the JavaScript engine is really doing with these functions anyway.

Below is a test that suggests that there's almost no difference at all between the two methods. At least on my box.

You may want to execute more iterations, but please note that this is apparently extremely slow on Edge.

// This is a warmup to make sure that both methods are passed through
// Just In Time (JIT) pilation, 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;
}

本文标签: Need help understanding function invocation in JavaScriptStack Overflow