VDone Demo VDone Demo
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)

Nikolay Tuzov

Backend Developer
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)
  • Introduction to ECMAScript 6
  • let and const Commands
  • Destructuring Assignment of Variables
  • String Extensions
  • New String Methods
  • Regular Expression Extensions
  • Number Extensions
  • Function Extensions
    • Default Parameter Values
      • Basic Usage
      • Combined with Destructuring Assignment Default Values
      • Position of Default Parameter Values
      • The length Property of Functions
      • Scope
      • Applications
    • Rest Parameters
    • Strict Mode
    • The name Property
    • Arrow Functions
      • Basic Usage
      • Important Considerations
      • When Not to Use Arrow Functions
      • Nested Arrow Functions
    • Tail Call Optimization
      • What Is a Tail Call?
      • Tail Call Optimization
      • Tail Recursion
      • Rewriting Recursive Functions
      • Strict Mode
      • Implementing Tail Recursion Optimization
    • Trailing Commas in Function Parameters
    • Function.prototype.toString()
    • Optional catch Binding
  • Array Extensions
  • Object Extensions
  • 对象的新增方法
  • Symbol
  • Set 和 Map 数据结构
  • Proxy
  • Reflect
  • Promise 对象
  • Iterator 和 for-of 循环
  • Generator Function Syntax
  • Asynchronous Applications of Generator Functions
  • async Functions
  • Class 的基本语法
  • Class 的继承
  • Module 的语法
  • Module 的加载实现
  • 编程风格
  • 读懂 ECMAScript 规格
  • Async Iterator
  • ArrayBuffer
  • 最新提案
  • 装饰器
  • 函数式编程
  • Mixin
  • SIMD
  • 参考链接
  • 《ES6 教程》笔记
阮一峰
2020-02-09
Contents

Function Extensions

# Function Extensions

# Default Parameter Values

# Basic Usage

Before ES6, it was not possible to set default values for function parameters directly. A workaround had to 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
1
2
3
4
5
6
7
8

The code above checks whether the parameter y of function log has been assigned a value. If not, it defaults to World. The drawback of this approach is that if y has been assigned a value whose boolean equivalent is false, the assignment does not take effect. As in the last line above, the parameter y is an empty string, but it gets replaced with the default value.

To avoid this problem, it is usually necessary to first check whether y has been assigned a value, and if not, set it to the default.

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

ES6 allows default values to be set for function parameters by writing them 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
1
2
3
4
5
6
7

As you can see, the ES6 syntax is much more concise than ES5, and very natural. 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 }
1
2
3
4
5
6
7

In addition to being concise, the ES6 syntax has two other benefits: first, anyone reading the code can immediately see which parameters are optional without having to look at the function body or documentation; second, it facilitates future code optimization — even if a parameter is completely removed from the public interface in a future version, existing code will still work.

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

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

In the code above, the parameter variable x is declared by default. Redeclaring it with let or const in the function body will throw an error.

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

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

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

Also, an easily overlooked point is that default parameter values are not passed by value. Instead, the default value expression is re-evaluated each time. In other words, default parameter values are lazily evaluated.

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

foo() // 100

x = 100;
foo() // 101
1
2
3
4
5
6
7
8
9

In the code above, the default value of parameter p is x + 1. Each time function foo is called, x + 1 is recalculated, rather than p defaulting to 100.

# Combined with Destructuring Assignment Default Values

Default parameter values can be combined with destructuring assignment default values.

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
1
2
3
4
5
6
7
8

The code above only uses object destructuring assignment default values, without using function parameter default values. Variables x and y are only generated through destructuring assignment when the function foo receives an object argument. If foo is called without an argument, x and y are not generated, resulting in an error. By providing a function parameter default value, this situation can be avoided.

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

foo() // undefined 5
1
2
3
4
5

The code above specifies that if no argument is provided, the default value for foo's parameter is an empty object.

Here is another example of destructuring assignment default values.

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

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

fetch('http://example.com')
// Error
1
2
3
4
5
6
7
8
9

