Function

Default value of function parameters

Basic usage

Before ES6, it was not possible to directly specify default values ​​for function parameters, and only workarounds could be used.

function log(x, y) {
  y = y || "World";
  console.log(x, y);
}

log("Hello"); // Hello World
log("Hello", "China"); // Hello China
log("Hello", ""); // Hello World

The above code checks whether the parameter y of the function log is assigned a value, if not, the default value is specified as World. The disadvantage of this way of writing is that if the parameter y is assigned, but the corresponding boolean value is false, the assignment will not work. Just like the last line of the above code, the parameter y is equal to the null character, and the result is changed to the default value.

In order to avoid this problem, it is usually necessary to first determine whether the parameter y is assigned a value, if not, then it is equal to the default value.

if (typeof y === "undefined") {
  y = "World";
}

ES6 allows to set default values ​​for the parameters of the function, that is, write directly after the parameter definition.

function log(x, y = "World") {
  console.log(x, y);
}

log("Hello"); // Hello World
log("Hello", "China"); // Hello China
log("Hello", ""); // Hello

As you can see, ES6 is much more concise and very natural than ES5. Here is another example.

function Point(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}

const p = new Point();
p; // {x: 0, y: 0}

In addition to conciseness, ES6's writing method has two advantages: first, people who read the code can immediately realize which parameters can be omitted without checking the function body or documentation; secondly, it is conducive to future code optimization, even in future versions In the external interface, completely removing this parameter will not cause the previous code to fail to run.

Parameter variables are declared by default, so they cannot be declared again with let or const.

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

In the above code, the parameter variable x is declared by default. In the function body, it cannot be declared again with let or const, otherwise an error will be reported.

When using parameter default values, functions cannot have parameters with the same name.

// No error
function foo(x, x, y) {
  // ...
}

// report an error
function foo(x, x, y = 1) {
  // ...
}
// SyntaxError: Duplicate parameter name not allowed in this context

In addition, one thing that is easy to overlook is that the parameter default value is not passed by value, but the value of the default value expression is recalculated every time. In other words, parameter default values ​​are evaluated lazily.

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo(); // 100

x = 100;
foo(); // 101

In the above code, the default value of the parameter p is x + 1. At this time, every time the function foo is called, x + 1 will be recalculated instead of the default p being equal to 100.

Used in conjunction with destructuring assignment default values

The parameter default value can be used in combination with the default value of the destructuring assignment.

function foo({ x, y = 5 }) {
  console.log(x, y);
}

foo({}); // undefined 5
foo({ x: 1 }); // 1 5
foo({ x: 1, y: 2 }); // 1 2
foo(); // TypeError: Cannot read property'x' of undefined

The above code only uses the default value of the destructuring assignment of the object, and does not use the default value of the function parameter. Only when the parameter of the function foo is an object, the variables x and y will be generated by destructuring assignment. If the function foo is called without providing parameters, the variables x and y will not be generated and an error will be reported. This can be avoided by providing default values ​​for function parameters.

function foo({ x, y = 5 } = {}) {
  console.log(x, y);
}

foo(); // undefined 5

The above code specifies that if no parameter is provided, the parameter of the function foo defaults to an empty object.

The following is another example of destructuring and assigning default values.

function fetch(url, { body = "", method = "GET", headers = {} }) {
  console.log(method);
}

fetch("http://example.com", {});
// "GET"

fetch("http://example.com");
// report an error

In the above code, if the second parameter of the function fetch is an object, you can set default values ​​for its three properties. This way of writing cannot omit the second parameter. If combined with the default value of the function parameter, the second parameter can be omitted. At this time, there is a double default value.

function fetch(url, { body = "", method = "GET", headers = {} } = {}) {
  console.log(method);
}

fetch("http://example.com");
// "GET"

In the above code, when the function fetch has no second parameter, the default value of the function parameter will take effect, and then the default value of the destructuring assignment will take effect, and the variable method will get the default value GET.

As an exercise, what is the difference between the following two ways of writing?

// Writing method one
function m1({ x = 0, y = 0 } = {}) {
  return [x, y];
}

// Writing method two
function m2({ x, y } = { x: 0, y: 0 }) {
  return [x, y];
}

Both of the above two ways of writing set default values ​​for the function parameters. The difference is that the default value of writing one function parameter is an empty object, but the default value of object destructuring assignment is set; the default value of writing two function parameter is a specific attribute The object, but the default value of the object destructuring assignment is not set.

