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
  • Array Extensions
    • Spread Operator
      • Definition
      • Replacing the apply Method
      • Applications of the Spread Operator
    • Array.from()
    • Array.of() — Always Returns an Array of Argument Values
    • Array Instance Method: copyWithin()
    • Array Instance Methods: find() and findIndex()
    • Array Instance Method: fill()
    • Array Instance Methods: entries(), keys(), and values()
    • Array Instance Method: includes()
    • Array Instance Methods: flat() and flatMap()
    • Array Empty Slots
    • Sorting Stability of Array.prototype.sort()
  • 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

Array Extensions

# Array Extensions

# Spread Operator

# Definition

The spread operator is three dots (...). It is the inverse of rest parameters, converting an array into a comma-separated sequence of arguments.

console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
1
2
3
4
5
6
7
8

This operator is primarily used in function calls.

function push(array, ...items) {
  array.push(...items);
}

function add(x, y) {
  return x + y;
}

const numbers = [4, 38];
add(...numbers) // 42
1
2
3
4
5
6
7
8
9
10

In the code above, both array.push(...items) and add(...numbers) are function calls that use the spread operator. The operator converts an array into an argument sequence.

The spread operator can be combined with regular function parameters very flexibly.

function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);
1
2
3

Expressions can also be placed after the spread operator.

const arr = [
  ...(x > 0 ? ['a'] : []),
  'b',
];
1
2
3
4

If the spread operator is followed by an empty array, it has no effect.

[...[], 1]
// [1]
1
2

Note that the spread operator can only be placed inside parentheses during function calls; otherwise, an error is thrown.

(...[1, 2])
// Uncaught SyntaxError: Unexpected number

console.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected number

console.log(...[1, 2])
// 1 2
1
2
3
4
5
6
7
8

In the three cases above, the spread operator is inside parentheses, but the first two cases throw errors because the parentheses containing the spread operator are not function calls.

# Replacing the apply Method

Since the spread operator can expand arrays, the apply method is no longer needed to convert arrays into function arguments.

// ES5 approach
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6 approach
function f(x, y, z) {
  // ...
}
let args = [0, 1, 2];
f(...args);
1
2
3
4
5
6
7
8
9
10
11
12
13

Below is a practical example of replacing apply with the spread operator, using Math.max to simplify finding the maximum element of an array.

// ES5 approach
Math.max.apply(null, [14, 3, 77])

// ES6 approach
Math.max(...[14, 3, 77])

// equivalent to
Math.max(14, 3, 77);
1
2
3
4
5
6
7
8

In the code above, since JavaScript does not provide a function to find the maximum element of an array, Math.max must be used with the array converted to an argument sequence to find the maximum. With the spread operator, Math.max can be used directly.

Another example is appending one array to the end of another using the push function.

// ES5 approach
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES6 approach
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);
1
2
3
4
5
6
7
8
9

In the ES5 version above, push does not accept arrays as arguments, so apply was used as a workaround. With the spread operator, arrays can be passed directly to push.

Here is another example.

// ES5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
// ES6
new Date(...[2015, 1, 1]);
1
2
3
4

# Applications of the Spread Operator

(1) Copying Arrays

Arrays are composite data types. Directly copying an array only copies the pointer to the underlying data structure, not a cloned new array.

const a1 = [1, 2];
const a2 = a1;

a2[0] = 2;
a1 // [2, 2]
1
2
3
4
5

In the code above, a2 is not a clone of a1 but rather another pointer to the same data. Modifying a2 directly causes a1 to change.

ES5 required a workaround to copy arrays.

const a1 = [1, 2];
const a2 = a1.concat();

a2[0] = 2;
a1 // [1, 2]
1
2
3
4
5

In the code above, a1 returns a clone of the original array. Modifying a2 afterwards does not affect a1.

The spread operator provides a convenient way to copy arrays.

const a1 = [1, 2];
// Approach 1
const a2 = [...a1];
// Approach 2
const [...a2] = a1;
1
2
3
4
5

In both approaches above, a2 is a clone of a1.

(2) Merging Arrays

The spread operator provides a new way to merge arrays.

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 array merge
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 array merge
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
1
2
3
4
5
6
7
8
9
10
11

However, both methods are shallow copies, so caution is needed.

const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];

const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];

a3[0] === a1[0] // true
a4[0] === a1[0] // true
1
2
3
4
5
6
7
8

In the code above, a3 and a4 are new arrays merged using two different methods, but their members are references to the original array members. This is shallow copying. Modifying the value a reference points to will be reflected in the new array.

(3) Combined with Destructuring Assignment