In the code above, if the second argument to fetch is an object, default values can be set for its three properties. However, this syntax requires the second argument. By combining with function parameter default values, the second argument can be omitted. This creates a double default.

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

fetch('http://example.com')
// "GET"
1
2
3
4
5
6

In the code above, when fetch has no second argument, the function parameter default value takes effect first, then the destructuring assignment default values take effect, and the variable method gets the default value GET.

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

// Approach 1
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// Approach 2
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}
1
2
3
4
5
6
7
8
9

Both approaches set default values for the function parameters. The difference is that in approach 1, the function parameter default value is an empty object with destructuring assignment default values set; in approach 2, the function parameter default value is an object with specific properties, but no destructuring assignment default values are set.

// No arguments provided
m1() // [0, 0]
m2() // [0, 0]

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

// x has a value, y does not
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// Neither x nor y has a value
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Position of Default Parameter Values

Typically, parameters with default values should be the last parameters (tail parameters). This makes it easy to see which parameters can be omitted. If a non-tail parameter has a default value, it effectively 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) // Error
f(1, undefined, 2) // [1, 5, 2]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

In the code above, the parameters with default values are not tail parameters. In this case, it is impossible to omit only that parameter without omitting those after it, unless undefined is explicitly passed.

If undefined is passed, it triggers the default value for that parameter. null does not have this effect.

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

foo(undefined, null)
// 5 null
1
2
3
4
5
6

In the code above, parameter x corresponds to undefined, triggering the default value, while parameter y equals null, which does not trigger the default value.

# The length Property of Functions

After specifying default values, the length property of the function returns the number of parameters that do not have default values. In other words, specifying default values causes the length property to become inaccurate.

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

In the code above, the return value of the length property equals the total number of parameters minus the number of parameters with default values. For example, in the last function, 3 parameters are defined and one parameter c has a default value, so length equals 3 minus 1, resulting in 2.

This is because the length property represents the number of parameters the function expects to receive. Once a parameter has a default value, it is no longer included in the expected count. Similarly, rest parameters (discussed later) are not counted in the length property.

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

If a parameter with a default value is not a tail parameter, the length property no longer counts parameters after it either.

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

# Scope

Once default values are set for parameters, the parameters form a separate scope during the function's declaration initialization. When initialization is complete, this scope disappears. This scoping behavior does not occur when no parameter default values are set.

var x = 1;

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

f(2) // 2
1
2
3
4
5
6
7

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

Consider another example.

let x = 1;

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

f() // 1
1
2
3
4
5
6
7
8

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

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

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

f() // ReferenceError: x is not defined
1
2
3
4
5
6

The following code will also throw an error.

var x = 1;

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

foo() // ReferenceError: x is not defined
1
2
3
4
5
6
7

In the code above, the parameter x = x forms a separate scope. What actually executes is let x = x, which throws a "x is not defined" error due to the temporal dead zone.

If the default value of a parameter is a function, that function's scope also follows this rule. Consider the following example.

let foo = 'outer';

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

bar(); // outer
1
2
3
4
5
6
7
8

In the code above, the default value of parameter func in function bar is an anonymous function that returns the variable foo. In the separate scope formed by the function parameters, the variable foo is not defined, so foo refers to the outer global variable foo, resulting in the output outer.

If written as follows, it will throw an error.

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

bar() // ReferenceError: foo is not defined
1
2
3
4
5
6

In the code above, the foo inside the anonymous function refers to the outer layer, but there is no foo variable declared in the outer layer, so it throws an error.

Here is a more complex example.

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

foo() // 3
x // 1
1
2
3
4
5
6
7
8
9

In the code above, the parameters of function foo form a separate scope. In this scope, variable x is first declared, then variable y is declared, with y's default value being an anonymous function. The variable x inside this anonymous function refers to the first parameter x in the same scope. Inside function foo, another internal variable x is declared with var. Since this variable and the first parameter x are not in the same scope, they are different variables. Therefore, after executing y, neither the internal variable x nor the outer global variable x changes.