// When the function has no parameters
m1(); // [0, 0]
m2(); // [0, 0]

// When both x and y have values
m1({ x: 3, y: 8 }); // [3, 8]
m2({ x: 3, y: 8 }); // [3, 8]

// The case where x has a value and y has no value
m1({ x: 3 }); // [3, 0]
m2({ x: 3 }); // [3, undefined]

// When both x and y have no value
m1({}); // [0, 0];
m2({}); // [undefined, undefined]

m1({ z: 3 }); // [0, 0]
m2({ z: 3 }); // [undefined, undefined]

The location of the parameter default value

Normally, the parameter that defines the default value should be the last parameter of the function. Because it is easier to see which parameters are omitted. If the non-tail parameter is set to the default value, in fact this parameter cannot be omitted.

// Example 1
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // error
f(undefined, 1) // [1, 1]

// Example 2
function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // report an error
f(1, undefined, 2) // [1, 5, 2]

In the above code, the parameters with default values ​​are not tail parameters. At this time, it is not possible to omit this parameter without omitting the following parameters, unless you explicitly enter undefined.

If you pass in undefined, the parameter will be triggered to be equal to the default value, and null will have no such effect.

function foo(x = 5, y = 6) {
  console.log(x, y);
}

foo(undefined, null);
// 5 null

In the above code, the x parameter corresponds to undefined, and the default value is triggered as a result, and the y parameter is equal to null, so the default value is not triggered.

The length property of the function

After the default value is specified, the length property of the function will return the number of parameters for which no default value is specified. In other words, after specifying the default value, the length property will be distorted.

(function (a) {}
  .length(
    // 1
    function (a = 5) {}
  )
  .length(
    // 0
    function (a, b, c = 5) {}
  ).length); // 2

In the above code, the return value of the length property is equal to the number of parameters of the function minus the number of parameters with default values ​​specified. For example, the last function above defines 3 parameters, one of which is c specifies the default value, so the length attribute is equal to 3 minus 1, and finally 2 is obtained.

This is because the meaning of the length property is the number of parameters the function expects to pass in. After a parameter is assigned a default value, the expected number of incoming parameters does not include this parameter. For the same reason, the rest parameter below will not be included in the length property.

(function (...args) {}.length); // 0

If the parameter with the default value is not the last parameter, the length attribute will no longer be counted in the subsequent parameters.

(function (a = 0, b, c) {}.length(
  // 0
  function (a, b = 1, c) {}
).length); // 1

Scope

Once the default value of the parameter is set, when the function is declared and initialized, the parameter will form a separate scope (context). When the initialization is over, this scope will disappear. This kind of grammatical behavior will not appear when the parameter default value is not set.

var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2); // 2

In the above code, the default value of the parameter y is equal to the variable x. When calling the function f, the parameters form a separate scope. In this scope, the default value variable x points to the first parameter x instead of the global variable x, so the output is 2.

Look at the following example again.

let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f(); // 1

In the above code, when the function f is called, the parameter y = x forms a separate scope. In this scope, the variable x itself is not defined, so it points to the outer global variable x. When the function is called, the local variable x inside the function body does not affect the default value variable x.

If at this time, the global variable x does not exist, an error will be reported.

function f(y = x) {
  let x = 2;
  console.log(y);
}

f(); // ReferenceError: x is not defined

If you write like this below, you will get an error.

var x = 1;

function foo(x = x) {
  // ...
}

foo(); // ReferenceError: x is not defined

In the above code, the parameter x = x forms a separate scope. The actual execution is let x = x. Due to the temporary dead zone, this line of code will report the error "x is not defined".

If the default value of the parameter is a function, the scope of the function also obeys this rule. Please see the example below.

let foo = "outer";

function bar(func = () => foo) {
  let foo = "inner";
  console.log(func());
}

bar(); // outer

In the above code, the default value of the parameter func of the function bar is an anonymous function, and the return value is the variable foo. In the separate scope formed by the function parameters, the variable foo is not defined, so foo points to the outer global variable foo, so outer is output.

If it is written as follows, an error will be reported.

function bar(func = () => foo) {
  let foo = "inner";
  console.log(func());
}

bar(); // ReferenceError: foo is not defined

In the above code, foo in the anonymous function points to the outer layer of the function, but the outer layer of the function does not declare the variable foo, so an error is reported.

Here is a more complicated example.

