Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Extension of the ES6 function


May 08, 2021 ES6


Table of contents


1. The default value for function parameters

Basic usage

Prior to ES6, the default value could not be specified directly for the parameters of the function, but only in a workable way.

  1. function log(x, y) {
  2. y = y || 'World';
  3. console.log(x, y);
  4. }
  5. log('Hello') // Hello World
  6. log('Hello', 'China') // Hello China
  7. log('Hello', '') // Hello World

The code above checks that the parameter y of the function log has no assignment, and if not, specifies the default value as World. T he disadvantage of this writing is that if the parameter y is assigned, but the corresponding Boolean value is false, the assignment has no effect. Like the last line of the code above, parameter y equals an empty character, and the result is changed to the default.

To avoid this problem, it is often necessary to determine whether parameter y is assigned and, if not, equal to the default value.

  1. if (typeof y === 'undefined') {
  2. y = 'World';
  3. }

ES6 allows the default value to be set for the parameters of the function, i.e. written directly after the parameter definition.

  1. function log(x, y = 'World') {
  2. console.log(x, y);
  3. }
  4. log('Hello') // Hello World
  5. log('Hello', 'China') // Hello China
  6. log('Hello', '') // Hello

As you can see, ES6 is much cleaner and more natural than ES5. Here's another example.

  1. function Point(x = 0, y = 0) {
  2. this.x = x;
  3. this.y = y;
  4. }
  5. const p = new Point();
  6. p // { x: 0, y: 0 }

In 简洁 to simplicity, ES6 has two benefits: first, people who read the code can immediately realize which parameters can be omitted without having to look at the function body or documentation; 优化

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

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

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

When you use parameter defaults, the function cannot have arguments with the same name.

  1. // 不报错
  2. function foo(x, x, y) {
  3. // ...
  4. }
  5. // 报错
  6. function foo(x, x, y = 1) {
  7. // ...
  8. }
  9. // SyntaxError: Duplicate parameter name not allowed in this context

Also, one thing to ignore is that the parameter default value is not passed, but the value of the default expression is recalculated each time. That is, the parameter default is lazy.

  1. let x = 99;
  2. function foo(p = x + 1) {
  3. console.log(p);
  4. }
  5. foo() // 100
  6. x = 100;
  7. foo() // 101

In the code above, the default value for parameter p is x plus 1. At this point, each time the function foo is called, x plus 1 is recalculated instead of the default p equal to 100.

Used in conjunction with deconstructed assignment defaults

参数默认值 be used 解构赋值 the default values of assignments.

  1. function foo({x, y = 5}) {
  2. console.log(x, y);
  3. }
  4. foo({}) // undefined 5
  5. foo({x: 1}) // 1 5
  6. foo({x: 1, y: 2}) // 1 2
  7. foo() // TypeError: Cannot read property 'x' of undefined

The above code uses only the deconstruction assignment default values of the object, not the default values of function parameters. V ariables x and y are generated by deconstructing assignments only if the parameters of the function foo are an object. I f the function foo is called without providing parameters, variables x and y are not generated, thus reporting an error. This can be avoided by providing default values for function parameters.

  1. function foo({x, y = 5} = {}) {
  2. console.log(x, y);
  3. }
  4. foo() // undefined 5

The code above specifies that the parameters of the function foo default to an empty object if no arguments are provided.

Here is another example of deconstructing the default value of an assignment.

  1. function fetch(url, { body = '', method = 'GET', headers = {} }) {
  2. console.log(method);
  3. }
  4. fetch('http://example.com', {})
  5. // "GET"
  6. fetch('http://example.com')
  7. // 报错

In the above code, if the second argument of the function fetch is an object, you can set default values for its three properties. T his type of writing does not omit the second argument, and if you combine the default values of the function arguments, you can omit the second argument. At this point, a double default value appears.

  1. function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
  2. console.log(method);
  3. }
  4. fetch('http://example.com')
  5. // "GET"

In the above code, when the function fetch does not have a second argument, the default value of the function argument takes effect, and then the default value of the deconstructed assignment takes effect before the variable method takes the default value GET.

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

  1. // 写法一
  2. function m1({x = 0, y = 0} = {}) {
  3. return [x, y];
  4. }
  5. // 写法二
  6. function m2({x, y} = { x: 0, y: 0 }) {
  7. return [x, y];
  8. }