If you remove the var from var x = 3, the internal variable x of function foo points to the first parameter x, which is the same as the x inside the anonymous function. So the final output would be 2, while the outer global variable x remains unaffected.

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

foo() // 2
x // 1
1
2
3
4
5
6
7
8
9

# Applications

Using default parameter values, you can specify that a certain parameter must not be omitted. If omitted, an error is thrown.

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

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

foo()
// Error: Missing parameter
1
2
3
4
5
6
7
8
9
10

In the code above, if function foo is called without an argument, the default value throwIfMissing function is called, throwing an error.

From the code above, you can also see that the default value of parameter mustBeProvided is the result of running the throwIfMissing function (note the parentheses after throwIfMissing). This indicates that parameter default values are not evaluated at definition time but at runtime. If the parameter has been assigned a value, the function in the default value will not run.

Additionally, you can set a parameter default value to undefined to indicate that the parameter is optional.

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

# Rest Parameters

ES6 introduces rest parameters (in the form ...variableName) to capture a function's extra arguments, eliminating the need to use the arguments object. The variable paired with rest parameters is an array that stores the extra arguments.

function add(...values) { // values is an array that stores the extra arguments
  let sum = 0;

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

  return sum;
}

add(2, 5, 3) // 10
1
2
3
4
5
6
7
8
9
10
11

The add function above is a summation function. Using rest parameters, any number of arguments can be passed to it.

Below is an example of using rest parameters to replace the arguments variable.

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

// Using rest parameters
const sortNumbers = (...numbers) => numbers.sort();
1
2
3
4
5
6
7

Comparing the two approaches above, the rest parameter syntax is more natural and concise.

The arguments object is not an array but an array-like object. To use array methods, Array.prototype.slice.call must first be used to convert it to an array. Rest parameters do not have this problem — they are a true array, and all array-specific methods can be used. Below is an example of rewriting the array push method using rest parameters.

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

var a = [];
push(a, 1, 2, 3)
1
2
3
4
5
6
7
8
9

Note that no other parameters can follow rest parameters (i.e., they must be the last parameter); otherwise, an error is thrown.

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

The length property of a function does not include rest parameters.

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

# Strict Mode

Starting from ES5, strict mode can be set inside a function.

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

ES2016 made a modification: if a function uses default values, destructuring assignment, or the spread operator, strict mode cannot be explicitly set inside the function; otherwise, an error is thrown.

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

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

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

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

The reason for this rule is that a function's strict mode applies to both the function body and the function parameters. However, during execution, parameters are processed first, then the function body. This creates an inconsistency: you can only know from the function body whether the parameters should be executed in strict mode, but the parameters are supposed to be executed before the function body.

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

In the code above, the default value of parameter value is the octal number 070, which is not allowed with the 0 prefix in strict mode. However, JavaScript actually executes value = 070 successfully first, then enters the function body and discovers that strict mode is needed, at which point it throws an error.

While it would be possible to parse the function body first and then execute the parameter code, this would undoubtedly increase complexity. Therefore, the standard simply forbids this usage: whenever parameters use default values, destructuring assignment, or the spread operator, strict mode cannot be explicitly specified.

Two methods can work around this restriction. The first is to set global strict mode, which is allowed.

'use strict';

function doSomething(a, b = a) {
  // code
}
1
2
3
4
5

The second is to wrap the function inside a parameterless immediately invoked function expression.

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

# The name Property

The name property of a function returns the function's name.

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

This property has been widely supported by browsers for a long time, but it was only standardized in ES6.

It is worth noting that ES6 modified the behavior of this property. If an anonymous function is assigned to a variable, ES5's name property returns an empty string, while ES6's name property returns the actual function name.

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"
1
2
3
4
5
6
7

In the code above, variable f is assigned an anonymous function. ES5 and ES6 return different values for the name property.

If a named function is assigned to a variable, both ES5 and ES6's name property return the named function's original name.

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"
1
2
3
4
5
6
7

