Array expansion

Spread operator

Meaning

The spread operator (spread) is three dots (...). It is like the inverse operation of rest parameters, which converts an array into a sequence of parameters separated by commas.

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>]

This operator is mainly used for function calls.

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

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

const numbers = [4, 38];
add(...numbers); // 42

In the above code, the two lines array.push(...items) and add(...numbers) are function calls, and they both use spread operators. This operator turns an array into a sequence of parameters.

The spread operator can be used in combination with normal function parameters, which is very flexible.

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

You can also put expressions after the spread operator.

const arr = [...(x > 0 ? ["a"] : []), "b"];

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

[...[], 1];
// [1]

Note that only when the function is called, the spread operator can be placed in parentheses, otherwise an error will be reported.

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

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

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

In the above three cases, the expansion operator is placed in parentheses, but the first two cases will report an error, because the parenthesis in which the expansion operator is located is not a function call.

Apply method of alternative function

Since the spread operator can expand the array, the apply method is no longer needed to turn the array into a function parameter.

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

// How to write ES6
function f(x, y, z) {
  // ...
}
let args = [0, 1, 2];
f(...args);

The following is a practical example of the spread operator instead of the apply method. The Math.max method is used to simplify the writing of the largest element of an array.

// How to write ES5
Math.max.apply(null, [14, 3, 77]);

// How to write ES6
Math.max(...[14, 3, 77]);

// Equivalent to
Math.max(14, 3, 77);

In the above code, since JavaScript does not provide a function to find the maximum element of an array, so you can only apply the Math.max function to convert the array into a sequence of parameters and then find the maximum value. With the spread operator, you can use Math.max directly.

Another example is to add an array to the end of another array through the push function.

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

// How to write ES6
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);

In the ES5 writing of the above code, the parameter of the push method cannot be an array, so the push method has to be used instead of the apply method. With the spread operator, you can pass the array directly to the push method.

Here is another example.

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

Application of spread operator

(1) Copy the array

Array is a composite data type. If it is copied directly, it just copies the pointer to the underlying data structure instead of cloning a brand new array.

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

a2[0] = 2;
a1; // [2, 2]

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

ES5 can only use workarounds to copy arrays.

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

a2[0] = 2;
a1; // [1, 2]

In the above code, a1 will return a clone of the original array, and modifying a2 will not affect a1.

The spread operator provides a convenient way to copy an array.

const a1 = [1, 2];
// Writing method one
const a2 = [...a1];
// Writing method two
const [...a2] = a1;

In the above two ways of writing, a2 is a clone of a1.

(2) Merge array

The spread operator provides a new way of combining arrays.

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

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

// ES6 merged array
[...arr1, ...arr2, ...arr3];
// ['a','b','c','d','e']

However, both of these methods are shallow copies, so you need to pay attention when using them.

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

In the above code, a3 and a4 are new arrays combined by two different methods, but their members are all references to the original array members, which is a shallow copy. If the value pointed to by the reference is modified, it will be reflected to the new array synchronously.

(3) Combined with destructuring assignment

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

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

Here are some other 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; // []

If the spread operator is used for array assignment, it can only be placed in the last digit of the parameter, otherwise an error will be reported.

const [...butLast, last] = [1, 2, 3, 4, 5];
// report an error

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

(4) String

The spread operator can also convert a string into a real array.

[..."hello"];
// ["h", "e", "l", "l", "o"]

The above wording has an important advantage, that is, it can correctly recognize four-byte Unicode characters.

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

In the first way of writing the above code, JavaScript will recognize a four-byte Unicode character as two characters, and the spread operator does not have this problem. Therefore, the function that correctly returns the length of the string can be written as follows.

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

length("x\uD83D\uDE80y"); // 3

All functions that involve manipulating four-byte Unicode characters have this problem. Therefore, it is best to use spread operators to rewrite.

let str ='x\uD83D\uDE80y';

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

[...str].reverse().join('')
//'y\uD83D\uDE80x'

In the above code, if the spread operator is not used, the reverse operation of the string is incorrect.

(5) Objects that implement the Iterator interface

Any object that defines an Iterator interface (refer to the Iterator chapter) can be converted into a real array using the spread operator.

let nodeList = document.querySelectorAll("div");
let array = [...nodeList];