var x = 1;
function foo(
  x,
  y = function () {
    x = 2;
  }
) {
  var x = 3;
  y();
  console.log(x);
}

foo(); // 3
x; // 1

In the above code, the parameters of the function foo form a single scope. In this scope, the variable x is declared first, and then the variable y is declared. The default value of y is an anonymous function. The variable x inside this anonymous function points to the first parameter x in the same scope. The function foo declares an internal variable x. This variable and the first parameter x are not the same variable because they are not in the same scope. Therefore, after executing y, the internal variable x And the value of the external global variable x has not changed.

If the var of var x = 3 is removed, the internal variable x of the function foo points to the first parameter x, which is consistent with the x inside the anonymous function, so the final output That is 2, and the outer global variable x is still unaffected.

var x = 1;
function foo(
  x,
  y = function () {
    x = 2;
  }
) {
  x = 3;
  y();
  console.log(x);
}

foo(); // 2
x; // 1

Application

Using the parameter default value, you can specify that a certain parameter cannot be omitted, and if it is omitted, an error will be thrown.

function throwIfMissing() {
  throw new Error("Missing parameter");
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo();
// Error: Missing parameter

If the foo function in the above code is called without parameters, it will call the default value throwIfMissing function, thus throwing an error.

You can also see from the above code that the default value of the parameter mustBeProvided is equal to the running result of the throwIfMissing function (note that there is a pair of parentheses after the function name throwIfMissing), which indicates that the default value of the parameter is not executed at the time of definition, but It is executed at runtime. If the parameter has been assigned, the function in the default value will not run.

In addition, the default value of the parameter can be set to undefined, indicating that this parameter can be omitted.

function foo(optional = undefined) {···}

rest parameters

ES6 introduces the rest parameter (in the form of ...variable name), which is used to obtain redundant parameters of the function, so that there is no need to use the arguments object. The variable with the rest parameter is an array, and the variable puts the extra parameters into the array.

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3); // 10

The add function in the above code is a summation function. With the rest parameter, any number of parameters can be passed to the function.

The following is an example of the rest parameter instead of the arguments variable.

// How to write the arguments variable
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// How to write rest parameters
const sortNumbers = (...numbers) => numbers.sort();

After comparing the two ways of writing the above code, it can be found that the way of writing the rest parameter is more natural and concise.

The arguments object is not an array, but an array-like object. So in order to use the array method, you must first convert it to an array using Array.prototype.slice.call. The rest parameter does not have this problem, it is a real array, and all methods specific to the array can be used. The following is an example of using the rest parameter to rewrite the array push method.

function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3);

Note that there can be no other parameters after the rest parameter (that is, it can only be the last parameter), otherwise an error will be reported.

// report an error
function f(a, ...b, c) {
  // ...
}

The length property of the function does not include the rest parameter.

(function (a) {}
  .length(
    // 1
    function (...a) {}
  )
  .length(
    // 0
    function (a, ...b) {}
  ).length); // 1

Strict mode

Starting from ES5, the function can be set to strict mode.

function doSomething(a, b) {
  "use strict";
  // code
}

ES2016 made a little modification, stipulating that as long as the function parameters use default values, destructuring assignments, or spread operators, the function cannot be explicitly set to strict mode, otherwise an error will be reported.

// report an error
function doSomething(a, b = a) {
  'use strict';
  // code
}

// report an error
const doSomething = function ({a, b}) {
  'use strict';
  // code
};

// report an error
const doSomething = (...a) => {
  'use strict';
  // code
};

const obj = {
  // report an error
  doSomething({a, b}) {
    'use strict';
    // code
  }
};

The reason for this provision is that the strict mode inside the function applies to both the function body and the function parameters. However, when the function is executed, the function parameters are executed first, and then the function body is executed. In this way, there is an unreasonable place. Only from the function body can we know whether the parameter should be executed in strict mode, but the parameter should be executed before the function body.

// report an error
function doSomething(value = 070) {
  'use strict';
  return value;
}

In the above code, the default value of the parameter value is the octal number 070, but the prefix 0 cannot be used to represent the octal system in strict mode, so an error should be reported. But in fact, the JavaScript engine will first successfully execute value = 070, then enter the function body, and find that it needs to be executed in strict mode, then it will report an error.

Although it is possible to parse the function body code first, and then execute the parameter code, this will undoubtedly increase the complexity. Therefore, the standard simply prohibits this usage. As long as the parameters use default values, destructuring assignments, or spread operators, strict mode cannot be explicitly specified.

