Interviewer: how does JavaScript implement the array flattening (flattening) method?
What is array flattening?
The concept is very simple. It means to reduce the dimension of a "multi-dimensional" array, such as:
// The original array is a "three-dimensional" array const array = [1, 2, [3, 4, [5, 6], 7], 8, 9] // It can be reduced to two dimensions newArray1 = [1, 2, 3, 4, [5, 6], 7, 8, 9] // It can also be reduced to one dimension newArray2 = [1, 2, 3, 4, 5, 6, 7, 8, 9]
Array flattening is also called array flattening and array dimensionality reduction.
2. Array flattening method in JS standard library
The array flattening method Array.prototype.flat() has been implemented in the JavaScript standard library
The flat() method recursively traverses the array according to a specified depth, and combines all elements with the elements in the traversed sub array into a new array.
Syntax: var newArray = arr.flat([depth])
Parameter: depth is an optional value, indicating the depth to traverse the multidimensional array. The default value is 1. It can be understood as the number of layers you want to expand (or reduce dimension).
Return value: a new array composed of the traversed elements and the elements of the subarray
give an example:
const array = [1, 2, [3, 4, [5, 6], 7], 8, 9] const newArray1 = array.flat() // Equivalent to array.flat(1); Dimensionality reduction // newArray1: [1, 2, 3, 4, [ 5, 6 ], 7, 8, 9] const newArray2 = array.flat(2) // Dimensionality reduction // newArray2: [1, 2, 3, 4, 5, 6, 7, 8, 9]
Special:
- When depth < = 0, the returned array has the same dimension as the original array (note that only the dimension is the same, see point 3 for vacancy)
const array = [1, 2, [3, 4, [5, 6], 7], 8, 9] array.flat(-1) // [1, 2, [3, 4, [5, 6], 7], 8, 9]
- depth=Infinity, the returned array becomes one-dimensional
array.flat(Infinity) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
- The original array has empty bits. The flat method will eliminate the empty bits, and even flat(0) will eliminate the empty bits. Therefore, point 1 says "only the same dimension". And the vacancy will be eliminated to which layer the flat method is expanded to, and the vacancy in the deeper layer will not be eliminated
const array1 = [1, , 2, [3, ,4, [5, 6], 7], 8, 9] // Notice that this array has two empty bits array.flat(0) // [ 1, 2, [ 3, ,4, [ 5, 6 ], 7 ], 8, 9 ] // The first vacancy is gone and the second vacancy is still there
3 implement a flat method
The flat method expands one layer (reducing one dimension): traverse the array to determine whether the current element is an array. If it is not an array, save it directly; If it is an array, expand it and save it
flat method to expand multi layers (reduce multi dimensions) is nothing more than to use recursion to perform the same operation on the sub elements of the array on the basis of expanding one layer.
This method can be divided into three steps:
1. How to traverse an array
2. How to determine whether an element is an array
3. Recursion
Implement the above three steps and combine them to get different flat implementations
3.1 how to traverse an array
There are many methods. Here are three types:
1. for related
- for loop
- for...of
for...in is built to traverse object properties and is not recommended for use with arrays
const array = [1, 2, [3, 4, [5, 6], 7], 8, 9] // for loop for (let i = 0; i < array.length; i++) { const element = array[i]; } // for...of for (const element of array) { }
2. Array method: a method that can directly get array elements
- forEach()
- reduce()
- map()
// forEach() array.forEach(element => { }); // reduce() array.reduce((pre, cur) => { const element = cur }, []) // map() array.map(element => { })
3. Array method: the method that returns the iterator object
- keys()
- values()
- entries()
// These three methods only obtain the traverser object, and need to be combined with for...of to traverse // keys() for (let i of array.keys()) { const element = array[i] } // values() for (let element of array.values() ) { } // entries() for (let [i, element] of array.entries()) { console.log(array[i]) console.log(element) }
3.2 how to determine whether an element is an array
A variable a is set to judge whether it is an array. Here are four methods:
- Array has a static method Array.isArray() to determine whether a variable is an array
The instanceof operator is used to detect whether the prototype attribute of the constructor appears on the prototype chain of an instance object.
If a is an array, Array.prototype will appear on its prototype chain
- Judge by the constructor of the object (this method may fail because the constructor can be changed manually)
- Judging by Object.prototype.toString(), this method can return a string representing the object
// Method 1 Array.isArray(a) // Method 2 a instanceof Array // Method 3 a.constructor === Array // Method 4 // Use call to call the toString method on Object.prototype Object.prototype.toString.call(a) === '[object Array]' // You can't judge this because this toString already overrides Object.prototype.toString // Only Object.prototype.toString can correctly judge the type a.toString()
3.3 recursion
Recursion: do the same for child elements
function flat() { let res = [] Traversal array { if (The current element is an array) { flat(Current element)Get a one-dimensional array Splicing one-dimensional arrays into res in } else { res.push(Current element) } } return res }
3.4 preliminary implementation of flat method
Select the traversal mode and array judgment mode, and combine recursion to preliminarily realize the flat method, such as:
function myFlat(arr) { let res = []; for (const item of arr) { if (Array.isArray(item)) { res = res.concat(myFlat(item)); // Note that the concat method returns a new array without changing the original array } else { res.push(item); } } return res; }
The myFlat method can flatten a "multi-dimensional" array into a one-dimensional array, but it cannot specify the depth of expansion, and it cannot handle array vacancies
4 optimization
4.1 specify deployment depth
In fact, it is very simple to handle the expansion depth. We can add a recursive termination condition, that is, depth < = 0. The code is as follows:
function myFlat(arr, depth = 1) { // If depth < = 0, return directly if (depth <= 0) { return arr } let res = []; for (const item of arr) { if (Array.isArray(item)) { // Every recursive call, depth-1 res = res.concat(myFlat(item, depth - 1)); } else { res.push(item); } } return res; }
4.2 array vacancy processing
In fact, we should try to avoid empty arrays
We mentioned the different methods of traversing an array, which deal with array vacancies differently
When traversing forEach, reduce and map, empty bits will be directly ignored; for...of will not be ignored. If it encounters a vacancy, it will be treated as undefined
4.2.1 for...of adding vacancy judgment
Therefore, we need to improve the myFlat method of for...of traversing the array:
function myFlat(arr, depth = 1) { if (depth <= 0) { return arr; } let res = []; for (const item of arr) { if (Array.isArray(item)) { res = res.concat(myFlat(item, depth - 1)); } else { // Determine array vacancy item !== undefined && res.push(item); } } return res; }
4.2.2 traversal of foreach and map methods
Of course, you can also use the forEach and map methods to traverse the array, so you don't have to judge manually
However, there is a special case to consider here, that is, when depth < = 0, we use the filter method to eliminate the array vacancy
// forEach function myFlat(arr, depth = 1) { if (depth <= 0) { return arr.filter(item => item !== undefined); } let res = []; arr.forEach((item) => { if (Array.isArray(item)) { res = res.concat(myFlat(item, depth - 1)); } else { res.push(item); } }); return res; } // map function myFlat(arr, depth = 1) { if (depth <= 0) { return arr.filter(item => item !== undefined); } let res = []; arr.map((item) => { if (Array.isArray(item)) { res = res.concat(myFlat(item, depth - 1)); } else { res.push(item); } }); return res; }
4.2.3 reduce method
Among them, the use of reduce method is the most concise, and it is also one of the methods often tested in the interview
function myFlat(arr, depth = 1) { return depth > 0 ? arr.reduce( (pre, cur) => pre.concat(Array.isArray(cur) ? myFlat(cur, depth - 1) : cur), [] ) : arr.filter((item) => item !== undefined); }
5 others
5.1 stack
In theory, recursive methods can usually be converted to non recursive methods, even with stacks
function myFlat(arr) { let res = []; const stack = [].concat(arr); while (stack.length > 0) { const item = stack.pop(); if (Array.isArray(item)) { // Expand one layer with extension operator stack.push(...item); } else { item !== undefined && res.unshift(item); } } return res; }
However, this method cannot specify the expansion depth, and can only be completely expanded into a one-dimensional array
5.2 improvement
To improve the disadvantage that the stack cannot specify the expansion depth, the code is as follows:
function myFlat(arr, depth = 1) { if (depth <= 0) { return arr.filter((item) => item !== undefined); } let res; let queue = [].concat(arr); while (depth > 0) { res = []; queue.forEach((item) => { if (Array.isArray(item)) { // Note that the filter method is used to remove the empty bits before expanding the array with the extension operator res.push(...item.filter((e) => e !== undefined)); } else { res.push(item); } }); depth--; queue = res; } return res; }
Official account [front end]