ES6 series 8: array expansion
Extension operator
meaning
The spread operator is three dots (...). It is like the inverse operation of rest parameters, turning an array into a comma separated sequence of parameters.
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 lines array.push(...items) and add(...numbers) are function calls, both of which use the extension operator. This operator changes an array into a sequence of parameters.
The extension 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]);
An expression can also be placed after the extension operator.
const arr = [ ...(x > 0 ? ['a'] : []), 'b', ];
If the extension operator is followed by an empty array, it has no effect.
[...[], 1] // [1]
Note that the extension operator can be placed in parentheses only when the function is called, 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 extension operator is placed in parentheses, but an error will be reported in the first two cases, because the parenthesis of the extension operator is not a function call.
apply method of substitution function
Since the extension operator can expand the array, the apply method is no longer needed to convert the array into a function parameter.
// Writing method of ES5 function f(x, y, z) { // ... } var args = [0, 1, 2]; f.apply(null, args); // Writing method of ES6 function f(x, y, z) { // ... } let args = [0, 1, 2]; f(...args);
The following is a practical example of the extension operator replacing the apply method. The Math.max method is applied to simplify the writing method of finding the largest element of an array.
// Writing method of ES5 Math.max.apply(null, [14, 3, 77]) // Writing method of ES6 Math.max(...[14, 3, 77]) // Equivalent to Math.max(14, 3, 77);
In the above code, because JavaScript does not mention the function of supplying and supplying the largest element of the array, it can only apply the Math.max function to convert the array into a parameter sequence and then find the maximum value. With the extension operator, you can directly use Math.max.
Another example is to add an array to the tail of another array through the push function.
// Writing method of ES5 var arr1 = [0, 1, 2]; var arr2 = [3, 4, 5]; Array.prototype.push.apply(arr1, arr2); // Writing method of ES6 let arr1 = [0, 1, 2]; let arr2 = [3, 4, 5]; arr1.push(...arr2);
In the ES5 writing method of the above code, the parameters of the push method cannot be arrays, so we have to use the push method flexibly through the apply method. With the extension operator, you can pass the array directly into 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 extension operator
(1) Copy array
Array is a composite data type. If it is copied directly, it only copies the pointer to the underlying data structure, rather than cloning a 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 data. Modifying a2 will directly lead to the change of a1.
ES5 can only copy arrays with workarounds.
const a1 = [1, 2]; const a2 = a1.concat(); a2[0] = 2; a1 // [1, 2]
In the above code, a1 will return the clone of the original array, and modifying a2 will not affect a1.
The extension operator provides a convenient way to copy an array.
const a1 = [1, 2]; // Writing method I const a2 = [...a1]; // Writing method 2 const [...a2] = a1;
In the above two ways, a2 is a clone of a1.
(2) Merge array
The extension operator provides a new way to write array merging.
const arr1 = ['a', 'b']; const arr2 = ['c']; const arr3 = ['d', 'e']; // Merged array of ES5 arr1.concat(arr2, arr3); // [ 'a', 'b', 'c', 'd', 'e' ] // Merged array of ES6 [...arr1, ...arr2, ...arr3] // [ 'a', 'b', 'c', 'd', 'e' ]
However, both methods are shallow copies, which should be paid attention to when using.
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 references to the original array members, which is a shallow copy. If the value pointed to by the reference is modified, it will be synchronously reflected in the new array.
(3) Combined with deconstruction assignment
Extension operators can be combined with deconstruction assignment to generate arrays.
// 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 extension operator is used for array assignment, it can only be placed in the last bit of the parameter, otherwise an error will be reported.
const [...butLast, last] = [1, 2, 3, 4, 5]; // report errors const [first, ...middle, last] = [1, 2, 3, 4, 5]; // report errors
(4) String
The extension operator can also convert strings into real arrays.
[...'hello'] // [ "h", "e", "l", "l", "o" ]
An important advantage of the above writing method is that 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 four byte Unicode characters as two characters. There is no problem with the extension operator. 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
This problem applies to functions that operate on four byte Unicode characters. Therefore, it is best to rewrite them all with extension operators.
let str = 'x\uD83D\uDE80y'; str.split('').reverse().join('') // 'y\uDE80\uD83Dx' [...str].reverse().join('') // 'y\uD83D\uDE80x'
In the above code, if the extension operator is not used, the reverse operation of the string is incorrect.
(5) An object that implements the Iterator interface
Any object with an Iterator interface defined can be converted into a real array with an extension 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 extension operator can turn it into a real array because the NodeList object implements the 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 ergodic interface of the Number object is defined first. After the extension operator automatically converts 5 into a Number instance, this interface will be called and custom results will be returned.
For those array like objects that do not have the Iterator interface deployed, the extension 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 extension operator will report an error. In this case, you can use the Array.from method instead to convert arrayLike into a real array.
(6) Map and Set structures, Generator function
The internal call of the extension operator is the Iterator interface of the data structure. Therefore, as long as the object with the Iterator interface can use the extension operator, such as 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 an ergodic object, so you can also use the extension operator.
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 an ergodic object. If the extension operator is executed on the ergodic object, the value obtained by internal traversal will be converted into an array.
If an extension operator is used 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 iteratable objects (including the new data structures Set and Map in ES6).
The following is an array like object. Array.from converts it into a real array.
let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; // Writing method of ES5 var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c'] // Writing method of ES6 let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
In practical applications, common array like objects are the NodeList collection returned by DOM operation 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.
Array.from can convert any data structure with the Iterator interface deployed into 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 string and Set structures have Iterator interfaces, so they can be converted into real arrays by Array.from.
Array.from as like as two peas, the new array is returned to the same array.
Array.from([1, 2, 3]) // [1, 2, 3]
It is worth reminding that the extension operator (...) can also convert some data structures into arrays.
// arguments object function foo() { const args = [...arguments]; } // NodeList object [...document.querySelectorAll('div')]
The extension operator calls behind the traverser interface (Symbol.iterator). 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 attribute. Therefore, any object with the length attribute can be converted to an array through the Array.from method, and the extension operator cannot convert it 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 of each position is undefined. The extension operator cannot convert this object.
For browsers that have not deployed this method, the Array.prototype.slice method can be used instead.
const toArray = (() => Array.from ? Array.from : obj => [].slice.call(obj) )();
Array.from can also accept the second parameter, which is similar to the map method of array. It 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 takes out the text content of 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)
The following example converts a member of an array whose Boolean value is false to 0.
Array.from([1, , 2, , 3], (n) => n || 0) // [1, 0, 2, 0, 3]
Another example is to return various data types.
function typesOf () { return Array.from(arguments, value => typeof value) } typesOf(null, [], NaN) // ['object', 'object', 'number']
If this keyword is used in the map function, you can also pass in the third parameter of Array.from 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 an original data structure, you can first process its value, and then turn it into a standardized 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 into an array and then return the length of the string. Because it can correctly handle various Unicode characters, it can avoid the bug that JavaScript counts 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 deficiency of array constructor Array(). Because the number of parameters is different, 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 or three parameters, the returned results are different. Array() returns a new array of parameters only when the number of parameters is no less than 2. When the parameter has only one positive integer, it actually specifies the length of the array.
Array.of() can basically replace Array() or new Array(), and there is no overload caused by 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 and implemented with the following code.
function ArrayOf(){ return [].slice.call(arguments); }
Instance method: copyWithin()
The copyWithin() method of the array instance copies the members at the specified location to other locations within the current array (overwriting the original members), and then returns the current array. That is, 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 location. If it is negative, it indicates the reciprocal.
- Start (optional): start reading data from this location. The default value is 0. If it is negative, it means to calculate from the end.
- End (optional): stop reading data before reaching this position. By default, it is equal to the length of the array. If it is negative, it means to calculate from the end.
These three parameters should 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 bit 3 to the end of the array are copied to the position starting from bit 0, and the results overwrite 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 is equivalent to position 3 and - 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 bit 2 to the end of the array and to bit 0 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 the copyWithin method of TypedArray // The following wording is required [].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4); // Int32Array [4, 2, 3, 4, 5]
Instance methods: find() and findIndex()
The find method of the array instance is used to find the first qualified array member. Its parameter is a callback function. 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 qualified members, undefined is returned.
[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, namely, the current value, the current position and the original array.
The findIndex method of an array instance is very similar to the find method. It returns the position of the first qualified array member. If all members do not meet the conditions, it returns - 1.
[1, 5, 10, 15].findIndex(function(value, index, arr) { return value > 9; }) // 2
Both methods can accept the 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, the person object, and the this object in the callback function points to the person object.
In addition, NaN can be found in both methods, which makes up for the deficiency of indexOf method of 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 Object.is method.
Instance method: fill()
The fill method fills an array with the given value.
['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 empty arrays. The existing elements in the array will be erased.
The fill method can also accept the second and third parameters to specify the start and end positions of the fill.
['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c']
The above code indicates that the fill method starts from bit 1, fills 7 into the original array, and ends before bit 2.
Note that if the filled type is object, the object assigned is the object with the same memory address, not the 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]]
Instance methods: entries(), keys() and values()
ES6 provides three new methods -- entries (), keys (), and values () -- for traversing arrays. They all return an ergodic object, which can be traversed by the for...of loop. The only difference is that keys() is the traversal of key names, values() is the traversal of key values, and entries() is the 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 do not 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']
Instance method: includes()
The Array.prototype.includes method returns a Boolean value indicating whether an array contains the given value, which is similar to the includes method of string. ES2016 introduces 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, which is 0 by default. If the second parameter is negative, it indicates the position of the reciprocal. If it is greater than the array length (for example, the second parameter is - 4, but the array length is 3), it will be reset to start from 0.
[1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true
Without this method, we usually use the indexOf method of the array to check whether it contains a value.
if (arr.indexOf(el) !== -1) { // ... }
The indexOf method has two disadvantages. First, it is not semantic enough. Its meaning is to find the first occurrence position of the parameter value, so it is not intuitive enough to compare whether it is not equal to - 1. Second, it uses the strict equality operator (= = =) internally, which will lead to misjudgment of NaN.
[NaN].indexOf(NaN) // -1
includes uses different judgment algorithms, so there is no 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 should be distinguished from includes.
- The has method of Map structure is used to find key names, 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) and WeakSet.prototype.has(value).
Example method: flat(), flatMap()
Array members are sometimes arrays. Array.prototype.flat() is used to "flatten" nested arrays into one-dimensional arrays. This method returns a new array without affecting 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.
By default, flat() will "flatten" only one layer. If you want to "flatten" multi-layer nested arrays, you can write the parameters of flat() method as an integer to represent the number of layers you want to flatten, which is 1 by default.
[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-layer nested array.
If you want to convert to a one-dimensional array no matter how many layers of nesting, you can use the Infinity keyword as a parameter.
[1, [2, [3]]].flat(Infinity) // [1, 2, 3]
If the original array has empty bits, the flat() method skips the empty bits.
[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 array composed of return values. 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 layer 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 only one layer can be expanded by default, so flatMap() returns a nested array.
The parameter of flatMap() method is a traversal function, which can accept three parameters: 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 to bind this in the traversal function.
Instance method: at()
For a long time, JavaScript does not support negative indexes of arrays. If you want to reference the last member of an array, you can't write it as arr[-1], and you can only use arr[arr.length - 1].
This is because the square bracket operator [] in the JavaScript language is used not only for arrays, but also for objects. For objects, the key name is in square brackets. For example, obj[1] refers to the key with the key name of string 1. Similarly, obj[-1] refers to the key with the key name of string - 1. Because the array of JavaScript is a special object, the negative number in square brackets can no longer have other semantics, that is, it is impossible to add a new syntax to support negative indexing.
In order to solve this problem, there is now one proposal , an at() method is added to the array instance, which accepts an integer as a parameter, returns the member of the corresponding position, and supports negative index. This method can be used not only for arrays, but also for strings and type arrays (TypedArray).
const arr = [5, 12, 8, 130, 44]; arr.at(2) // 8 arr.at(-2) // 130
If the parameter position exceeds the array range, at() returns undefined.
Empty bit of array
Array vacancy means that there is no value at a certain position of the array. For example, the array returned by the Array() constructor is null.
Array(3) // [, , ,]
In the above code, Array(3) returns an array with 3 empty bits.
Note that the vacancy is not undefined. If the value of a position is equal to undefined, it is still valuable. The empty bit has no value, which can be illustrated by the in operator.
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.
The processing of vacancies in ES5 is very inconsistent, and vacancies are ignored in most cases.
- forEach(), filter(), reduce(), every() and some() will skip the empty bits.
- map() skips the empty bit, but keeps the value
- join() and toString() treat empty bits as undefined, while undefined and null are processed into 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 turns the vacancy to undefined.
The Array.from() method will change the empty bits of the array to undefined, that is, this method will not ignore the empty bits.
Array.from(['a',,'b']) // [ "a", undefined, "b" ]
The extension operator (...) also turns empty bits to undefined.
[...['a',,'b']] // [ "a", undefined, "b" ]
copyWithin() copies the empty bits together.
[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
fill() treats empty bits as normal array positions.
new Array(3).fill('a') // ["a","a","a"]
The for...of loop also traverses the empty bits.
let arr = [, ,]; for (let i of arr) { console.log(1); } // 1 // 1
In the above code, the array arr has two empty bits, and for...of does not ignore them. If the map() method is used to traverse, the empty bits will be skipped.
entries(), keys(), values(), find() and findIndex() will process the empty bits 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
Since the processing rules of vacancies are very inconsistent, it is recommended to avoid vacancies.
Sorting stability of Array.prototype.sort()
stable sorting is an important attribute of sorting algorithm. It means that the order of items with the same sorting keyword remains unchanged before and 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"]
The above code sorts the array arr alphabetically. In the sorting results, straw is in front of the spork, which is consistent with the original order, so the sorting algorithm stableSorting is a 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 sorting result is that spork is in front of straw, which is opposite to the original order, so the sorting algorithm unstableSorting is unstable.
Among the common sorting algorithms, insert sort, merge sort and bubble sort are stable, while heap sort and quick sort are unstable. The main disadvantage of unstable sorting is that multiple sorting may cause problems. Suppose there is a list of last name and first name, which is required to be sorted according to "last name is the primary keyword and first name is the secondary keyword". Developers may sort by first name and then by last name. If the sorting algorithm is stable, the sorting effect of "last name before first name" can be achieved. Not if it's unstable.
Earlier ECMAScript did not specify whether the default sorting algorithm of Array.prototype.sort() is stable or not, which is left to the browser to decide, which leads to some implementations being unstable. ES2019 It is specified that the default sorting algorithm of Array.prototype.sort() must be stable. This provision has been achieved. Now the default sorting algorithms of various main implementations of JavaScript are stable.