In the above code, the querySelectorAll method returns a NodeList object. It is not an array, but an array-like object. At this time, the spread operator can turn it into a real array because the NodeList object implements Iterator.

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]

In the above code, the iterator interface of the Number object is first defined. After the spread operator automatically converts the 5 into the Number instance, this interface will be called and the customized result will be returned.

For those array-like objects that do not deploy the Iterator interface, the spread operator cannot turn them into real arrays.

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

// TypeError: Cannot spread non-iterable object.
let arr = [...arrayLike];

In the above code, arrayLike is an array-like object, but if the Iterator interface is not deployed, the spread operator will report an error. At this time, you can use the Array.from method to convert arrayLike into a real array.

(6) Map and Set structure, Generator function

The extension operator internally calls the Iterator interface of the data structure, so any object with the Iterator interface can use the extension operator, such as the Map structure.

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

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

After the Generator function runs, it returns a iterator object, so the spread operator can also be used.

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

[...go()]; // [1, 2, 3]

In the above code, the variable go is a Generator function. After execution, it returns a iterator object. Executing the spread operator on this iterator object will convert the value obtained from the internal traversal into an array.

If you use the spread operator for an object without an Iterator interface, an error will be reported.

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

Array.from()

The Array.from method is used to convert two types of objects into real arrays: array-like objects and iterable objects (including the new ES6 data structures Set and Map).

Below is an array-like object, Array.from turns it into a real array.

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

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

// How to write ES6
let arr2 = Array.from(arrayLike); // ['a','b','c']

In practical applications, common array-like objects are the NodeList collection returned by DOM operations and the arguments object inside the function. Array.from can turn them into real 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);
  // ...
}

In the above code, the querySelectorAll method returns an array-like object. You can turn this object into a real array, and then use the filter method.

As long as the data structure of the Iterator interface is deployed, Array.from can convert it to an array.

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

let namesSet = new Set(["a", "b"]);
Array.from(namesSet); // ['a','b']

In the above code, both the string and the Set structure have the Iterator interface, so it can be converted into a real array by Array.from.

If the argument is a real array, Array.from will return a new array exactly the same.

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

It is worth reminding that the spread operator (...) can also convert some data structures into arrays.

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

// NodeList object
[...document.querySelectorAll("div")];

The iterator interface (Symbol.iterator) is called behind the spread operator. If an object does not deploy this interface, it cannot be converted. The Array.from method also supports array-like objects. The so-called array-like objects have only one essential feature, that is, they must have the length property. Therefore, any object with the length property can be converted to an array through the Array.from method, and the spread operator cannot be converted at this time.

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

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

For browsers that have not deployed this method, you can use the Array.prototype.slice method instead.

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

Array.from can also accept a second parameter, which is similar to the map method of an array, which is used to process each element and put the processed value 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]

The following example is to extract the text content of a group 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);

The following example converts the members with the boolean value of false in the array to 0.

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

Another example is to return various types of data.

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

If the this keyword is used in the map function, the third parameter of Array.from can also be passed in to bind this.

Array.from() can convert various values ​​into real arrays, and also provides the map function. This actually means that as long as there is a primitive data structure, you can process its value first, and then convert it into a standard array structure, and then you can use a large number of array methods.

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

In the above code, the first parameter of Array.from specifies the number of times the second parameter runs. This feature makes the usage of this method very flexible.

Another application of Array.from() is to convert a string to an array, and then return the length of the string. Because it can handle various Unicode characters correctly, it can avoid the bug that JavaScript will count Unicode characters larger than \uFFFF as two characters.

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

Array.of()

The Array.of() method is used to convert a set of values ​​into an array.

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

The main purpose of this method is to make up for the shortcomings of the array constructor Array(). Because of the difference in the number of parameters, the behavior of Array() will be different.

Array(); // []
Array(3); // [,, ,]
Array(3, 11, 8); // [3, 11, 8]

In the above code, when the Array() method has no parameters, one parameter, and three parameters, the returned results are different. Only when the number of parameters is not less than 2, Array() will return a new array composed of parameters. When the parameter has only one positive integer, it actually specifies the length of the array.

Array.of() can basically be used to replace Array() or new Array(), and there is no overloading due to different parameters. Its behavior is very uniform.

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