Function instances returned by the Function constructor have a name property value of anonymous.

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

Functions returned by bind have a name property value prefixed with bound.

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

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

# Arrow Functions

# Basic Usage

ES6 allows functions to be defined using the "arrow" (=>).

var f = v => v;

// equivalent to
var f = function (v) {
  return v;
};
1
2
3
4
5
6

If the arrow function requires no parameters or multiple parameters, use parentheses for the parameter section.

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;
};
1
2
3
4
5
6
7
8
9

If the arrow function's code block contains more than one statement, curly braces must be used and a return statement must be included.

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

Since curly braces are interpreted as a code block, if an arrow function directly returns an object, parentheses must be added around the object; otherwise, an error is thrown.

// Error
let getTempItem = id => { id: id, name: "Temp" };

// Correct
let getTempItem = id => ({ id: id, name: "Temp" });
1
2
3
4
5

Here is a special case that runs but produces an incorrect result.

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

In the code above, the original intent is to return the object { a: 1 }, but the engine interprets the curly braces as a code block. It executes the statement a: 1, where a is interpreted as a statement label, so the actual executed statement is 1;. The function then ends with no return value.

If the arrow function has only one statement and no return value is needed, the following syntax avoids writing curly braces.

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

Arrow functions can be combined with variable destructuring.

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

// equivalent to
function full(person) {
  return person.first + ' ' + person.last;
}
1
2
3
4
5
6

Arrow functions make expressions more concise.

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

The code above defines two simple utility functions in just two lines. Without arrow functions, this would take more lines and be less visually clear.

One use of arrow functions is simplifying callbacks.

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

// Arrow function
[1,2,3].map(x => x * x);
1
2
3
4
5
6
7

Another example:

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

// Arrow function
var result = values.sort((a, b) => a - b);
1
2
3
4
5
6
7

Below is an example combining rest parameters with arrow functions.

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]]
1
2
3
4
5
6
7
8
9

# Important Considerations

There are several important points to keep in mind when using arrow functions.

(1) The this object inside the function body is the object at the time of definition, not the object at the time of execution.

(2) It cannot be used as a constructor — i.e., the new operator cannot be used; otherwise, an error is thrown.

(3) The arguments object cannot be used — it does not exist inside the function body. Rest parameters can be used instead.

(4) The yield command cannot be used — therefore, arrow functions cannot serve as Generator functions.

Among these four points, the first is particularly noteworthy. The this object's reference is mutable, but in arrow functions, it is fixed.

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

var id = 21;

foo.call({ id: 42 });
// id: 42
1
2
3
4
5
6
7
8
9
10

In the code above, the setTimeout argument is an arrow function. This arrow function takes effect when function foo is created, but its actual execution occurs 100 milliseconds later. With a regular function, this at execution time would point to the global window object, outputting 21. However, the arrow function causes this to always point to the object where the function was defined (in this case {id: 42}), so the output is 42.

Arrow functions allow this inside setTimeout to be bound to the scope at definition time, rather than pointing to the scope at runtime. Here is another example.

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // Arrow function
  setInterval(() => this.s1++, 1000);
  // Regular 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

In the code above, Timer sets up two timers: one using an arrow function and the other using a regular function. The arrow function's this is bound to the scope at definition time (the Timer function), while the regular function's this points to the scope at runtime (the global object). So after 3100 milliseconds, timer.s1 has been updated 3 times, while timer.s2 has not been updated at all.

Arrow functions make this reference fixed, which is very useful for encapsulating callbacks. Here is an example with a DOM event callback encapsulated in an object.