Both of the above writes set the default value for the parameters of the function, the difference being that the default value of the argument of the function is empty, but the default value of the object deconstruction assignment is set;

  1. // 函数没有参数的情况
  2. m1() // [0, 0]
  3. m2() // [0, 0]
  4. // x 和 y 都有值的情况
  5. m1({x: 3, y: 8}) // [3, 8]
  6. m2({x: 3, y: 8}) // [3, 8]
  7. // x 有值,y 无值的情况
  8. m1({x: 3}) // [3, 0]
  9. m2({x: 3}) // [3, undefined]
  10. // x 和 y 都无值的情况
  11. m1({}) // [0, 0];
  12. m2({}) // [undefined, undefined]
  13. m1({z: 3}) // [0, 0]
  14. m2({z: 3}) // [undefined, undefined]

The location of the parameter default value

Typically, the argument that defines the default value should be the end argument 尾参数 B ecause it's easier to see which parameters are omitted. If a non-tail parameter sets a default value, it is virtually impossible to omit.

  1. // 例一
  2. function f(x = 1, y) {
  3. return [x, y];
  4. }
  5. f() // [1, undefined]
  6. f(2) // [2, undefined]
  7. f(, 1) // 报错
  8. f(undefined, 1) // [1, 1]
  9. // 例二
  10. function f(x, y = 5, z) {
  11. return [x, y, z];
  12. }
  13. f() // [undefined, 5, undefined]
  14. f(1) // [1, 5, undefined]
  15. f(1, ,2) // 报错
  16. f(1, undefined, 2) // [1, 5, 2]

In the code above, the parameters with default values are not tail parameters. At this point, you cannot simply omit the argument, not the argument that follows it, unless you explicitly enter undefined.

If undefined is passed in, the parameter is triggered equal to the default, and null does not have this effect.

  1. function foo(x = 5, y = 6) {
  2. console.log(x, y);
  3. }
  4. foo(undefined, null)
  5. // 5 null

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

The length property of the function

When the default value is specified, the length property of the function returns the number of arguments that do not specify the default value. That is, when the 默认值 specified, the length 失真

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

In the above code, the return value of the length property is equal to the number of arguments in the function minus the number of arguments that specify the default value. For example, the last function above defines three parameters, one of which c specifies the default value, so the length property is equal to 3 minus 1 and ends up with 2.

This is because the length property means that the function expects the number of arguments to be passed in. W hen an argument specifies a default value, the expected number of incoming arguments does not include this parameter. Similarly, the rest parameters of the following article are not counted in the length property.

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

If the argument with the default value set is not a tail argument, the length property is no longer counted in the following parameters.

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

Scope

Once the default value for the argument is set, when the function declares initialization, the argument 作用域(context) B y the end of the initialization, the scope disappears. This syntax behavior does not occur when the parameter default value is not set.

  1. var x = 1;
  2. function f(x, y = x) {
  3. console.log(y);
  4. }
  5. f(2) // 2

In the code above, the default value of parameter y is equal to the variable x. W hen function f is called, the argument forms a separate scope. In this scope, the default variable x points to the first argument x instead of the global variable x, so the output is 2.

Let's look at the example below.

  1. let x = 1;
  2. function f(y = x) {
  3. let x = 2;
  4. console.log(y);
  5. }
  6. f() // 1

In the code above, when the function f is called, the argument y s x forms a separate scope. I n this scope, the variable x itself is not defined, so it points to the global variable x of the outer layer. When a function is called, the local variable x inside the function body does not affect the default variable x.

If, at this point, the global variable x does not exist, an error is reported.

  1. function f(y = x) {
  2. let x = 2;
  3. console.log(y);
  4. }
  5. f() // ReferenceError: x is not defined

Write like this below, will also report errors.

  1. var x = 1;
  2. function foo(x = x) {
  3. // ...
  4. }
  5. foo() // ReferenceError: x is not defined

In the code above, the parameter x s x forms a separate scope. The actual execution is let x s x, which misses "x undefined" because of a transsexual dead zone.

If the default value for an argument is a function, the scope of the function follows this rule. Take a look at the example below.

  1. let foo = 'outer';
  2. function bar(func = () => foo) {
  3. let foo = 'inner';
  4. console.log(func());
  5. }
  6. bar(); // outer

In the code above, the default value for the argument func of the function bar is an anonymous function with a return value of the variable foo. Inside the separate scope formed by the function parameters, the variable foo is not defined, so foo points to the global variable foo on the outer layer and therefore outputs outer.

If you write it down like this, you'll get it wrong.

  1. function bar(func = () => foo) {
  2. let foo = 'inner';
  3. console.log(func());
  4. }
  5. bar() // ReferenceError: foo is not defined

In the code above, the foo inside 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 it is wrong.

Here is a more complex example.

  1. var x = 1;
  2. function foo(x, y = function() { x = 2; }) {
  3. var x = 3;
  4. y();
  5. console.log(x);
  6. }
  7. foo() // 3
  8. x // 1