The spread operator can be combined with destructuring assignment to generate arrays.

// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
1
2
3
4

Here are some more examples.

const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest  // []

const [first, ...rest] = ["foo"];
first  // "foo"
rest   // []
1
2
3
4
5
6
7
8
9
10
11

When using the spread operator for array assignment, it can only be placed in the last position; otherwise, an error is thrown.

const [...butLast, last] = [1, 2, 3, 4, 5];
// Error

const [first, ...middle, last] = [1, 2, 3, 4, 5];
// Error
1
2
3
4
5

(4) Strings

The spread operator can also convert strings into true arrays.

[...'hello']
// [ "h", "e", "l", "l", "o" ]
1
2

An important benefit of this approach is correctly recognizing four-byte Unicode characters.

'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3
1
2

In the first approach above, JavaScript identifies a four-byte Unicode character as 2 characters. The spread operator does not have this problem. Therefore, a function that correctly returns string length can be written as follows.

function length(str) {
  return [...str].length;
}

length('x\uD83D\uDE80y') // 3
1
2
3
4
5

All functions that operate on four-byte Unicode characters have this issue. Therefore, it is best to rewrite them using the spread operator.

let str = 'x\uD83D\uDE80y';

str.split('').reverse().join('')
// 'y\uDE80\uD83Dx'

[...str].reverse().join('')
// 'y\uD83D\uDE80x'
1
2
3
4
5
6
7

In the code above, without the spread operator, the string's reverse operation would be incorrect.

(5) Objects Implementing the Iterator Interface

Any object that has a defined iterator interface (see the Iterator chapter) can be converted to a true array using the spread operator.

let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
1
2

In the code above, querySelectorAll returns a NodeList object, which is not an array but an array-like object. The spread operator can convert it to a true array because the NodeList object implements the Iterator interface.

Number.prototype[Symbol.iterator] = function*() {
  let i = 0;
  let num = this.valueOf();
  while (i < num) {
    yield i++;
  }
}

console.log([...5]) // [0, 1, 2, 3, 4]
1
2
3
4
5
6
7
8
9

In the code above, an iterator interface is defined on Number's prototype. After the spread operator automatically converts 5 to a Number instance, it calls this interface, returning the custom result.

For array-like objects that have not implemented the Iterator interface, the spread operator cannot convert them to true arrays.

let arrayLike = {
  '0': 'a',
  '1': 'b',
  '2': 'c',
  length: 3
};

// TypeError: Cannot spread non-iterable object.
let arr = [...arrayLike];
1
2
3
4
5
6
7
8
9

In the code above, arrayLike is an array-like object without the Iterator interface, so the spread operator throws an error. In this case, Array.from can be used to convert arrayLike to a true array.

(6) Map and Set Structures, Generator Functions

The spread operator internally calls the Iterator interface of the data structure. Therefore, any object with an Iterator interface can use the spread operator, such as Map structures.

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]
1
2
3
4
5
6
7

Generator functions return an iterator object after execution, so the spread operator can also be used.

const go = function*(){
  yield 1;
  yield 2;
  yield 3;
};

[...go()] // [1, 2, 3]
1
2
3
4
5
6
7

In the code above, variable go is a Generator function. After execution, it returns an iterator object. Applying the spread operator to this iterator converts the internally iterated values into an array.

Using the spread operator on objects without an Iterator interface will throw an error.

const obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object
1
2

# Array.from()

The Array.from method converts two types of objects into true arrays: array-like objects and iterable objects (including the new ES6 data structures Set and Map).

Below is an array-like object that Array.from converts into a true array.

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5 approach
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6 approach
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
1
2
3
4
5
6
7
8
9
10
11
12

In practice, common array-like objects include NodeList collections from DOM operations and the arguments object inside functions. Array.from can convert both into true arrays.

// NodeList object
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
  return p.textContent.length > 100;
});

// arguments object
function foo() {
  var args = Array.from(arguments);
  // ...
}
1
2
3
4
5
6
7
8
9
10
11

In the code above, querySelectorAll returns an array-like object that can be converted to a true array and then use the filter method.

Any data structure with a deployed Iterator interface can be converted by Array.from.

Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']

let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
1
2
3
4
5

In the code above, both strings and Set structures have Iterator interfaces, so they can be converted by Array.from.

If the argument is already a true array, Array.from returns an identical new array.

Array.from([1, 2, 3])
// [1, 2, 3]
1
2

It is worth noting that the spread operator (...) can also convert certain data structures to arrays.

// arguments object
function foo() {
  const args = [...arguments];
}