Two methods can circumvent this restriction. The first is to set a global strict mode, which is legal.

"use strict";

function doSomething(a, b = a) {
  // code
}

The second is to wrap the function in an immediate execution function with no parameters.

const doSomething = (function () {
  "use strict";
  return function (value = 42) {
    return value;
  };
})();

name attribute

The name attribute of a function returns the name of the function.

function foo() {}
foo.name; // "foo"

This attribute has long been widely supported by browsers, but it was not written into the standard until ES6.

It should be noted that ES6 has made some changes to the behavior of this attribute. If you assign an anonymous function to a variable, the name property of ES5 will return an empty string, while the name property of ES6 will return the actual function name.

var f = function () {};

// ES5
f.name; // ""

// ES6
f.name; // "f"

In the above code, the variable f is equal to an anonymous function, and the value returned by the name property of ES5 and ES6 is different.

If you assign a named function to a variable, the name properties of ES5 and ES6 both return the original name of the named function.

const bar = function baz() {};

// ES5
bar.name; // "baz"

// ES6
bar.name; // "baz"

The function instance returned by the Function constructor, the value of the name attribute is anonymous.

new Function().name; // "anonymous"

For the function returned by bind, the value of the name attribute will be prefixed with bound.

function foo() {}
foo
  .bind({})
  .name(
    // "bound foo"

    function () {}
  )
  .bind({}).name; // "bound "

Arrow function

Basic usage

ES6 allows the use of "arrows" (=>) to define functions.

var f = (v) => v;

// Equivalent to
var f = function (v) {
  return v;
};

If the arrow function does not require parameters or requires multiple parameters, use a parenthesis to represent the parameter part.

var f = () => 5;
// Equivalent to
var f = function () {
  return 5;
};

var sum = (num1, num2) => num1 + num2;
// Equivalent to
var sum = function (num1, num2) {
  return num1 + num2;
};

If there is more than one statement in the code block of the arrow function, use curly braces to enclose them and use the return statement to return.

var sum = (num1, num2) => {
  return num1 + num2;
};

Because the braces are interpreted as code blocks, if the arrow function returns an object directly, you must add the parentheses outside the object, otherwise an error will be reported.

// report an error
let getTempItem = id => {id: id, name: "Temp" };

// No error
let getTempItem = id => ({ id: id, name: "Temp" });

The following is a special case, although it can run, but will get wrong results.

let foo = () => {
  a: 1;
};
foo(); // undefined

In the above code, the original intention is to return an object { a: 1 }, but because the engine considers the braces to be a code block, a line of statement a: 1 is executed. At this time, a can be interpreted as the label of the statement, so the statement actually executed is 1;, and then the function ends with no return value.

If the arrow function has only one line of statement and does not need to return a value, you can use the following writing method without writing curly braces.

let fn = () => void doesNotReturn();

Arrow functions can be used in conjunction with variable destructuring.

const full = ({ first, last }) => first + "" + last;

// Equivalent to
function full(person) {
  return person.first + "" + person.last;
}

Arrow functions make the expression more concise.

const isEven = (n) => n % 2 === 0;
const square = (n) => n * n;

The above code only uses two lines to define two simple utility functions. If you don't use arrow functions, you may have to take up multiple lines, and it's not as eye-catching as it is now.

One use of arrow functions is to simplify callback functions.

// Common function writing
[1, 2, 3].map(function (x) {
  return x * x;
});

// Arrow function writing
[1, 2, 3].map((x) => x * x);

Another example is

// Common function writing
var result = values.sort(function (a, b) {
  return a - b;
});

// Arrow function writing
var result = values.sort((a, b) => a - b);

The following is an example of combining the rest parameter with an arrow function.

const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5);
// [1,2,3,4,5]

const headAndTail = (head, ...tail) => [head, tail];

headAndTail(1, 2, 3, 4, 5);
// [1,[2,3,4,5]]

Use precautions

There are several points to note when using arrow functions.

(1) The arrow function does not have its own this object (see below for details).

(2) It cannot be used as a constructor, that is, you cannot use the new command on an arrow function, otherwise an error will be thrown.

(3) The arguments object cannot be used, which does not exist in the function body. If you want to use it, you can use the rest parameter instead.

(4) The yield command cannot be used, so arrow functions cannot be used as generator functions.