In the code above, the parameters of the function foo form a separate scope. I n this scope, the variable x is declared first, and then the variable y, the default value of y is an anonymous function. T he variable x inside this anonymous function points to the first argument x of the same scope. An internal variable x is declared inside the function foo, which is not the same variable as the first argument x because it is not the same scope, so the values of the internal variable x and the external global variable x have not changed since y was executed.

If you remove the var of var x s 3, the internal variable x of the function foo points to the first argument x, which is consistent with the x inside the anonymous function, so the last output is 2, while the global variable x of the outer layer remains unaffected.

  1. var x = 1;
  2. function foo(x, y = function() { x = 2; }) {
  3. x = 3;
  4. y();
  5. console.log(x);
  6. }
  7. foo() // 2
  8. x // 1

Application

With parameter defaults, you can specify that a parameter must not be omitted and throw an error if omitted.

  1. function throwIfMissing() {
  2. throw new Error('Missing parameter');
  3. }
  4. function foo(mustBeProvided = throwIfMissing()) {
  5. return mustBeProvided;
  6. }
  7. foo()
  8. // Error: Missing parameter

The foo function in the code above throws an error by calling the default value throwIfMissing function if there are no arguments at the time of the call.

As you can also see from the above code, the default value of the parameter mustBeProvided is equal to the result of the run of the throwIfMissing function (note that the function name throwIfMissing is followed by a pair of parentheses), which indicates that the default value of the argument is not executed at definition, but at run time. If the argument has been assigned, the function in the default does not run.

Alternatively, you can set the default value of the argument to undefined, indicating that the parameter can be omitted.

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

2. Rest parameter

ES6 introduces rest 参数 (in the form of ... v ariable name), which is used to get the excess parameters of the function, so that you don't need to use the arguments object. The rest argument is paired with 数组 puts extra arguments into the array.

  1. function add(...values) {
  2. let sum = 0;
  3. for (var val of values) {
  4. sum += val;
  5. }
  6. return sum;
  7. }
  8. add(2, 5, 3) // 10

The add function add code above is a sum 求和函数 can be passed in to any number of arguments using the rest argument.

The following is an example of a rest parameter in place of the arguments variable.

  1. // arguments变量的写法
  2. function sortNumbers() {
  3. return Array.prototype.slice.call(arguments).sort();
  4. }
  5. // rest参数的写法
  6. const sortNumbers = (...numbers) => numbers.sort();

The two writing methods of the above code can be compared and you can see that the rest parameters are written more naturally and concisely.

The arguments object is not an array, but an array-like object. S o in order to use the method of arrays, you must first convert them into arrays using Array.prototype.slice.call. T he rest parameter does not have this problem, it is a real array, array-specific methods can be used. The following is an example of overwriteing the array push method with the rest parameter.

  1. function push(array, ...items) {
  2. items.forEach(function(item) {
  3. array.push(item);
  4. console.log(item);
  5. });
  6. }
  7. var a = [];
  8. push(a, 1, 2, 3)

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

  1. // 报错
  2. function f(a, ...b, c) {
  3. // ...
  4. }

The length property of the function, excluding the rest argument.

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

3. Strict mode

Starting ES5 the inside of the function can be set 严格模式

  1. function doSomething(a, b) {
  2. 'use strict';
  3. // code
  4. }

ES2016 modification to 默认值 state 解构赋值 that as long as the function parameters use default values, deconstruct assignments, or 扩展运算符 operators, the inside of the function cannot be explicitly set to strict mode, otherwise errors will be reported.

  1. // 报错
  2. function doSomething(a, b = a) {
  3. 'use strict';
  4. // code
  5. }
  6. // 报错
  7. const doSomething = function ({a, b}) {
  8. 'use strict';
  9. // code
  10. };
  11. // 报错
  12. const doSomething = (...a) => {
  13. 'use strict';
  14. // code
  15. };
  16. const obj = {
  17. // 报错
  18. doSomething({a, b}) {
  19. 'use strict';
  20. // code
  21. }
  22. };

The reason for this is that the strict pattern inside the function applies to both 函数体 and the function 函数参数 H owever, when a function executes, the function argument is executed before the function body is executed. There is an unreasonable place that only from the function body can we know whether the argument should be executed in strict mode, but the argument should be executed before the function body.

  1. // 报错
  2. function doSomething(value = 070) {
  3. 'use strict';
  4. return value;
  5. }

In the code above, the default value of the parameter value is octal number 070, but the prefix 0 cannot be used in strict mode to represent octal, so an error should be reported. In practice, however, the JavaScript engine successfully executes value s 070, then enters the inside of the function body and discovers that it needs to be executed in strict mode before it reports an error.

Although you can parse the function body code before executing the parameter code, this undoubtedly adds complexity. Therefore, standard ropes prohibit this use, and strict mode cannot be explicitly specified as long as the parameters use default values, deconstruct assignments, or extended operators.