var handler = {
  id: '123456',

  init: function() { // Note: not using an arrow function here; an arrow function would make this point to the global object
    document.addEventListener('click',
      event => this.doSomething(event.type), false);
  },

  doSomething: function(type) {
    console.log('Handling ' + type  + ' for ' + this.id);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12

In the init method above, an arrow function is used. This causes this inside the arrow function to always point to the handler object. Otherwise, the callback's this.doSomething would throw an error because this would point to the document object.

The fixed nature of this is not because arrow functions have an internal mechanism for binding this. The actual reason is that arrow functions do not have their own this — the internal this is the this of the outer code block. Because they have no this, they cannot be used as constructors.

So, converting an arrow function to ES5 looks like 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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

In the code above, the converted ES5 version clearly shows that arrow functions have no this of their own — they reference the outer layer's this.

How many this references are in the following code?

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

There is only one this in the code above — the this of function foo. So t1, t2, and t3 all output the same result. All inner functions are arrow functions with no this of their own; their this is actually the this of the outermost foo function.

Besides this, the following three variables also do not exist in arrow functions and instead refer to the corresponding variables of the outer function: arguments, super, and new.target.

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

foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
1
2
3
4
5
6
7
8

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

Additionally, since arrow functions have no this of their own, they naturally cannot use call(), apply(), or bind() to change the this reference.

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

In the code above, the arrow function has no this of its own, so bind has no effect, and the internal this points to the outer this.

The this object in JavaScript has long been a source of confusion. Using this in object methods requires great care. Arrow functions "bind" this, largely solving this issue.

# When Not to Use Arrow Functions

Since arrow functions make this "static" instead of "dynamic," there are two situations where arrow functions should not be used.

The first situation is defining object methods that include this.

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

In the code above, cat.jumps() is an arrow function, which is incorrect. When cat.jumps() is called, if it were a regular function, this inside the method would point to cat. Written as an arrow function as above, this points to the global object, so the expected result is not achieved. This is because objects do not form a separate scope, so the scope at the time the jumps arrow function is defined is the global scope.

The second situation is when dynamic this is needed — arrow functions should not be used.

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

The code above will throw an error when the button is clicked because the event listener is an arrow function, making this the global object. If changed to a regular function, this would dynamically point to the clicked button object.

Additionally, if the function body is complex with many lines or involves extensive read/write operations rather than purely computing values, arrow functions should not be used. Regular functions should be used instead to improve code readability.

# Nested Arrow Functions

Arrow functions can be nested inside other arrow functions. Here is a multiply-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]
1
2
3
4
5
6
7
8
9
10

This 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]
1
2
3
4
5
6

Below is an example implementing a pipeline mechanism, where the output of one function becomes the input of the next.

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
1
2
3
4
5
6
7
8
9

If you find the above less readable, you can use the following approach.

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

mult2(plus1(5))
// 12
1
2
3
4
5

Arrow functions also make it very convenient to rewrite lambda calculus.

// Lambda calculus notation
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

// ES6 notation
var fix = f => (x => f(v => x(x)(v)))
               (x => f(v => x(x)(v)));
1
2
3
4
5
6

The two notations above are almost in one-to-one correspondence. Since lambda calculus is extremely important to computer science, this makes ES6 a viable alternative tool for exploring computer science.

# Tail Call Optimization

# What Is a Tail Call?

A tail call is an important concept in functional programming. It is very simple to explain in one sentence: it refers to a function whose last step is calling another function.

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

In the code above, the last step of function f is calling function g — this is a tail call.

The following three cases are NOT tail calls.

// Case 1
function f(x){
  let y = g(x);
  return y;
}

// Case 2
function f(x){
  return g(x) + 1;
}