Array.of() always returns an array of parameter values. If there are no parameters, an empty array is returned.

The Array.of() method can be simulated with the following code.

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

copyWithin() of array instance

The copyWithin() method of the array instance copies the member at the specified position to another position in the current array (it will overwrite the original member), and then returns the current array. In other words, using this method will modify the current array.

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

It accepts three parameters.

-target (required): replace data from this position. If it is a negative value, it means a reciprocal. -start (optional): start reading data from this position, the default is 0. If it is a negative value, it means that the calculation starts from the end. -end (optional): stop reading data before reaching this position, which is equal to the length of the array by default. If it is a negative value, it means that the calculation starts from the end.

These three parameters should all be numerical values, if not, they will be automatically converted to numerical values.

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

The above code means that the members (4 and 5) from position 3 to the end of the array are copied to the position starting from position 0, and the original 1 and 2 are overwritten as a result.

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 is equivalent to position 3, -1 is equivalent to 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 the 2nd position to the end of the array to the 0th position
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]

// For platforms that do not deploy TypedArray's copyWithin method
// Need to use the following wording
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]

find() and findIndex() of array instance

The find method of an array instance is used to find the first qualified array member. Its parameter is a callback function, and all array members execute the callback function in turn until the first member whose return value is true is found, and then the member is returned. If there are no eligible members, return undefined.

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

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

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

In the above code, the callback function of the find method can accept three parameters, which are the current value, the current position, and the original array in turn.

The usage of the findIndex method of an array instance is very similar to the find method. It returns the position of the first array member that meets the condition. If all members do not meet the condition, it returns -1.

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

Both of these methods can accept a second parameter, which is used to bind the this object of the callback function.

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

In the above code, the find function receives the second parameter person object, and the this object in the callback function points to the person object.

In addition, these two methods can find NaN, which makes up for the insufficiency of the indexOf method of the array.

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

  [NaN].findIndex((y) => Object.is(NaN, y));
// 0

In the above code, the indexOf method cannot recognize the NaN member of the array, but the findIndex method can be done with the help of the Object.is method.

Fill() of array instance

The fill method uses the given value to fill an array.

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

new Array(3).fill(7);
// [7, 7, 7]

The above code shows that the fill method is very convenient for the initialization of an empty array. All existing elements in the array will be erased.

The fill method can also accept the second and third parameters, which are used to specify the start and end positions of the filling.

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

The above code indicates that the fill method starts from the 1st position, fills the original array with 7 and ends before the 2nd position.

Note that if the filled type is an object, then the object with the same memory address is assigned instead of a deep copy object.

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]]

The entries(), keys() and values() of the array instance

ES6 provides three new methods-entries(), keys() and values()-for traversing arrays. They all return an iterator object (see the chapter "Iterator" for details), which can be traversed with for...of loop. The only difference is that keys() is a traversal of key names, values() Is a traversal of key-values, and entries() is a traversal of 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"

If you don't use the for...of loop, you can manually call the next method of the iterator object to traverse.

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']

Array instance includes()

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

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

The second parameter of this method indicates the starting position of the search, and the default is 0. If the second parameter is a negative number, it indicates the position of the countdown. If it is greater than the length of the array (for example, the second parameter is -4, but the array length is 3), it will be reset from 0. Start.

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

Before this method, we usually use the indexOf method of the array to check whether it contains a certain value.

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

The indexOf method has two shortcomings. One is that it is not semantic enough. Its meaning is to find the first position of the parameter value, so it is necessary to compare whether it is not equal to -1, which is not intuitive enough to express. The second is that it uses the strict equality operator (===) internally for judgment, which will lead to a misjudgment of NaN.

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

includes uses a different judgment algorithm, so there is no such problem.

[NaN].includes(NaN);
// true

The following code is used to check whether the current environment supports this method, if not, deploy 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

In addition, Map and Set data structures have a has method, which needs to be distinguished from includes.

-The has method of the Map structure is used to find the key name, such as Map.prototype.has(key), WeakMap.prototype.has(key), Reflect.has(target, propertyKey) . -The has method of the Set structure is used to find values, such as Set.prototype.has(value), WeakSet.prototype.has(value).