// NodeList object
[...document.querySelectorAll('div')]
1
2
3
4
5
6
7

The spread operator relies on the iterator interface (Symbol.iterator). If an object has not deployed this interface, it cannot be converted. Array.from also supports array-like objects. The essential characteristic of an array-like object is simply having a length property. Therefore, any object with a length property can be converted to an array via Array.from, while the spread operator cannot.

Array.from({ length: 3 });
// [ undefined, undefined, undefined ]
1
2

In the code above, Array.from returns an array with three members, each with a value of undefined. The spread operator cannot convert this object.

For browsers that have not implemented this method, Array.prototype.slice can be used as a substitute.

const toArray = (() =>
  Array.from ? Array.from : obj => [].slice.call(obj)
)();
1
2
3

Array.from also accepts a second argument that works like the array's map method, processing each element and placing the processed values into the returned array.

Array.from(arrayLike, x => x * x);
// equivalent to
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]
1
2
3
4
5
6

Below is an example of extracting text content from a set of DOM nodes.

let spans = document.querySelectorAll('span.name');

// map()
let names1 = Array.prototype.map.call(spans, s => s.textContent);

// Array.from()
let names2 = Array.from(spans, s => s.textContent)
1
2
3
4
5
6
7

The following example converts array members with falsy boolean values to 0.

Array.from([1, , 2, , 3], (n) => n || 0)
// [1, 0, 2, 0, 3]
1
2

Another example returns the type of various data.

function typesOf () {
  return Array.from(arguments, value => typeof value)
}
typesOf(null, [], NaN)
// ['object', 'object', 'number']
1
2
3
4
5

If the map function uses the this keyword, a third argument can be passed to Array.from to bind this.

Array.from() can convert various values to true arrays and provides map functionality. This effectively means that given any raw data structure, you can first process its values and then convert it to a standard array structure, enabling the use of numerous array methods.

Array.from({ length: 2 }, () => 'jack')
// ['jack', 'jack']
1
2

In the code above, the first argument to Array.from specifies how many times the second argument runs. This feature makes the method very flexible.

Another application of Array.from() is converting a string to an array and then returning the string length. Because it correctly handles various Unicode characters, it avoids JavaScript's bug of counting characters greater than \uFFFF as two characters.

function countSymbols(string) {
  return Array.from(string).length;
}
1
2
3

# Array.of() — Always Returns an Array of Argument Values

The Array.of method converts a set of values into an array.

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
1
2
3

The main purpose of this method is to address the inconsistency of the Array() constructor. Depending on the number of arguments, Array() behaves differently.

Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
1
2
3

In the code above, Array behaves differently with zero, one, or three arguments. Only when there are 2 or more arguments does Array() return a new array composed of the arguments. With only one argument, it actually specifies the array length.

Array.of can essentially replace Array() or new Array() without the overloading issues caused by different arguments. Its behavior is very consistent.

Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]
1
2
3
4

Array.of always returns an array composed of the argument values. If there are no arguments, it returns an empty array.

The Array.of method can be polyfilled as follows.

function ArrayOf(){
  return [].slice.call(arguments);
}
1
2
3

# Array Instance Method: copyWithin()

The copyWithin() method copies members from a specified position within the current array to another position (overwriting existing members) and returns the current array. In other words, this method modifies the current array.

Array.prototype.copyWithin(target, start = 0, end = this.length)
1

It accepts three arguments.

  • target (required): Start replacing data from this position. If negative, it counts from the end.
  • start (optional): Start reading data from this position, default is 0. If negative, it counts from the end.
  • end (optional): Stop reading data before this position, default equals the array length. If negative, it counts from the end.

All three arguments should be numbers; if not, they are automatically converted.

[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
1
2

The code above copies members from position 3 to the end of the array (4 and 5) to positions starting from 0, overwriting the original 1 and 2.

Here are more examples.

// Copy position 3 to position 0
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]

// -2 equals position 3, -1 equals position 4
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]

// Copy position 3 to position 0
[].copyWithin.call({length: 5, 3: 1}, 0, 3)
// {0: 1, 3: 1, length: 5}

// Copy from position 2 to end, to position 0
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]

// For platforms that have not deployed TypedArray's copyWithin method
// use the following approach
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# Array Instance Methods: find() and findIndex()

The find method of array instances finds the first array member that satisfies a condition. Its argument is a callback function that is executed by all array members in sequence until the first member returning true is found and returned. If no member satisfies the condition, undefined is returned.

[1, 4, -5, 10].find((n) => n < 0)
// -5
1
2

The code above finds the first member less than 0 in the array.