There are two ways to circumvent this limitation. The first is to set a strict pattern of globality, which is legal.

  1. 'use strict';
  2. function doSomething(a, b = a) {
  3. // code
  4. }

The second is to wrap the function in an argumentless immediate execution function.

  1. const doSomething = (function () {
  2. 'use strict';
  3. return function(value = 42) {
  4. return value;
  5. };
  6. }());

4. name property

The name name the function, which returns the function name 函数名

  1. function foo() {}
  2. foo.name // "foo"

This property has long been widely supported by browsers, but it wasn't until ES6 that it was written to the standard.

It is important to note that ES6 has made some changes to the behavior of this property. If an anonymous function is assigned to a variable, the name property of ES5 returns an empty string, while the name property of ES6 returns the actual function name.

  1. var f = function () {};
  2. // ES5
  3. f.name // ""
  4. // ES6
  5. f.name // "f"

In the above code, the variable f equals an anonymous function, and the name properties of ES5 and ES6 return different values.

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

  1. const bar = function baz() {};
  2. // ES5
  3. bar.name // "baz"
  4. // ES6
  5. bar.name // "baz"

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

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

The function returned by bind, the name property value is prefixed with bound.

  1. function foo() {};
  2. foo.bind({}).name // "bound foo"
  3. (function(){}).bind({}).name // "bound "

5. Arrow function

Basic usage

ES6 “箭头” to be defined using the "arrow" .

  1. var f = v => v;
  2. // 等同于
  3. var f = function (v) {
  4. return v;
  5. };

If the arrow function does not require arguments or requires more than one argument, use a 圆括号 the parameter part.

  1. var f = () => 5;
  2. // 等同于
  3. var f = function () { return 5 };
  4. var sum = (num1, num2) => num1 + num2;
  5. // 等同于
  6. var sum = function(num1, num2) {
  7. return num1 + num2;
  8. };

If the arrow function has more than one block of code than one statement, enclose them in braces and return with the return statement.

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

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

  1. // 报错
  2. let getTempItem = id => { id: id, name: "Temp" };
  3. // 不报错
  4. let getTempItem = id => ({ id: id, name: "Temp" });

Here's a special case where you can run, but you get the wrong results.

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

In the code above, the original intent was to return an object, a: 1, but a line of statement a:1 was executed because the engine thought the braces were blocks of code. A t this point, 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 statements and does not need to return a value, you can write below without having to write braces.

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

箭头函数 be used 变量解构

  1. const full = ({ first, last }) => first + ' ' + last;
  2. // 等同于
  3. function full(person) {
  4. return person.first + ' ' + person.last;
  5. }

The arrow function makes the expression more 简洁

  1. const isEven = n => n % 2 === 0;
  2. const square = n => n * n;

The above code defines two simple tool functions in just two lines. If you don't use the arrow function, you may have to take up many lines, and it's not as eye-catching as it is now.

One use of arrow functions is to simplify callback functions.

  1. // 正常函数写法
  2. [1,2,3].map(function (x) {
  3. return x * x;
  4. });
  5. // 箭头函数写法
  6. [1,2,3].map(x => x * x);

Another example is

  1. // 正常函数写法
  2. var result = values.sort(function (a, b) {
  3. return a - b;
  4. });
  5. // 箭头函数写法
  6. var result = values.sort((a, b) => a - b);

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

  1. const numbers = (...nums) => nums;
  2. numbers(1, 2, 3, 4, 5)
  3. // [1,2,3,4,5]
  4. const headAndTail = (head, ...tail) => [head, tail];
  5. headAndTail(1, 2, 3, 4, 5)
  6. // [1,[2,3,4,5]]

Use attention points

Arrow functions have several use notation points.

(1) The this object in the function body is the object in which it was defined, not the object in which it was used.

(2) It cannot be treated 构造函数 that is, the new command cannot be used, otherwise an error is thrown.

(3) You can't arguments object, which doesn't exist in the function body. If you want to use it, you can use the rest parameter instead.

(4) The yield command cannot yield so the arrow function cannot be used as a Generator function.

Of the four points above, the first is particularly noteworthy. The pointing of this object is variable, but in the arrow function, it is fixed.

  1. function foo() {
  2. setTimeout(() => {
  3. console.log('id:', this.id);
  4. }, 100);
  5. }
  6. var id = 21;
  7. foo.call({ id: 42 });
  8. // id: 42

In the code above, setTimeout an arrow function whose definition takes effect when the foo generated, and its true execution 100 毫秒 I f it is a normal function, this should point to the global object window when executed, and 21 should be output. However, the arrow function causes this to always point to the object in which the function definition is in effect (in this case, .id: 42), so the output is 42.