flat(), flatMap() of array instance

The members of an array are sometimes arrays. Array.prototype.flat() is used to "flatten" the nested array into a one-dimensional array. This method returns a new array, which has no effect on the original data.

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

In the above code, there is an array in the members of the original array. The flat() method takes out the members of the sub-array and adds them to the original position.

flat() will only “flatten” one layer by default. If you want to “flatten” a multi-layer nested array, you can write the parameter of the flat() method as an integer to indicate the number of layers you want to flatten. The default Is 1.

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

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

In the above code, the parameter of flat() is 2, which means to "flatten" the two-level nested array.

If you want to convert it into a one-dimensional array no matter how many levels of nesting there are, you can use the Infinity keyword as a parameter.

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

If the original array has vacancies, the flat() method will skip the vacancies.

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

The flatMap() method executes a function on each member of the original array (equivalent to executing Array.prototype.map()), and then executes the flat() method on the returned value array. This method returns a new array without changing the original array.

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

flatMap() can only expand one level of array.

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

In the above code, the traversal function returns a double-layer array, but by default only one layer can be expanded, so flatMap() returns a nested array.

The parameter of the flatMap() method is a traversal function, which can accept three parameters, which are the current array member, the position of the current array member (starting from zero), and the original array.

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

The flatMap() method can also have a second parameter, which is used to bind the this in the traversal function.

Array space

The vacancy of the array means that there is no value in a certain position of the array. For example, the array returned by the Array constructor is empty.

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

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

Note that the space is not undefined, the value of a position is equal to undefined, and it still has a value. Gaps have no value, and the in operator can illustrate this point.

0 in [undefined, undefined, undefined]; // true
0 in [, , ,]; // false

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

ES5's handling of vacancies is already very inconsistent, and vacancies are ignored in most cases.

-forEach(), filter(), reduce(), every() and some() will all skip empty slots. -map() will skip the space, but will keep this value -join() and toString() will treat gaps as undefined, while undefined and null will be treated 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,,"

ES6 explicitly converts vacancies to undefined.

The Array.from method will convert the space of the array to undefined, that is, this method will not ignore the space.

Array.from(["a", , "b"]);
// ["a", undefined, "b"]

The spread operator (...) will also convert the gap to undefined.

[...["a", , "b"]];
// ["a", undefined, "b"]

copyWithin() will copy the empty space together.

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

fill() treats empty positions as normal array positions.

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

The for...of loop will also traverse the slots.

let arr = [, ,];
for (let i of arr) {
  console.log(1);
}
// 1
// 1

In the above code, the array arr has two spaces, and for...of does not ignore them. If you change to the map method to traverse, the space will be skipped.

entries(), keys(), values(), find() and findIndex() will treat empty spaces 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

As the rules for handling vacancies are very inconsistent, it is recommended to avoid vacancies.

Sort stability of Array.prototype.sort()

Sorting stability (stable sorting) is an important attribute of the sorting algorithm. It refers to items with the same sorting key and the order before and after sorting remains unchanged.

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"]

The above code sorts the array arr according to the first letter. In the sorting result, straw is before spork, which is consistent with the original order, so the sorting algorithm stableSorting is stable sorting.

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

arr.sort(unstableSorting);
// ["apple", "peach", "spork", "straw"]

In the above code, the sort result is spork before straw, which is opposite to the original order, so the sorting algorithm unstableSorting is unstable.

Among the common sorting algorithms, insertion sort, merge sort, bubble sort, etc. are all stable, while heap sort, quick sort, etc. are unstable. The main disadvantage of unstable sorting is that it may cause problems in multiple sorting. Suppose there is a list of surnames and first names, and it is required to sort by "last name as the main keyword and first name as the secondary keyword". Developers may sort by first name and then by last name. If the sorting algorithm is stable, then the sorting effect of "surname first, first name" can be achieved. If it is unstable, it won't work.

Earlier ECMAScript did not stipulate whether the default sorting algorithm of Array.prototype.sort() is stable or not is left to the browser's own decision, which causes some implementations to be unstable. ES2019 clearly stipulates that the default sorting algorithm of Array.prototype.sort() must be stable. This provision has been achieved, and now the default sorting algorithms of all major implementations of JavaScript are stable.