Writing in the front

One of the more interesting changes to ES6 functions is the engine optimization of the tail-call system. A tail call is when a function is called as the last statement of another function, like this

function doSomething() {
    return doSomethingElse();
}
Copy the code

In ES5, when doSomethingElse is executed, a stack frame is added, and the corresponding stack frame for doSomething is kept in memory, making it prone to programming problems such as stack overflow and other unexpected errors when looped.

Tail call in ES6

Conditions for tail-call optimization in ES6

  • In strict mode.
  • The last call does not access the current stack frame variable (that is, the function is not a closure)
  • Inside a function, the tail call is the last statement.
  • The result of the last call is returned as a function value.

The tail call does not create a new stack frame, but clears and reuses the current stack frame if the following conditions are met;

"use strict";

// can be optimized
function doSomething(){/ / after optimizationreturn doSomethingElse();
}

// cannot be optimized, additional operations must be added after the return value
function doSomething(){
    return 1+doSomethingElse();
}

// Cannot be optimized, the call is not in the tail
function doSomething(){
    var result = doSomethingElse();
    return result;
}

// cannot optimize, func is a closure
function doSomething(){
    var num=1, func = () = > num;
    return func();
}

Copy the code

Tail call case

The main logic of this code is to achieve the function of factorial


function factorial(n) {
    if (n <= 1) {
        return 1;
    } else {
        return n * factorial(n - 1); }}Copy the code

Cannot be optimized because multiplication is performed before recursive calls; If n is a large number, stack overflow is possible;

'use strict';
function factorial_opt(n, p=1) {
    if (n <= 1) {
        return 1 * p;
    } else {
        let result = n * p
        return factorial_opt(n - 1, result)
    }
}
Copy the code

After optimization, the code uses a parameter to save the last execution result (each call will calculate the result of multiplication), no additional function call is required, and also conforms to the characteristics of optimization;

Verification Results (I)

Open the Google Browser console run code

It doesn’t seem like we wanted to open the Node test code

V10.24.1 $node - v $node foo js/Users/shuai/workspace/foo js: 2function factorial_opt(n, p=1) {
                      ^
RangeError: Maximum call stack size exceeded
    at factorial_opt (/Users/shuai/workspace/foo.js:2:23)
    ...
Copy the code

The magic thing is that the error message appears and the optimized code is not working in line with Google Browser.

Finally, the following result is obtained through Google query, which is transferred from Github Issue Comment

Tail-call optimization was implemented in V8 but abandoned, mainly because implementing tail-call optimization meant modifying the call stack during code execution, which resulted in incomplete information about the execution flow and caused two problems:

For these reasons, the V8 team recommends changing the specification to STC(Syntatic Tail Calls) instead of PTC(Proper Tail Calls). That is, use a specific syntax for trailing calls, such as return continue func(). Currently not supported by V8, it provided — Harmony-explicit-tailcalls and — Harmony-explicit-tailcalls to enable tail-call optimization, but has since been removed.

Verification Results (II)

After a lot of hard work, Safari finally found a way to support tail-call optimizations

The final result is as follows:

  • Unoptimized execution results

  • Optimized execution result

RangeError: Maximum Call stack size exceeded The optimized code executes normally and efficiently.

Fibonacci numbers

// The general implementation
function fibonacci(n) {
    if(n==0 || n == 1)
        return n;
    return fibonacci(n-1) + fibonacci(n-2);
}

// After the tail-call is optimized
'use strict'
function fibonacci_opt(n, current = 0, next = 1) {
    if(n <= 1) {
        return next
    }
    return fibonacci_opt(n - 1, next, next+current)
}

fibonacci_opt(5)
Copy the code

conclusion

Although tail-call optimization is part of the ES6 specification, it is not implemented in most JS engines. Optimisation -> Proper Tail Calls (Tail Call Optimisation)

Keep tail calls in mind when coding recursion code; In cases where the recursive function is evaluated large enough, tail calls can greatly optimize program performance.