Of the four points above, the most important is the first point. For ordinary functions, the internal this points to the object where the function is running, but this is not true for arrow functions. It does not have its own this object, and the internal this is the this in the upper scope at the time of definition. In other words, the direction of this inside arrow functions is fixed, in contrast, the direction of this of ordinary functions is variable.

function foo() {
  setTimeout(() => {
    console.log("id:", this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

In the above code, the parameter of setTimeout() is an arrow function. The definition of this arrow function takes effect when the foo function is generated, and its actual execution has to wait until 100 milliseconds later. If it is a normal function, when executed, this should point to the global object window, and then 21 should be output. However, the arrow function causes this to always point to the object where the function definition takes effect (in this case, {id: 42}), so 42 is printed out.

The following example shows that the callback functions are arrow functions and ordinary functions respectively. Compare their internal this points.

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // Arrow function
  setInterval(() => this.s1++, 1000);
  // Ordinary function
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log("s1:", timer.s1), 3100);
setTimeout(() => console.log("s2:", timer.s2), 3100);
// s1: 3
// s2: 0

In the above code, two timers are set up inside the Timer function, which use arrow functions and ordinary functions respectively. The this of the former is bound to the scope of the definition (ie the Timer function), and the this of the latter points to the scope of the runtime (ie the global object). So, after 3100 milliseconds, timer.s1 has been updated 3 times, but timer.s2 has not been updated once.

Arrow functions can actually make this point to be fixed, and bind this to make it no longer mutable. This feature is very helpful for encapsulating callback functions. The following is an example, the callback function of DOM event is encapsulated in an object.

var handler = {
  id: "123456",

  init: function () {
    document.addEventListener(
      "click",
      (event) => this.doSomething(event.type),
      false
    );
  },

  doSomething: function (type) {
    console.log("Handling " + type + " for " + this.id);
  },
};

In the init() method of the above code, an arrow function is used, which causes the this in the arrow function to always point to the handler object. If the callback function is a normal function, then running the line this.doSomething() will report an error, because at this time this points to the document object.

In short, the arrow function does not have its own this at all, resulting in the inner this being the this of the outer code block. Because it does not have this, it cannot be used as a constructor.

Below is the ES5 code generated by Babel to the arrow function, which can clearly illustrate the direction of this.

// ES6
function foo() {
  setTimeout(() => {
    console.log("id:", this.id);
  }, 100);
}

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log("id:", _this.id);
  }, 100);
}

In the above code, the converted ES5 version clearly shows that the arrow function does not have its own this at all, but refers to the outer this.

In the following code, how many points does this point to?

function foo() {
  return () => {
    return () => {
      return () => {
        console.log("id:", this.id);
      };
    };
  };
}

var f = foo.call({ id: 1 });

var t1 = f.call({ id: 2 })()(); // id: 1
var t2 = f().call((id: 3))(); // id: 1
var t3 = f()().call({ id: 4 }); // id: 1

The answer is that this only points to one point, which is the this of function foo. This is because all inner functions are arrow functions and do not have their own this, and their this are actually the most The this of the outer foo function. So no matter how nested, t1, t2, and t3 all output the same result. If all the inner functions in this example are written as ordinary functions, then the this of each function points to a different object at runtime.

Except for this, the following three variables also do not exist in the arrow function, pointing to the corresponding variables of the outer function: arguments, super, new.target.

function foo() {
  setTimeout(() => {
    console.log("args:", arguments);
  }, 100);
}

foo(2, 4, 6, 8);
// args: [2, 4, 6, 8]

In the above code, the variable arguments inside the arrow function is actually the arguments variable of the function foo.

In addition, since the arrow function does not have its own this, of course, you cannot use the methods of call(), apply(), and bind() to change the direction of this.

(function () {
  return [(() => this.x).bind({ x: "inner" })()];
}.call({ x: "outer" }));
// ['outer']

In the above code, the arrow function does not have its own this, so the bind method is invalid, and the inner this points to the outer this.

The this object of JavaScript language has been a headache for a long time. You must be very careful when using this in object methods. The arrow function "bind" this, to a large extent solve this problem.

Not applicable occasions

Since arrow functions change this from "dynamic" to "static", arrow functions should not be used in the following two situations.

The first occasion is to define the method of the object, and the method includes this.

const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  },
};

In the above code, the cat.jumps() method is an arrow function, which is wrong. When calling cat.jumps(), if it is a normal function, this inside the method points to cat; if written as an arrow function as above, it makes this point to the global object, so the expected result will not be obtained. This is because the object does not constitute a separate scope, so the scope of the jumps arrow function definition is the global scope.