The arrow function lets the this inside setTimeout bind the scope in which the definition is made, rather than pointing to the scope in which the runtime is located. Here's another example.

  1. function Timer() {
  2. this.s1 = 0;
  3. this.s2 = 0;
  4. // 箭头函数
  5. setInterval(() => this.s1++, 1000);
  6. // 普通函数
  7. setInterval(function () {
  8. this.s2++;
  9. }, 1000);
  10. }
  11. var timer = new Timer();
  12. setTimeout(() => console.log('s1: ', timer.s1), 3100);
  13. setTimeout(() => console.log('s2: ', timer.s2), 3100);
  14. // s1: 3
  15. // s2: 0

In the code above, two timers are set inside the Timer function, using the arrow function and the normal function, respectively. T he former's this binding defines the scope in which it is located (that is, the Timer function), and the latter's this points to the scope in which the runtime is located (that is, the global object). So, after 3100 milliseconds, timer.s1 is updated three times, and timer.s2 is not updated once.

Arrow functions allow this to point to fixation, a feature that is useful for encapsulating callback functions. Here is an example of a doM event's callback function encapsulated in an object.

  1. var handler = {
  2. id: '123456',
  3. init: function() {
  4. document.addEventListener('click',
  5. event => this.doSomething(event.type), false);
  6. },
  7. doSomething: function(type) {
  8. console.log('Handling ' + type + ' for ' + this.id);
  9. }
  10. };

In the init method of the above code, an arrow function is used, which causes the this inside the arrow function to always point to the handler object. Otherwise, when the callback function runs, the line this.doSomething will report an error because at this point to the document object.

This points to the fixation, not because the arrow function has a mechanism to bind this, the actual reason is that the arrow function does not have its own this, resulting in the internal this is the outer code block of this. It is precisely because it does not have this that it cannot be used as a constructor.

Therefore, the code for the arrow function to ES5 is as follows.

  1. // ES6
  2. function foo() {
  3. setTimeout(() => {
  4. console.log('id:', this.id);
  5. }, 100);
  6. }
  7. // ES5
  8. function foo() {
  9. var _this = this;
  10. setTimeout(function () {
  11. console.log('id:', _this.id);
  12. }, 100);
  13. }

In the code above, the converted version of ES5 makes it clear that the arrow function does not have its own this at all, but refers to the external version of this.

How many of the following codes are this?

  1. function foo() {
  2. return () => {
  3. return () => {
  4. return () => {
  5. console.log('id:', this.id);
  6. };
  7. };
  8. };
  9. }
  10. var f = foo.call({id: 1});
  11. var t1 = f.call({id: 2})()(); // id: 1
  12. var t2 = f().call({id: 3})(); // id: 1
  13. var t3 = f()().call({id: 4}); // id: 1

In the above code, there is only one this, which is the this of the function foo, so t1, t2, t3 all output the same result. Because all inner functions are arrow functions and do not have their own this, their this is actually the this of the outer outerst foo function.

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

  1. function foo() {
  2. setTimeout(() => {
  3. console.log('args:', arguments);
  4. }, 100);
  5. }
  6. foo(2, 4, 6, 8)
  7. // args: [2, 4, 6, 8]

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

In addition, since the arrow function does not have its own this, it is of course not able to change the pointing of this with call(), apply(), bind() these methods.

  1. (function() {
  2. return [
  3. (() => this.x).bind({ x: 'inner' })()
  4. ];
  5. }).call({ x: 'outer' });
  6. // ['outer']

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

This object in the JavaScript language has long been a headache, and you must be very careful when using this in object methods. The arrow function "binding" this largely solves this problem.

Not applicable on occasions

Because the arrow function makes this “动态” “静态” arrow function should not be used in the following two situations.

The first is the method 定义对象 the object, and the method includes this

  1. const cat = {
  2. lives: 9,
  3. jumps: () => {
  4. this.lives--;
  5. }
  6. }

In the code above, the cat.jumps() method is an arrow function, which is wrong. W hen cat.jumps() are called, the this inside the method points to cat if it is a normal function, and if it is written as an arrow function like the one above, this points to the global object and therefore does not get the expected result. This is because the object does not constitute a separate scope, resulting in the jumps arrow function defining the scope as a global scope.

The second occasion is when dynamic this is required, and the arrow function should not be used.

  1. var button = document.getElementById('press');
  2. button.addEventListener('click', () => {
  3. this.classList.toggle('on');
  4. });

When the above code runs, clicking the button will report an error because button's listening function is an arrow function, causing the this inside to be the global object. If you change to a normal function, this will dynamically point to the button object being clicked.