// Case 3
function f(x){
  g(x);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

In the code above, case 1 has an assignment after calling g, so it is not a tail call even though the semantics are the same. Case 2 also has an operation after the call, even though it is on the same line. Case 3 is equivalent to the following code.

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

A tail call does not necessarily appear at the end of the function — it just needs to be the final operation.

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

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

# Tail Call Optimization

Tail calls are different from other calls because of their special position.

We know that function calls create a "call record" in memory, also known as a "call frame," which stores information like the call position and internal variables. If function A internally calls function B, a call frame for B is created on top of A's call frame. Only when B finishes running and returns its result to A does B's call frame disappear. If B internally calls C, there is another call frame for C, and so on. All call frames form a "call stack."

Since a tail call is the last operation of a function, there is no need to preserve the outer function's call frame, because the call position, internal variables, and other information will no longer be used. The inner function's call frame can directly replace the outer function's call frame.

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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

In the code above, if function g were not a tail call, function f would need to preserve the values of internal variables m and n, g's call position, and other information. But since calling g is the end of function f, when the last step is reached, f(x)'s call frame can be completely deleted, keeping only g(3)'s call frame.

This is called "tail call optimization" — keeping only the inner function's call frame. If all functions are tail calls, it is possible to have only one call frame entry at each execution point, which would greatly reduce memory usage. This is the significance of tail call optimization.

Note that the inner function's call frame can replace the outer function's call frame only if the inner function no longer uses the outer function's internal variables; otherwise, tail call optimization cannot be performed.

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

The function above will not undergo tail call optimization because the inner function inner uses the outer function addOne's internal variable one.

Note that currently only Safari supports tail call optimization; Chrome and Firefox do not.

# Tail Recursion

When a function calls itself, it is called recursion. When a tail call calls itself, it is called tail recursion.

Recursion is very memory-intensive because it requires maintaining hundreds or thousands of call frames simultaneously, making "stack overflow" errors likely. With tail recursion, since only one call frame exists, "stack overflow" errors never occur.

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

factorial(5) // 120
1
2
3
4
5
6

The code above is a factorial function. Computing the factorial of n requires preserving up to n call records, with complexity O(n).

Rewritten as tail recursion with only one call record, the complexity becomes O(1).

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

factorial(5, 1) // 120
1
2
3
4
5
6

Another well-known example is computing the Fibonacci sequence, which also demonstrates the importance of tail recursion optimization.

The non-tail-recursive Fibonacci implementation:

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

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

Fibonacci(10) // 89
Fibonacci(100) // timeout
Fibonacci(500) // timeout
1
2
3
4
5
6
7
8
9

The tail-recursion-optimized Fibonacci implementation:

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
1
2
3
4
5
6
7
8
9

This shows that "tail call optimization" is extremely significant for recursive operations, which is why some functional programming languages include it in their language specifications. ES6 does the same: for the first time, it explicitly specifies that all ECMAScript implementations must deploy "tail call optimization." This means that in ES6, using tail recursion will never cause stack overflow (or timeout from deep recursion), saving memory.

# Rewriting Recursive Functions

Implementing tail recursion often requires rewriting the recursive function to ensure that the last step only calls itself. The way to achieve this is to rewrite all internal variables as function parameters. For example, in the factorial function above, the intermediate variable total is rewritten as a function parameter. The drawback is that this is not very intuitive — it is not immediately obvious why computing the factorial of 5 requires passing two parameters 5 and 1.

Two approaches can solve this problem. The first is to provide a normal-form function outside 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
1
2
3
4
5
6
7
8
9
10

The code above calls the tail-recursive function tailFactorial through a normal-form factorial function factorial, making it look much more natural.

In functional programming, there is a concept called currying, which converts a multi-parameter function into single-parameter form. Currying can also be applied 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

The code above uses currying to convert the tail-recursive function tailFactorial into factorial, which accepts only one parameter.

The second approach is much simpler: use ES6 default parameter values.

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

factorial(5) // 120
1
2
3
4
5
6

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

To summarize, recursion is fundamentally a loop operation. Pure functional programming languages have no loop commands — all loops are implemented through recursion. This is why tail recursion is extremely important for these languages. For other languages that support "tail call optimization" (such as Lua, ES6), you simply need to know that loops can be replaced with recursion, and whenever recursion is used, tail recursion should be preferred.

# Strict Mode

ES6's tail call optimization is only enabled in strict mode; it has no effect in normal mode.

This is because in normal mode, functions have two internal variables that can track the call stack.

  • func.arguments: returns the function's arguments at the time of calling.
  • func.caller: returns the function that called the current function.

When tail call optimization occurs, the function's call stack is rewritten, causing these two variables to become inaccurate. Strict mode disables these two variables, so tail call mode only works in strict mode.

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

# Implementing Tail Recursion Optimization

Since tail recursion optimization only works in strict mode, is there a way to use tail recursion optimization in normal mode or in environments that do not support it? The answer is yes — you can implement it yourself.

The principle is very simple. The reason tail recursion needs optimization is that too many call frames cause overflow. To prevent overflow, just reduce the call frames. How? Replace "recursion" with "loops."

Here 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(…)
1
2
3
4
5
6
7
8
9
10

In the code above, sum is a recursive function. Parameter x is the value to accumulate, and parameter y controls the number of recursions. Specifying 100000 recursions for sum will throw an error indicating the maximum call stack size has been exceeded.

A trampoline function can convert recursive execution into loop execution.

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

The code above is a trampoline function implementation. It accepts a function f as a parameter and continues executing as long as f returns a function. Note that this returns a function and then executes it, rather than calling a function inside a function — this avoids recursive execution and eliminates the problem of an excessively large call stack.

Then, the original recursive function needs to be rewritten so that each step returns another function.

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

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

Now, executing sum using the trampoline function will not cause call stack overflow.

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

The trampoline function is not true tail recursion 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

In the code above, the tco function is the implementation of tail recursion optimization. Its key lies in the state variable active. By default, this variable is inactive. Once the tail recursion optimization process begins, this variable is activated. Then, each round of recursion where sum returns undefined avoids recursive execution. The accumulated array stores the parameters for each round of sum execution, ensuring it always has values. This ensures that the while loop inside the accumulator function always executes. This cleverly converts "recursion" into a "loop," and each round's parameters replace the previous round's, guaranteeing only one layer in the call stack.

# Trailing Commas in Function Parameters

ES2017 allows (opens new window) the last parameter in function definitions to have a trailing comma.

Previously, function definitions and calls did not allow a comma after the last parameter.

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

clownsEverywhere(
  'foo',
  'bar'
);
1
2
3
4
5
6
7
8
9

In the code above, adding a comma after param2 or bar would throw an error.

If written as above, with each parameter on its own line, modifying the code later to add a third parameter or adjust the parameter order would require adding a comma after the previously last parameter. For version control systems, this would show the line where the comma was added as also having changed. This looks redundant, so the new syntax allows trailing commas in definitions and calls.

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

clownsEverywhere(
  'foo',
  'bar',
);
1
2
3
4
5
6
7
8
9

This rule also makes function parameter trailing comma behavior consistent with arrays and objects.

# Function.prototype.toString()

ES2019 (opens new window) modified the toString() method on function instances.

The toString() method returns the function's source code. Previously, it would omit comments and whitespace.

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

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

In the code above, the original source code of function foo includes a comment and whitespace between the function name foo and the parentheses, but toString() omitted them.

The revised toString() method now explicitly requires returning an exact copy of the original source code.

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

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

# Optional catch Binding

JavaScript's try...catch structure previously required the catch clause to be followed by a parameter to receive the error object thrown by the try block.

try {
  // ...
} catch (err) {
  // handle error
}
1
2
3
4
5

In the code above, the catch clause has the parameter err.

In many cases, the catch block may not use this parameter. However, to ensure correct syntax, it still had to be written. ES2019 (opens new window) changed this, allowing the catch clause to omit the parameter.

try {
  // ...
} catch {
  // ...
}
1
2
3
4
5
Edit (opens new window)
#ES6
Last Updated: 2026/03/21, 12:14:36
Number Extensions
Array Extensions

← Number Extensions Array Extensions→

Recent Updates
01
How I Discovered Disposable Email — A True Story
06-12
02
Animations in Grid Layout
09-15
03
Renaming a Git Branch
08-11
More Articles >
Theme by VDone | Copyright © 2026-2026 Nikolay Tuzov | MIT License | Telegram
  • Auto
  • Light Mode
  • Dark Mode
  • Reading Mode