Let's look at another example.

globalThis.s = 21;

const obj = {
  s: 42,
  m: () => console.log(this.s),
};

obj.m(); // 21

In the above example, obj.m() is defined using arrow functions. The processing method of the JavaScript engine is to first generate the arrow function in the global space, and then assign it to obj.m, which causes the this inside the arrow function to point to the global object, so the output of obj.m() is the global The 21 of the space, not the 42 inside the object. The above code is actually equivalent to the following code.

globalThis.s = 21;
globalThis.m = () => console.log(this.s);

const obj = {
  s: 42,
  m: globalThis.m,
};

obj.m(); // 21

For the above reason, it is recommended to use traditional writing to define the properties of objects instead of using arrow functions.

The second occasion is when dynamic this is needed, and arrow functions should not be used either.

var button = document.getElementById("press");
button.addEventListener("click", () => {
  this.classList.toggle("on");
});

When the above code is running, an error will be reported when the button is clicked, because the listening function of button is an arrow function, resulting in this inside being a global object. If it is changed to a normal function, this will dynamically point to the button object that was clicked.

In addition, if the function body is very complicated, there are many lines, or there are a lot of read and write operations inside the function, not only for calculating the value, then you should not use arrow functions, but use ordinary functions, which can improve the code performance. Readability.

Nested arrow functions

Inside the arrow function, you can also use the arrow function. The following is a multiple nested function in ES5 syntax.

function insert(value) {
  return {
    into: function (array) {
      return {
        after: function (afterValue) {
          array.splice(array.indexOf(afterValue) + 1, 0, value);
          return array;
        },
      };
    },
  };
}

insert(2).into([1, 3]).after(1); //[1, 2, 3]

The above function can be rewritten using arrow functions.

let insert = (value) => ((into: (array) => ((after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  return array;
}})});

insert(2).into([1, 3]).after(1); //[1, 2, 3]

The following is an example of deploying a pipeline mechanism (pipeline), that is, the output of the previous function is the input of the next function.

const pipeline =
  (...funcs) =>
  (val) =>
    funcs.reduce((a, b) => b(a), val);

const plus1 = (a) => a + 1;
const mult2 = (a) => a * 2;
const addThenMult = pipeline(plus1, mult2);

addThenMult(5);
// 12

If you feel that the above writing is less readable, you can also use the following writing.

const plus1 = (a) => a + 1;
const mult2 = (a) => a * 2;

mult2(plus1(5));
// 12

The arrow function has another function, that is, it can easily rewrite the λ calculus.

// How to write λ calculus
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

// How to write ES6
var fix = f => (x => f(v => x(x)(v)))
               (x => f(v => x(x)(v)));

The above two writing methods are almost one-to-one correspondence. Since λ-calculus is very important for computer science, this allows us to use ES6 as an alternative tool to explore computer science.

Tail call optimization

What is a tail call?

Tail Call is an important concept of functional programming. It is very simple and can be said clearly in one sentence, which means that the last step of a function is to call another function.

function f(x) {
  return g(x);
}

In the above code, the last step of function f is to call function g, which is called tail call.

The following three cases are not tail calls.

// case one
function f(x) {
  let y = g(x);
  return y;
}

// Situation two
function f(x) {
  return g(x) + 1;
}

// Situation three
function f(x) {
  g(x);
}

In the above code, the first situation is that there is an assignment operation after the function g is called, so it is not a tail call, even if the semantics are exactly the same. Case 2 also belongs to the operation after the call, even if it is written in one line. Case three is equivalent to the following code.

function f(x) {
  g(x);
  return undefined;
}

The tail call does not necessarily appear at the end of the function, as long as it is the last operation.

function f(x) {
  if (x > 0) {
    return m(x);
  }
  return n(x);
}

In the above code, the functions m and n are both tail calls, because they are both the last operation of the function f.

Tail call optimization

The reason why the tail call is different from other calls lies in its special call location.

We know that a function call will form a "call record" in the memory, also known as a "call frame", which saves information such as the call location and internal variables. If the function B is called inside the function A, a call frame of B will also be formed above the call frame of A. After B is finished, the result is returned to A, then the call frame of B will disappear. If function B also calls function C inside, then there is a call frame of C, and so on. All call frames form a "call stack" (call stack).