In addition, if the function body is complex, there are many lines, or there are a large number of read and write operations inside the function, not simply to calculate the value, then should not use the arrow function, but to use ordinary functions, which can improve code readability.

Nested arrow functions

Inside the arrow function, you can also use the arrow function again. Below is a multi-nesting function of the ES5 多重嵌套

  1. function insert(value) {
  2. return {into: function (array) {
  3. return {after: function (afterValue) {
  4. array.splice(array.indexOf(afterValue) + 1, 0, value);
  5. return array;
  6. }};
  7. }};
  8. }
  9. insert(2).into([1, 3]).after(1); //[1, 2, 3]

The above function can be overwrite using the arrow function.

  1. let insert = (value) => ({into: (array) => ({after: (afterValue) => {
  2. array.splice(array.indexOf(afterValue) + 1, 0, value);
  3. return array;
  4. }})});
  5. insert(2).into([1, 3]).after(1); //[1, 2, 3]

The following is an example of a deployment pipeline mechanism, where the output of the previous function is the input to the latest function.

  1. const pipeline = (...funcs) =>
  2. val => funcs.reduce((a, b) => b(a), val);
  3. const plus1 = a => a + 1;
  4. const mult2 = a => a * 2;
  5. const addThenMult = pipeline(plus1, mult2);
  6. addThenMult(5)
  7. // 12

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

  1. const plus1 = a => a + 1;
  2. const mult2 = a => a * 2;
  3. mult2(plus1(5))
  4. // 12

The Arrow function also has the ibility of easily rewriting the calculation.

  1. // λ演算的写法
  2. fix = λf.(λx.fv.x(x)(v)))(λx.fv.x(x)(v)))
  3. // ES6的写法
  4. var fix = f => (x => f(v => x(x)(v)))
  5. (x => f(v => x(x)(v)));

The above two writing methods, almost one-to-one correspondence. Because calculations are important for computer science, we can explore computer science with ES6 as an alternative.

6. Tail call optimization

What is a tail call?

Tail Call 尾调用 an important concept of functional programming, which is very simple in itself, and it is clear in one sentence that the last step of a function is to call another function.

  1. function f(x){
  2. return g(x);
  3. }

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

The following three cases are not tail calls.

  1. // 情况一
  2. function f(x){
  3. let y = g(x);
  4. return y;
  5. }
  6. // 情况二
  7. function f(x){
  8. return g(x) + 1;
  9. }
  10. // 情况三
  11. function f(x){
  12. g(x);
  13. }

In the above code, the case is that after calling function g, there is an assignment operation, so it does not belong to the tail call, even if the semantics are exactly the same. S cenario two also belongs to the operation after the call, even if written on a line. Case three is equivalent to the following code.

  1. function f(x){
  2. g(x);
  3. return undefined;
  4. }

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

  1. function f(x) {
  2. if (x > 0) {
  3. return m(x)
  4. }
  5. return n(x);
  6. }

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

Tail call optimization

The reason the tail call is different from other calls is that it has a 调用位置

We know that function calls form a “调用记录” also “调用帧” that holds information such as the call location and internal variables. I f function B is called inside function A, a call frame of B is also formed above the call frame for A. T he call frame for B does not disappear until the B run is over and the result is returned to A. I f function C is also called inside function B, there is also a C call frame, and so on. All call frames form a "call stack".

Because the tail call is the last step of the function, there is no need to keep the call frame of the outer function, because the call position, internal variables and other information will no longer be used, as long as the call frame of the inner function is used directly, instead of the call frame of the outer function.

  1. function f() {
  2. let m = 1;
  3. let n = 2;
  4. return g(m + n);
  5. }
  6. f();
  7. // 等同于
  8. function f() {
  9. return g(3);
  10. }
  11. f();
  12. // 等同于
  13. g(3);

In the above code, if the function g is not a tail call, function f needs to save information such as the values of the internal variables m and n, the call location of g, and so on. However, since the function f ends after the call g, it is possible to delete the call frame of f(x) and keep only the call frame of g(3) until the last step.

This is called tail call optimization, which preserves only the call frames of the inner function. I f all functions are tail calls, it is perfectly possible to call only one frame per execution, which will save a lot of memory. This is what Tail Call Optimization means.

Note that the call frame of the inner function replaces the call frame of the outer function only if the internal variable of the outer function is no longer used, otherwise "tail call optimization" cannot be performed.

  1. function addOne(a){
  2. var one = 1;
  3. function inner(b){
  4. return b + one;
  5. }
  6. return inner(a);
  7. }

The above function is not optimized for tail calls because the inner inner of the inner function uses the internal variable one of the outer function addOne.

Note that only Safari currently supports tail call optimization, and neither Chrome and Firefox do.

Tail recursion

The function calls itself, called 递归 If the tail calls itself, it is called 尾递归