[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10
1
2
3

In the code above, the find method's callback can accept three arguments: the current value, the current position, and the original array.

The findIndex method works very similarly to find, but returns the position of the first qualifying array member. If no member qualifies, it returns -1.

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2
1
2
3

Both methods accept a second argument to bind the callback function's this object.

function f(v){
  return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person);    // 26
1
2
3
4
5

In the code above, the find function receives a second argument, the person object. The this object in the callback refers to the person object.

Additionally, both methods can detect NaN, addressing a limitation of the array's indexOf method.

[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0
1
2
3
4
5

In the code above, the indexOf method cannot identify NaN in an array, but findIndex can with the help of Object.is.

# Array Instance Method: fill()

The fill method fills an array with a given value.

['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]
1
2
3
4
5

The code above shows that fill is very convenient for initializing empty arrays. Existing elements in the array are completely replaced.

The fill method also accepts a second and third argument to specify the start position and end position for filling.

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
1
2

The code above indicates that fill starts filling at position 1 with the value 7, stopping before position 2.

Note that if the fill type is an object, the assigned values share the same memory address — it is not a deep copy.

let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]

let arr = new Array(3).fill([]);
arr[0].push(5);
arr
// [[5], [5], [5]]
1
2
3
4
5
6
7
8
9

# Array Instance Methods: entries(), keys(), and values()

ES6 provides three new methods — entries(), keys(), and values() — for iterating over arrays. They all return an iterator object (see the Iterator chapter) that can be traversed with for...of loops. The only difference is that keys() iterates over key names, values() iterates over key values, and entries() iterates over key-value pairs.

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Without for...of, you can manually call the iterator object's next method for traversal.

let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']
1
2
3
4
5

# Array Instance Method: includes()

Array.prototype.includes returns a boolean indicating whether an array contains a given value, similar to the string includes method. ES2016 introduced this method.

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true
1
2
3

The method's second argument specifies the starting search position, defaulting to 0. If the second argument is negative, it indicates a position counted from the end. If this results in a value greater than the array length (e.g., second argument is -4 but array length is 3), the search resets to start from 0.

[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true
1
2

Before this method existed, the array's indexOf method was typically used to check whether a value is present.

if (arr.indexOf(el) !== -1) {
  // ...
}
1
2
3

indexOf has two drawbacks. First, it is not very semantic — its meaning is to find the first occurrence position of the argument value, so comparing against -1 is not very intuitive. Second, it uses strict equality (===) internally, which causes incorrect results for NaN.

[NaN].indexOf(NaN)
// -1
1
2

includes uses a different comparison algorithm and does not have this problem.

[NaN].includes(NaN)
// true
1
2

The following code checks whether the current environment supports this method. If not, it deploys a simple alternative version.

const contains = (() =>
  Array.prototype.includes
    ? (arr, value) => arr.includes(value)
    : (arr, value) => arr.some(el => el === value)
)();
contains(['foo', 'bar'], 'baz'); // => false
1
2
3
4
5
6

Additionally, Map and Set data structures have a has method that should not be confused with includes.

  • Map's has method is for looking up key names, e.g., Map.prototype.has(key), WeakMap.prototype.has(key), Reflect.has(target, propertyKey).
  • Set's has method is for looking up values, e.g., Set.prototype.has(value), WeakSet.prototype.has(value).

# Array Instance Methods: flat() and flatMap()

Array members are sometimes themselves arrays. Array.prototype.flat() flattens nested arrays into a one-dimensional array. This method returns a new array without modifying the original.

[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]
1
2

In the code above, a member of the original array is an array. flat() extracts the sub-array's members and places them at the original position.

flat() only flattens one level by default. To flatten multiple levels of nested arrays, pass an integer argument to flat() specifying the number of levels to flatten, with a default of 1.

[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
1
2
3
4
5

In the code above, flat() has an argument of 2, meaning two levels of nesting are flattened.

To flatten all levels of nesting into a one-dimensional array, use the Infinity keyword as the argument.

[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]
1
2

If the original array has empty slots, flat() skips them.

[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]
1
2

The flatMap() method executes a function on each member of the original array (equivalent to Array.prototype.map()), then calls flat() on the resulting array. This method returns a new array without modifying the original.

// equivalent to [2, 3, 4].map(x => [x, x*2]).flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
1
2
3

flatMap() can only flatten one level of arrays.

// equivalent to [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]
1
2
3

In the code above, the traversal function returns a double-nested array, but since only one level can be flattened by default, flatMap() still returns a nested array.

The flatMap() method's argument is a traversal function that can accept three arguments: the current array member, the current position (zero-based), and the original array.

arr.flatMap(function callback(currentValue[, index[, array]]) {
  // ...
}[, thisArg])
1
2
3

flatMap() can also accept a second argument to bind this inside the traversal function.

# Array Empty Slots

An array empty slot means a certain position in an array has no value. For example, arrays returned by the Array constructor are all empty slots.

Array(3) // [, , ,]
1

In the code above, Array(3) returns an array with 3 empty slots.

Note that an empty slot is not undefined. A position with a value of undefined still has a value. An empty slot has no value at all. The in operator illustrates this.

0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false
1
2

The code above shows that position 0 of the first array has a value, while position 0 of the second array has no value.

ES5's handling of empty slots was already very inconsistent, with most operations ignoring them.

  • forEach(), filter(), reduce(), every(), and some() all skip empty slots.
  • map() skips empty slots but preserves the value.
  • join() and toString() treat empty slots as undefined, and undefined and null are processed as empty strings.
// forEach method
[,'a'].forEach((x,i) => console.log(i)); // 1

// filter method
['a',,'b'].filter(x => true) // ['a','b']

// every method
[,'a'].every(x => x==='a') // true

// reduce method
[1,,2].reduce((x,y) => x+y) // 3

// some method
[,'a'].some(x => x !== 'a') // false

// map method
[,'a'].map(x => 1) // [,1]

// join method
[,'a',undefined,null].join('#') // "#a##"

// toString method
[,'a',undefined,null].toString() // ",a,,"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

ES6 explicitly converts empty slots to undefined.

The Array.from method converts array empty slots to undefined, meaning it does not ignore empty slots.

Array.from(['a',,'b'])
// [ "a", undefined, "b" ]
1
2

The spread operator (...) also converts empty slots to undefined.

[...['a',,'b']]
// [ "a", undefined, "b" ]
1
2

copyWithin() copies empty slots as well.

[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
1

fill() treats empty slots as normal array positions.

new Array(3).fill('a') // ["a","a","a"]
1

for...of loops also iterate over empty slots.

let arr = [, ,];
for (let i of arr) {
  console.log(1);
}
// 1
// 1
1
2
3
4
5
6

In the code above, array arr has two empty slots. for...of does not ignore them. Using map for traversal would skip the empty slots.

entries(), keys(), values(), find(), and findIndex() treat empty slots as undefined.

// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]

// keys()
[...[,'a'].keys()] // [0,1]

// values()
[...[,'a'].values()] // [undefined,"a"]

// find()
[,'a'].find(x => true) // undefined

// findIndex()
[,'a'].findIndex(x => true) // 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Since empty slot handling rules are very inconsistent, it is recommended to avoid empty slots altogether.

# Sorting Stability of Array.prototype.sort()

Sorting stability is an important property of sorting algorithms. It means that items with the same sorting key maintain their original relative order after sorting.

const arr = [
  'peach',
  'straw',
  'apple',
  'spork'
];

const stableSorting = (s1, s2) => {
  if (s1[0] < s2[0]) return -1;
  return 1;
};

arr.sort(stableSorting)
// ["apple", "peach", "straw", "spork"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14

The code above sorts the array arr by first letter. In the result, straw comes before spork, consistent with the original order. This means the sorting algorithm stableSorting is a stable sort.

const unstableSorting = (s1, s2) => {
  if (s1[0] <= s2[0]) return -1;
  return 1;
};

arr.sort(unstableSorting)
// ["apple", "peach", "spork", "straw"]
1
2
3
4
5
6
7

In the code above, the result has spork before straw, which is the reverse of the original order. This means the sorting algorithm unstableSorting is unstable.

Among common sorting algorithms, insertion sort, merge sort, and bubble sort are stable, while heap sort and quicksort are unstable. The main drawback of unstable sorting is that it can cause problems with multi-key sorting. Suppose you have a list of first names and last names, and you want to sort by "last name as primary key, first name as secondary key." A developer might first sort by first name, then by last name. If the sorting algorithm is stable, this achieves the "last name first, then first name" sorting effect. If it is unstable, it does not.

The earlier ECMAScript specification did not require Array.prototype.sort()'s default sorting algorithm to be stable, leaving it up to browsers to decide. This resulted in some unstable implementations. ES2019 (opens new window) explicitly specifies that Array.prototype.sort()'s default sorting algorithm must be stable. This has been achieved, and all major JavaScript implementations now use stable default sorting algorithms.

Edit (opens new window)
#ES6
Last Updated: 2026/03/21, 12:14:36
Function Extensions
Object Extensions

← Function Extensions Object 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