Since the tail call is the last operation of the function, there is no need to keep the call frame of the outer function, because the call location, internal variables and other information will not be used anymore, as long as the call frame of the inner function is used directly to replace the outer function The call frame is fine.

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// Equivalent to
function f() {
  return g(3);
}
f();

// Equivalent to
g(3);

In the above code, if the function g is not called at the end, the function f needs to save the value of the internal variables m and n, the call location of g and other information. But since the function f ends after calling g, it is possible to delete the call frame of f(x) at the last step, and only keep the call frame of g(3).

This is called "Tail call optimization", that is, only the call frame of the inner function is retained. If all functions are called tails, it is possible to have only one call frame each time it is executed, which will greatly save memory. This is the meaning of "tail call optimization".

Note that only the internal variables of the outer function are no longer used, the call frame of the inner function will replace the call frame of the outer function, otherwise the "tail call optimization" cannot be performed.

function addOne(a) {
  var one = 1;
  function inner(b) {
    return b + one;
  }
  return inner(a);
}

The above function will not perform tail call optimization, because the inner function inner uses the inner variable one of the outer function addOne.

Note that currently only the Safari browser supports tail call optimization, neither Chrome nor Firefox.

Tail recursion

The function calls itself, which is called recursion. If the tail calls itself, it is called tail recursion.

Recursion is very memory intensive, because thousands or hundreds of call frames need to be saved at the same time, which is prone to "stack overflow" errors. But for tail recursion, because there is only one call frame, the "stack overflow" error will never occur.

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5); // 120

The above code is a factorial function. To calculate the factorial of n, at most n call records need to be saved, and the complexity is O(n).

If rewritten as tail recursion, only one call record is kept, and the complexity is O(1).

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1); // 120

There is another well-known example, the calculation of Fibonacci sequence, which can also fully illustrate the importance of tail recursive optimization.

The non-tail recursive Fibonacci sequence is implemented as follows.

function Fibonacci(n) {
  if (n <= 1) {
    return 1;
  }

  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10); // 89
Fibonacci(100); // timeout
Fibonacci(500); // timeout

The Fibonacci sequence optimized by tail recursion is implemented as follows.

function Fibonacci2(n, ac1 = 1, ac2 = 1) {
  if (n <= 1) {
    return ac2;
  }

  return Fibonacci2(n - 1, ac2, ac1 + ac2);
}

Fibonacci2(100); // 573147844013817200000
Fibonacci2(1000); // 7.0330367711422765e+208
Fibonacci2(10000); // Infinity

It can be seen that "tail call optimization" is of great significance to recursive operations, so some functional programming languages ​​write it into language specifications. The same is true for ES6. For the first time, it is clearly stipulated that all implementations of ECMAScript must deploy "tail call optimization". This means that as long as tail recursion is used in ES6, stack overflow (or timeout caused by layers of recursion) will not occur, which saves memory.

Rewrite of recursive function

The implementation of tail recursion often needs to rewrite the recursive function to ensure that the last step only calls itself. The way to do this is to rewrite all the internal variables used into function parameters. For example, in the above example, the factorial function factorial needs an intermediate variable total, then rewrite this intermediate variable as a function parameter. The disadvantage of this is that it is not intuitive, and it is difficult to see at first glance. Why do we need to pass in two parameters 5 and 1 to calculate the factorial of 5?

Two methods can solve this problem. The first method is to provide a normal form of function in addition to the tail recursive function.

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

function factorial(n) {
  return tailFactorial(n, 1);
}

factorial(5); // 120

The above code uses a normal form factorial function factorial to call the tail recursive function tailFactorial, which looks much more normal.

Functional programming has a concept called currying, which means converting a multi-parameter function into a single-parameter form. Currying can also be used here.

function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);
  };
}

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

const factorial = currying(tailFactorial, 1);

factorial(5); // 120

The above code uses currying to change the tail recursive function tailFactorial into a factorial that only accepts one parameter.

The second method is much simpler, which is to use ES6 function default values.

function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5); // 120

In the above code, the parameter total has a default value of 1, so there is no need to provide this value when calling.

To sum up, recursion is essentially a loop operation. Purely functional programming languages ​​do not have loop operation commands. All loops are implemented by recursion, which is why tail recursion is extremely important for these languages. For other languages ​​that support "tail call optimization" (such as Lua, ES6), you only need to know that loops can be replaced by recursion, and once recursion is used, it is best to use tail recursion.

Strict mode

ES6's tail call optimization is only enabled in strict mode, and normal mode is invalid.

This is because in normal mode, there are two variables inside the function, which can track the call stack of the function.