Recursion 费内存 because it requires hundreds of call frames to be saved at the same “栈溢出” "stack overflow" error. But for the tail return, because there is only one call frame, the "stack overflow" error never occurs.

  1. function factorial(n) {
  2. if (n === 1) return 1;
  3. return n * factorial(n - 1);
  4. }
  5. factorial(5) // 120

The above code is a multiplier function that calculates the factories of n and requires saving up to n call records, with complexity O(n).

If overwrite to tail recursion, only one call record is retained, complexity O(1).

  1. function factorial(n, total) {
  2. if (n === 1) return total;
  3. return factorial(n - 1, n * total);
  4. }
  5. factorial(5, 1) // 120

Another well-known example is the calculation of Fibonacci columns, which also fully illustrates the importance of tail recursive optimization.

The non-tail recursive Fibonacci number column is implemented below.

  1. function Fibonacci (n) {
  2. if ( n <= 1 ) {return 1};
  3. return Fibonacci(n - 1) + Fibonacci(n - 2);
  4. }
  5. Fibonacci(10) // 89
  6. Fibonacci(100) // 超时
  7. Fibonacci(500) // 超时

The tail recursive optimized Fibonacci number column is implemented below.

  1. function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  2. if( n <= 1 ) {return ac2};
  3. return Fibonacci2 (n - 1, ac2, ac1 + ac2);
  4. }
  5. Fibonacci2(100) // 573147844013817200000
  6. Fibonacci2(1000) // 7.0330367711422765e+208
  7. Fibonacci2(10000) // Infinity

Thus, “尾调用优化” recursive operations, so some functional programming languages write it to language specifications. T he same is true of ES6, which for the first time makes it clear that all ECMAScript implementations must “尾调用优化” This means that as long 尾递归 as you use tail recursion in ES6, there is no stack overflow (or layer recursion-caused timeout), which is relatively memory-saving.

Recursive function override

The implementation of tail recursion often requires overwriteing the recursive function to ensure that the last step calls only itself. T he way to do this is to rewrite all the internal variables used into function parameters. F or example, in the example above, the factorial function needs to use an intermediate variable total, which is rewritten into a function's argument. The disadvantage of this is that it is not very intuitive, it is difficult to see at first glance, why is it necessary to calculate the order of 5 and pass in two parameters 5 and 1?

There are two ways to solve this problem. Method one provides a normal form of function in addition to the tail recursive function.

  1. function tailFactorial(n, total) {
  2. if (n === 1) return total;
  3. return tailFactorial(n - 1, n * total);
  4. }
  5. function factorial(n) {
  6. return tailFactorial(n, 1);
  7. }
  8. factorial(5) // 120

The above code looks much more normal by calling the tail recursive function tailfactorial through a normal form of the factor function factorial.

Functional programming has a concept called currying, which means converting multi-argument functions into single-argument forms. Curry can also be used here.

  1. function currying(fn, n) {
  2. return function (m) {
  3. return fn.call(this, m, n);
  4. };
  5. }
  6. function tailFactorial(n, total) {
  7. if (n === 1) return total;
  8. return tailFactorial(n - 1, n * total);
  9. }
  10. const factorial = currying(tailFactorial, 1);
  11. factorial(5) // 120

The above code changes the tail recursive function tailFactorial to a factorial that accepts only one argument through Curry.

The second method is much simpler, which is to take the function default value of ES6.

  1. function factorial(n, total = 1) {
  2. if (n === 1) return total;
  3. return factorial(n - 1, n * total);
  4. }
  5. factorial(5) // 120

In the code above, the parameter total has a default value of 1, so this value is not provided when called.

To sum up, recursion is essentially a circular operation. P ure functional programming languages do not have loop operation commands, and all loops are implemented with recursion, which is why tail recursion is extremely important for these languages. For other languages that support "end call optimization" (e.g. Lua, ES6), it is important to know that loops can be replaced by recursion, and that once recursion is used, it is best to use tail recursion.

Strict mode

ES6's 尾调用 Optimization is only 严格模式 in strict mode, which is not valid.

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

  • func.arguments: Returns the parameters of the function at the time of the call.
  • func.caller: Returns the function that called the current function.

When the tail call optimization occurs, the call stack of the function overrides, so the above two variables are distorted. Strict mode disables both variables, so the tail call mode only works in strict mode.

  1. function restricted() {
  2. 'use strict';
  3. restricted.caller; // 报错
  4. restricted.arguments; // 报错
  5. }
  6. restricted();

The implementation of tail recursive optimization

Tail recursive optimization only works in strict mode, so in normal mode, or in environments that do not support the function, is there any way to use tail recursive optimization? The answer is yes, that is, to achieve tail recursive optimization yourself.

Its principle is very simple. T ail recursion needs to be optimized because there are too many call stacks that cause overflows, so as long as you reduce the call stack, there will be no overflow. W hat can be done to reduce the call stack? is to “循环” “递归”

Here is a normal recursive function.

  1. function sum(x, y) {
  2. if (y > 0) {
  3. return sum(x + 1, y - 1);
  4. } else {
  5. return x;
  6. }
  7. }
  8. sum(1, 100000)
  9. // Uncaught RangeError: Maximum call stack size exceeded(…)

In the code above, sum is a recursive function, parameter x is the value that needs to be added up, and parameter y controls the number of recursives. Once you specify sum recursion 100,000 times, an error is reported, prompting you to exceed the maximum number of calls to the stack.

Trampoline converts recursive execution into circular execution.

  1. function trampoline(f) {
  2. while (f && f instanceof Function) {
  3. f = f();
  4. }
  5. return f;
  6. }

Above is an implementation of the trampoline function, which accepts a function f as an argument. A s long as f returns a function after execution, it continues. Note that here is to return a function and then execute the function instead of calling the function inside the function, thus avoiding recursive execution and eliminating the problem of the call stack being too large.

Then, all you have to do is rewrite the original recursive function to return another function at each step.

  1. function sum(x, y) {
  2. if (y > 0) {
  3. return sum.bind(null, x + 1, y - 1);
  4. } else {
  5. return x;
  6. }
  7. }

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

Now, using the trampoline function to execute sum, no call stack overflow occurs.

  1. trampoline(sum(1, 100000))
  2. // 100001

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

  1. function tco(f) {
  2. var value;
  3. var active = false;
  4. var accumulated = [];
  5. return function accumulator() {
  6. accumulated.push(arguments);
  7. if (!active) {
  8. active = true;
  9. while (accumulated.length) {
  10. value = f.apply(this, accumulated.shift());
  11. }
  12. active = false;
  13. return value;
  14. }
  15. };
  16. }
  17. var sum = tco(function(x, y) {
  18. if (y > 0) {
  19. return sum(x + 1, y - 1)
  20. }
  21. else {
  22. return x
  23. }
  24. });
  25. sum(1, 100000)
  26. // 100001

In the above code, the tco function is the implementation of tail recursive optimization, and its mystery lies in the state variable active. B y default, this variable is not activated. O nce you enter the process of tail recursive optimization, this variable is activated. E ach round of recursive sum then returns undefined, thus avoiding recursive execution, while the accumulated array holds the parameters of each round of sum execution, always with values, which ensures that the while loop inside the accumulator function is always executed. This subtly changes "recursive" to "loop", and the parameters of the next round replace those of the previous round, ensuring that the call stack has only one layer.

7. The end comma of the function argument

ES2017 allows the last argument of 尾逗号 to have a trailing comma.

Previously, commas were not allowed after the last argument when functions were defined and called.

  1. function clownsEverywhere(
  2. param1,
  3. param2
  4. ) { /* ... */ }
  5. clownsEverywhere(
  6. 'foo',
  7. 'bar'
  8. );

In the code above, if you add a bar param2 or bar, you will report an error.

If, as above, you write the arguments into multiple lines (i.e., each argument occupies one line), and later, when you modify the code, you want to add a third argument to the function clownsEverywhere, or adjust the order of the parameters, you will have to add a comma after the original last argument. F or the version management system, the line where the comma is added also changes. This may seem redundant, so the new syntax allows for a comma directly at the tail when defined and called.

  1. function clownsEverywhere(
  2. param1,
  3. param2,
  4. ) { /* ... */ }
  5. clownsEverywhere(
  6. 'foo',
  7. 'bar',
  8. );

This rule also makes the function parameters consistent with the tail comma rules of arrays and objects.

8. Function.prototype.toString()

ES2019 makes changes to toString() the function instance.

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

  1. function /* foo comment */ foo () {}
  2. foo.toString()
  3. // function foo() {}

In the code above, the original code for the function foo contains comments, and there are spaces between the function name foo and parentheses, but the toString() method omits them.

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

  1. function /* foo comment */ foo () {}
  2. foo.toString()
  3. // "function /* foo comment */ foo () {}"

9. The parameters of the catch command are omitted

Try in JavaScript try...catch structure, which explicitly required catch commands to be followed by parameters, accepts error objects thrown by try blocks.

  1. try {
  2. // ...
  3. } catch (err) {
  4. // 处理错误
  5. }

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

Many times, catch blocks may not be able to use this parameter. H owever, in order to ensure that the syntax is correct, you must still write. ES2019 made a change to allow catch statements to omit parameters.

  1. try {
  2. // ...
  3. } catch {
  4. // ...
  5. }