-func.arguments: Returns the parameters of the function when it is called. -func.caller: Returns the function that called the current function.

When tail call optimization occurs, the call stack of the function will be rewritten, so the above two variables will be distorted. Strict mode disables these two variables, so tail call mode only takes effect in strict mode.

function restricted() {
  "use strict";
  restricted.caller; // error
  restricted.arguments; // error
}
restricted();

Implementation of Tail Recursion Optimization

Tail recursion optimization only takes effect in strict mode. Is there a way to use tail recursion optimization in normal mode or in environments that do not support this feature? The answer is yes, it is to implement tail recursion optimization by yourself.

Its principle is very simple. The reason why tail recursion needs to be optimized is that there are too many call stacks and overflow, so as long as the call stack is reduced, there will be no overflow. What can be done to reduce the call stack? It is to replace "recursion" with "loop".

The following is a normal recursive function.

function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}

sum(1, 100000);
// Uncaught RangeError: Maximum call stack size exceeded(…)

In the above code, sum is a recursive function, the parameter x is the value to be accumulated, and the parameter y controls the number of recursions. Once you specify sum to recurse 100,000 times, an error will be reported, indicating that the maximum number of calls stack has been exceeded.

Trampoline function (trampoline) can turn recursive execution into loop execution.

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

The above is an implementation of the trampoline function, which accepts a function f as a parameter. As long as f returns a function after execution, execution continues. Note that here is to return a function, and then execute the function, instead of calling the function in the function, so that recursive execution is avoided, thereby eliminating the problem of excessive call stack.

Then, what we need to do is to rewrite the original recursive function to return another function at each step.

function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
  } else {
    return x;
  }
}

In the above code, each execution of the sum function will return another version of itself.

Now, using the trampoline function to execute sum, the call stack will not overflow.

trampoline(sum(1, 100000));
// 100001

The trampoline function is not really tail-recursive optimization, the following implementation is.

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];

  return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}

var sum = tco(function (x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
});

sum(1, 100000);
// 100001

In the above code, the tco function is the implementation of tail recursion optimization, and its secret lies in the state variable active. By default, this variable is not activated. Once in the process of tail recursive optimization, this variable is activated. Then, each round of recursive sum returns undefined, so recursive execution is avoided; and the accumulated array stores the parameters of each round of sum execution, which always have values, which guarantees The while loop inside the accumulator function is always executed. In this way, "recursion" is changed to "loop" very cleverly, and the parameters of the next round will replace the parameters of the previous round, ensuring that the call stack has only one layer.

The trailing comma of function parameters

ES2017 Allow The last parameter of the function has a trailing comma.

Previously, when the function was defined and called, a comma was not allowed after the last parameter.

function clownsEverywhere(param1, param2) {
  /* ... */
}

clownsEverywhere("foo", "bar");

In the above code, if you add a comma after param2 or bar, an error will be reported.

If you write the parameters in multiple lines like the above (that is, each parameter occupies one line), when you modify the code later, if you want to add a third parameter to the function clownsEverywhere, or adjust the order of the parameters, it is bound to be the original last parameter Add a comma after it. For the version management system, it will show that the line where the comma is added has also changed. This seems a bit redundant, so the new syntax allows a comma at the end of the definition and call.

function clownsEverywhere(param1, param2) {
  /* ... */
}

clownsEverywhere("foo", "bar");

This rule also makes function parameters consistent with the trailing comma rules for arrays and objects.

Function.prototype.toString()

ES2019 Modified the toString() method of function instances.

The toString() method returns the function code itself, comments and spaces were previously omitted.

function /* foo comment */ foo() {}

foo.toString();
// function foo() {}

In the above code, the original code of the function foo contains comments, and there are spaces between the function name foo and the parentheses, but they are omitted in the toString() method.

The modified toString() method clearly requires the return of exactly the same original code.

function /* foo comment */ foo() {}

foo.toString();
// "function /* foo comment */ foo () {}"

Parameter omission of catch command

The try...catch structure of the JavaScript language previously clearly required that the catch command must be followed by parameters and accept the error object thrown by the try code block.

try {
  // ...
} catch (err) {
  // handle errors
}

In the above code, the catch command is followed by the parameter err.

In many cases, the catch code block may not use this parameter. However, in order to ensure the correct grammar, it must be written. ES2019 has changed to allow the catch statement to omit parameters.

try {
  // ...
} catch {
  // ...
}