发布于 2026-01-05 1 阅读
0

通过实现来理解数组方法——DEV 的全球展示与讲述挑战赛,由 Mux 呈现:展示你的项目!

通过实现所有数组方法来理解它们。

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

作者:Maciej Cieślar ✏️

要对给定数组使用方法,我们输入 ` [].methodName.`。所有方法都定义在Array.prototype对象中。不过,这里我们不会使用这些定义的方法;相反,我们将从最简单的方法开始,定义我们自己的版本,并在此基础上逐步构建,直到实现所有方法。

学习的最佳方法莫过于拆解重组。请注意,在实现过程中,我们不会重写现有方法,因为这绝非明智之举(我们导入的某些包可能依赖于这些方法)。此外,这样做还能让我们比较自身版本与原始方法的性能差异。

所以,与其这样写:

Array.prototype.map = function map() {
 // implementation
};
Enter fullscreen mode Exit fullscreen mode

我们将这样做:

function map(array) {
 // takes an array as the first argument
 // implementation
}
Enter fullscreen mode Exit fullscreen mode

我们还可以使用class关键字并扩展Array构造函数来实现我们的方法,如下所示:

class OwnArray extends Array {
 public constructor(...args) {
   super(...args);
 }

 public map() {
   // implementation
   return this;
 }
}
Enter fullscreen mode Exit fullscreen mode

唯一的区别在于array,我们将使用关键字而不是this参数。

但是,我觉得这样做会造成不必要的混乱,所以我们还是坚持第一种方法。

既然如此,让我们从最简单的方法开始吧forEach

LogRocket 免费试用横幅

遍历集合

.forEach

Array.prototype.forEach方法接受一个回调函数,并对数组中的每个元素执行该回调函数,而不会以任何方式改变数组。

[1, 2, 3, 4, 5].forEach(value => console.log(value));
Enter fullscreen mode Exit fullscreen mode

执行

function forEach(array, callback) {
 const { length } = array;

 for (let index = 0; index < length; index += 1) {
   const value = array[index];
   callback(value, index, array);
 }
}
Enter fullscreen mode Exit fullscreen mode

我们遍历数组并对每个元素执行回调函数。这里需要注意的是,该方法不返回任何值——因此,从某种意义上说,它返回了空值undefined

方法链

使用数组方法的一大优势在于可以将多个操作串联起来。请看以下代码:

function getTodosWithCategory(todos, category) {
 return todos
   .filter(todo => todo.category === category)
   .map(todo => normalizeTodo(todo));
}
Enter fullscreen mode Exit fullscreen mode

这样,我们就无需将结果保存map到变量中,从而通常会得到更美观的代码。

很遗憾,forEach它没有返回输入数组!这意味着我们无法执行以下操作:

// Won't work!
function getTodosWithCategory(todos, category) {
 return todos
   .filter(todo => todo.category === category)
   .forEach((value) => console.log(value))
   .map(todo => normalizeTodo(todo));
}
Enter fullscreen mode Exit fullscreen mode

console.log当然,这里是没用的

日志记录实用程序函数

我编写了一个简单的实用函数,可以更好地解释每个方法的作用:它接受什么输入,它返回什么,以及它是否会改变数组。

function logOperation(operationName, array, callback) {
 const input = [...array];
 const result = callback(array);

 console.log({
   operation: operationName,
   arrayBefore: input,
   arrayAfter: array,
   mutates: mutatesArray(input, array), // shallow check
   result,
 });
}
Enter fullscreen mode Exit fullscreen mode

以下是我们的实现中运行的实用函数forEach

logOperation('forEach', [1, 2, 3, 4, 5], array => forEach(array, value => console.log(value)));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'forEach',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: undefined
}
Enter fullscreen mode Exit fullscreen mode

由于我们将方法实现为函数,因此必须使用以下语法:forEach(array, ...)而不是array.forEach(...)

注意:我还为每个方法创建了测试用例,以确保它们按预期工作——您可以在存储库中找到它们。

。地图

最常用的方法之一是Array.prototype.map……它允许我们通过将现有值转换为新值来创建一个新数组。

[1, 2, 3].map(number => number * 5);
// -> [5, 10, 15]
Enter fullscreen mode Exit fullscreen mode

执行

function map(array, callback) {
 const result = [];
 const { length } = array;

 for (let index = 0; index < length; index += 1) {
   const value = array[index];

   result[index] = callback(value, index, array);
 }

 return result;
}
Enter fullscreen mode Exit fullscreen mode

提供给该方法的回调函数接受旧值作为参数,并返回一个新值,然后将该新值保存在新数组(此处称为)的相同索引下result

这里需要特别注意的是,我们返回的是一个新数组,而不是修改旧数组。这一点非常重要,因为这里数组和对象都是以引用的形式传递的。如果你对引用和值之间的区别感到困惑,可以阅读这篇文章

logOperation('map', [1, 2, 3, 4, 5], array => map(array, value => value + 5));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'map',
  input: [ 1, 2, 3, 4, 5 ],
  output: [ 6, 7, 8, 9, 10 ],
  mutates: false
}
Enter fullscreen mode Exit fullscreen mode

。筛选

另一个非常有用的方法是`filter` Array.prototype.filter。顾名思义,它会筛选出回调函数返回值为`false`的值false。每个值都会保存到一个新数组中,稍后返回该数组。

[1, 2, 3, 4, 5].filter(number => number >= 3);
// -> [3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

执行

function filter(array, callback) {
 const result = [];

 const { length } = array;

 for (let index = 0; index < length; index += 1) {
   const value = array[index];

   if (callback(value, index, array)) {
     push(result, value);
   }
 }

 return result;
}
Enter fullscreen mode Exit fullscreen mode

我们获取每个值,并检查提供的回调是否已返回truefalse然后根据情况将该值添加到新创建的数组中或将其丢弃。

请注意,这里我们使用数组push上的方法,result而不是将值保存到它在输入数组中的原始索引位置。这样就result不会因为丢弃的值而出现空位。

logOperation('filter', [1, 2, 3, 4, 5], array => filter(array, value => value >= 2));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'filter',
  input: [ 1, 2, 3, 4, 5 ],
  output: [ 2, 3, 4, 5 ],
  mutates: false
}
Enter fullscreen mode Exit fullscreen mode

。减少

诚然,这种reduce方法比较复杂。然而,它的用途极其广泛,因此,深入理解它的工作原理至关重要。它接收一个数组,并输出一个单一的值。从某种意义上说,它将数组简化为该特定值。

该值的具体计算方式需要在回调函数中指定。我们来看一个例子——最简单的用法reduce,即对一个数字数组求和:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce((sum, number) => {
   return sum + number;
 }, 0) // -> 55
Enter fullscreen mode Exit fullscreen mode

请注意,这里的回调函数接受两个参数:sumnumber。第一个参数始终是上一次迭代返回的结果,第二个参数是我们当前在循环中考虑的数组元素。

因此,当我们遍历数组时,sum它将包含循环当前索引之前所有数字的总和,因为每次迭代我们都将数组的当前值加到它上面。

执行

function reduce(array, callback, initValue) {
 const { length } = array;

 let acc = initValue;
 let startAtIndex = 0;

 if (initValue === undefined) {
   acc = array[0];
   startAtIndex = 1;
 }

 for (let index = startAtIndex; index < length; index += 1) {
   const value = array[index];
   acc = callback(acc, value, index, array);
 }

 return acc;
}
Enter fullscreen mode Exit fullscreen mode

我们创建两个变量,accstartAtIndex,并用它们的默认值初始化它们,分别是参数initValue0

然后,我们检查是否initValue未定义。如果未定义,则必须将数组的第一个值设置为初始值,并且为了避免重复计算初始元素,将设置startAtIndex1

每次迭代,该reduce方法都会将回调的结果保存到累加器(acc)中,以便在下一次迭代中使用。对于第一次迭代,累加器被设置为initValuearray[0]

logOperation('reduce', [1, 2, 3, 4, 5], array => reduce(array, (sum, number) => sum + number, 0));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'reduce',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: 15
}
Enter fullscreen mode Exit fullscreen mode

搜索

对数组而言,还有什么比查找特定值更常见的操作呢?以下几种方法可以帮助我们实现这一点。

.findIndex

顾名思义,它findIndex可以帮助我们找到给定值在数组中的索引。

[1, 2, 3, 4, 5, 6, 7].findIndex(value => value === 5); // 4
Enter fullscreen mode Exit fullscreen mode

该方法会对数组中的每个元素执行提供的回调函数,直到回调函数返回结果为止true。然后,该方法返回当前索引。如果未找到值,-1则返回 null。

执行

function findIndex(array, callback) {
 const { length } = array;

 for (let index = 0; index < length; index += 1) {
   const value = array[index];

   if (callback(value, index, array)) {
     return index;
   }
 }

 return -1;
}
Enter fullscreen mode Exit fullscreen mode
logOperation('findIndex', [1, 2, 3, 4, 5], array => findIndex(array, number => number === 3));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'findIndex',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: 2
}
Enter fullscreen mode Exit fullscreen mode

。寻找

find唯一的区别在于findIndex它返回的是实际值而不是索引。在我们的实现中,我们可以重用已经实现的findIndex

[1, 2, 3, 4, 5, 6, 7].findIndex(value => value === 5); // 5
Enter fullscreen mode Exit fullscreen mode

执行

function find(array, callback) {
 const index = findIndex(array, callback);

 if (index === -1) {
   return undefined;
 }

 return array[index];
}
Enter fullscreen mode Exit fullscreen mode
logOperation('find', [1, 2, 3, 4, 5], array => find(array, number => number === 3));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'find',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: 3
}
Enter fullscreen mode Exit fullscreen mode

.indexOf

indexOf这是获取给定值索引的另一种方法。不过,这次我们将实际值作为参数传递,而不是传递函数。同样,为了简化实现,我们可以使用之前实现的findIndex

[3, 2, 3].indexOf(3); // -> 0
Enter fullscreen mode Exit fullscreen mode

执行

function indexOf(array, searchedValue) {
 return findIndex(array, value => value === searchedValue);
}
Enter fullscreen mode Exit fullscreen mode

findIndex我们会根据要查找的值,向其提供适当的回调函数。

logOperation('indexOf', [1, 2, 3, 4, 5], array => indexOf(array, 3));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'indexOf',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: 2
}
Enter fullscreen mode Exit fullscreen mode

.lastIndexOf

lastIndexOf其工作方式与 `get()` 相同indexOf,只是它从数组末尾开始。此外,我们(像 `get()` 一样indexOf)将要查找的值作为参数传递,而不是通过回调函数传递。

[3, 2, 3].lastIndexOf(3); // -> 2
Enter fullscreen mode Exit fullscreen mode

执行

function lastIndexOf(array, searchedValue) {
 for (let index = array.length - 1; index > -1; index -= 1) {
   const value = array[index];

   if (value === searchedValue) {
     return index;
   }
 }

 return -1;
}
Enter fullscreen mode Exit fullscreen mode

我们对 执行与 相同的操作findIndex,但不执行回调,而是比较valuesearchedValue。如果比较结果为true,则返回索引;如果未找到该值,则返回-1

logOperation('lastIndexOf', [1, 2, 3, 4, 5, 3], array => lastIndexOf(array, 3));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'lastIndexOf',
  arrayBefore: [ 1, 2, 3, 4, 5, 3 ],
  arrayAfter: [ 1, 2, 3, 4, 5, 3 ],
  mutates: false,
  result: 5
}
Enter fullscreen mode Exit fullscreen mode

。每一个

every当我们想要检查数组中的所有元素是否满足给定条件时,这种方法就非常有用。

[1, 2, 3].every(value => Number.isInteger(value)); // -> true
Enter fullscreen mode Exit fullscreen mode

你可以把这个方法看作是逻辑ANDevery的数组等价物

执行

function every(array, callback) {
 const { length } = array;

 for (let index = 0; index < length; index += 1) {
   const value = array[index];

   if (!callback(value, index, array)) {
     return false;
   }
 }

 return true;
}
Enter fullscreen mode Exit fullscreen mode

我们对每个值执行回调函数。如果false在任何时候返回了 `true`,则退出循环,整个方法返回 `false` false。如果循环在未触发 ` iftrue` 语句的情况下终止(所有元素都返回 `false` true),则该方法返回 ` false` true

logOperation('every', [1, 2, 3, 4, 5], array => every(array, number => Number.isInteger(number)));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'every',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: true
}
Enter fullscreen mode Exit fullscreen mode

。一些

现在来看完全相反的情况everysome即使回调函数只执行一次并返回 true true,该函数也会返回 true true。与该every方法类似,你可以将该some方法视为逻辑OR的数组等价物。

[1, 2, 3, 4, 5].some(number => number === 5); // -> true
Enter fullscreen mode Exit fullscreen mode

执行

function some(array, callback) {
 const { length } = array;

 for (let index = 0; index < length; index += 1) {
   const value = array[index];

   if (callback(value, index, array)) {
     return true;
   }
 }

 return false;
}
Enter fullscreen mode Exit fullscreen mode

我们对每个值执行回调函数。如果true在任何时候返回了 `true`,则退出循环,整个方法返回 `false` true。如果循环在未触发 ` iftrue` 语句的情况下终止(所有元素都返回 `false` false),则该方法返回 ` false` false

logOperation('some', [1, 2, 3, 4, 5], array => some(array, number => number === 5));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'some',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: true
}
Enter fullscreen mode Exit fullscreen mode

包括

includes方法的工作方式与该some方法类似,但我们不是提供回调函数,而是提供一个要与元素进行比较的值作为参数。

[1, 2, 3].includes(3); // -> true
Enter fullscreen mode Exit fullscreen mode

执行

function includes(array, searchedValue) {
 return some(array, value => value === searchedValue);
}
Enter fullscreen mode Exit fullscreen mode
logOperation('includes', [1, 2, 3, 4, 5], array => includes(array, 5));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'includes',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: true
}
Enter fullscreen mode Exit fullscreen mode

扁平化

有时数组会嵌套两到三层,我们希望将其扁平化,也就是减少嵌套层级。例如,假设我们想把所有值都移到顶层。为了帮助我们实现这一点,语言新增了两个方法:`flatMap`flatflatMap`flatMap`。

。平坦的

flat方法通过从嵌套数组中提取值来减少嵌套深度。

[1, 2, 3, [4, 5, [6, 7, [8]]]].flat(1); // -> [1, 2, 3, 4, 5, [6, 7, [8]]]
Enter fullscreen mode Exit fullscreen mode

由于我们提供的参数级别为1,因此只有第一级数组会被扁平化;其余级别保持不变。

[1, 2, 3, [4, 5]].flat(1) // -> [1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

执行

function flat(array, depth = 0) {
 if (depth < 1 || !Array.isArray(array)) {
   return array;
 }

 return reduce(
   array,
   (result, current) => {
     return concat(result, flat(current, depth - 1));
   },
   [],
 );
}
Enter fullscreen mode Exit fullscreen mode

首先,我们检查深度参数是否小于某个阈值1。如果小于该阈值,则表示无需展平,我们应该直接返回数组。

其次,我们检查array参数是否真的是该类型Array,因为如果不是,那么扁平化的概念就没有意义,所以我们直接返回该参数。

我们使用reduce之前实现过的函数。我们从一个空数组开始,然后取出数组中的每个值array并将其扁平化。

注意,我们每次调用flat函数(depth - 1)时都会递减depth参数,以避免无限循环。扁平化完成后,我们将返回值添加到result数组中。

注意:concat此处使用该函数将两个数组合并在一起。该函数的实现方式如下所述。

logOperation('flat', [1, 2, 3, [4, 5, [6]]], array => flat(array, 2));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'flat',
  arrayBefore: [ 1, 2, 3, [ 4, 5, [Array] ] ],
  arrayAfter: [ 1, 2, 3, [ 4, 5, [Array] ] ],
  mutates: false,
  result: [ 1, 2, 3, 4, 5, 6 ]
}
Enter fullscreen mode Exit fullscreen mode

.flatMap

flatMap顾名思义,它是 `map`flat和 `flatMap`的组合map。首先,我们根据回调函数进行映射,然后再将结果展平。

map上述方法中,对于每个值,我们都只返回一个值。这样,一个包含三个元素的数组在映射后仍然包含三个元素。使用 ` flatMapmap` 方法,我们可以在提供的回调函数中返回一个数组,该数组随后会被扁平化。

[1, 2, 3].flatMap(value => [value, value, value]); // [1, 1, 1, 2, 2, 2, 3, 3, 3]
Enter fullscreen mode Exit fullscreen mode

返回的每个数组都会被扁平化,这样我们就不再得到一个嵌套了三个数组的数组,而是得到一个包含九个元素的数组。

执行

function flatMap(array, callback) {
 return flat(map(array, callback), 1);
}
Enter fullscreen mode Exit fullscreen mode

根据上面的解释,我们首先使用map,然后将得到的数组的数组展平一层。

logOperation('flatMap', [1, 2, 3], array => flatMap(array, number => [number, number]));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'flatMap',
  arrayBefore: [ 1, 2, 3 ],
  arrayAfter: [ 1, 2, 3 ],
  mutates: false,
  result: [ 1, 1, 2, 2, 3, 3 ]
}
Enter fullscreen mode Exit fullscreen mode

数组的连接、追加和反转

.concat

正如你刚才看到的,这个concat方法对于合并两个或多个数组非常有用。它之所以被广泛使用,是因为它不会改变原有数组;相反,它会返回一个新数组,所有提供的数组都会合并到这个新数组中。

[1, 2, 3].concat([4, 5], 6, [7, 8]) // -> [1, 2, 3, 4, 5, 6, 7, 8]
Enter fullscreen mode Exit fullscreen mode

执行

function concat(array, ...values) {
 const result = [...array];
 const { length } = values;

 for (let index = 0; index < length; index += 1) {
   const value = values[index];

   if (Array.isArray(value)) {
     push(result, ...value);
   } else {
     push(result, value);
   }
 }

 return result;
}
Enter fullscreen mode Exit fullscreen mode

concat它接受一个数组作为第一个参数,接受数量不定的值作为第二个参数,这些值可以是数组(但也可以是任何其他值,例如原始值)。

首先,我们result通过复制提供的数组来创建新数组(使用扩展运算符,将提供的数组的值展开到一个新数组中)。然后,当我们遍历剩余的值时,我们会检查每个值是否为数组。如果是,则使用相应的push函数将其值添加到新result数组中。

如果我们这样做push(result, value),只会将数组作为一个元素添加到主数组中。但是,通过使用扩展运算符push(result, ...value),我们可以将主数组中的所有值都添加到result主数组中。从某种意义上说,我们把数组扁平化了一层!

否则,如果当前值不是数组,我们也将其添加到result数组中——当然,这次不会使用扩展运算符。

logOperation('concat', [1, 2, 3, 4, 5], array => concat(array, 1, 2, [3, 4]));
Enter fullscreen mode Exit fullscreen mode
{
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: [
    1, 2, 3, 4, 5,
    1, 2, 3, 4
  ]
}
Enter fullscreen mode Exit fullscreen mode

。加入

join方法将数组转换为字符串,并用选定的字符串分隔各个值。

['Brian', 'Matt', 'Kate'].join(', ') // -> Brian, Matt, Kate
Enter fullscreen mode Exit fullscreen mode

执行

function join(array, joinWith) {
 return reduce(
   array,
   (result, current, index) => {
     if (index === 0) {
       return current;
     }

     return `${result}${joinWith}${current}`;
   },
   '',
 );
}
Enter fullscreen mode Exit fullscreen mode

我们使用该reduce函数:将提供的数组传递给它,并将初始值设置为空字符串。到目前为止都很简单明了。

回调函数reduce是魔法发生的地方:reduce 遍历提供的数组,并将结果字符串拼接起来,joinWith在数组的值之间放置所需的分隔符(作为 传递)。

array[0]值需要一些特殊处理,因为此时它仍然是未定义的(它是一个空字符串),而且我们也不希望第一个元素前面result有分隔符( )。joinWith

logOperation('join', [1, 2, 3, 4, 5], array => join(array, ', '));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'join',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: '1, 2, 3, 4, 5'
}
Enter fullscreen mode Exit fullscreen mode

。撤销

reverse方法会反转数组中值的顺序。

[1, 2, 3].reverse(); // -> [3, 2, 1]
Enter fullscreen mode Exit fullscreen mode

执行

function reverse(array) {
 const result = [];

 const lastIndex = array.length - 1;

 for (let index = lastIndex; index > -1; index -= 1) {
   const value = array[index];
   result[lastIndex - index] = value;
 }

 return result;
}
Enter fullscreen mode Exit fullscreen mode

思路很简单:首先,我们定义一个空数组,并将作为参数传入的数组的最后一个索引保存下来。然后,我们反向遍历传入的数组,将每个值保存到数组的指定(lastIndex - index)位置result,最后返回该数组。

logOperation('reverse', [1, 2, 3, 4, 5], array => reverse(array));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'reverse',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: [ 5, 4, 3, 2, 1 ]
}
Enter fullscreen mode Exit fullscreen mode

添加、删除和追加值

。转移

shift方法将数组中的值向下移动一个索引,从而删除第一个值,然后返回该值。

[1, 2, 3].shift(); // -> 1
Enter fullscreen mode Exit fullscreen mode

执行

function shift(array) {
 const { length } = array;
 const firstValue = array[0];

 for (let index = 1; index < length; index += 1) {
   const value = array[index];
   array[index - 1] = value;
 }

 array.length = length - 1;

 return firstValue;
}
Enter fullscreen mode Exit fullscreen mode

首先,我们保存给定数组的原始长度和初始值(也就是将所有元素向下移动一位后将被丢弃的值)。然后,我们遍历数组,并将每个元素的索引向下移动一位。完成后,我们更新数组的长度并返回初始值。

logOperation('shift', [1, 2, 3, 4, 5], array => shift(array));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'shift',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 2, 3, 4, 5 ],
  mutates: true,
  result: 1
}
Enter fullscreen mode Exit fullscreen mode

取消切换

unshift方法向数组开头添加一个或多个值,并返回该数组的长度。

[2, 3, 4].unshift(1); // -> [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

执行

function unshift(array, ...values) {
 const mergedArrays = concat(values, ...array);
 const { length: mergedArraysLength } = mergedArrays;

 for (let index = 0; index < mergedArraysLength; index += 1) {
   const value = mergedArrays[index];
   array[index] = value;
 }

 return array.length;
}
Enter fullscreen mode Exit fullscreen mode

首先,我们将values(作为参数传递的各个值)和array(我们要取消移位的数组)连接起来。需要注意的是values,它们要放在前面;它们必须位于原始数组的前面。

然后我们保存这个新数组的长度,并遍历它,将它的值保存到原始数组中,并覆盖最初存在的值。

logOperation('unshift', [1, 2, 3, 4, 5], array => unshift(array, 0));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'unshift',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 0, 1, 2, 3, 4, 5 ],
  mutates: true,
  result: 6
}
Enter fullscreen mode Exit fullscreen mode

。片

从数组中取出单个值很简单:只需使用索引即可。但有时,我们可能需要一次性取出数组中更大的一部分——比如三四个元素。这时,` sliceget_ ...

我们指定起始索引和结束索引,并将slice结果数组从原始数组中按这些索引位置截取。但请注意,结束索引参数并非包含所有元素;在以下示例中,只有索引为 0、1342的元素5才会出现在结果数组中。

[1, 2, 3, 4, 5, 6, 7].slice(3, 6); // -> [4, 5, 6]
Enter fullscreen mode Exit fullscreen mode

执行

function slice(array, startIndex = 0, endIndex = array.length) {
 const result = [];

 for (let index = startIndex; index < endIndex; index += 1) {
   const value = array[index];

   if (index < array.length) {
     push(result, value);
   }
 }

 return result;
}
Enter fullscreen mode Exit fullscreen mode

我们遍历数组,从startIndex`a` 到endIndex`b`,并将每个值添加到 `c` 中result。这里我们也使用了默认参数,这样slice当没有传递任何参数时,该方法只需创建一个数组的副本。我们通过将 `a` 默认设置startIndex为`b` 0,并将`c` 设置endIndex为数组的长度来实现这一点。

注意:该if语句确保push仅当给定索引下的值存在于原始数组中时才执行操作。

logOperation('slice', [1, 2, 3, 4, 5], array => slice(array, 1, 3));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'slice',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4, 5 ],
  mutates: false,
  result: [ 2, 3 ]
}
Enter fullscreen mode Exit fullscreen mode

。拼接

splice方法同时从数组中移除指定数量的值,并在其位置插入其他值。虽然乍看之下并不明显,但我们也可以添加比移除更多的值,反之亦然。

首先,我们指定起始索引,然后指定要删除的值的数量,其余参数是要插入的值。

const arr = [1, 2, 3, 4, 5];

arr.splice(0, 2, 3, 4, 5);

arr // -> [3, 4, 5, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

执行

function splice(array, insertAtIndex, removeNumberOfElements, ...values) {
 const firstPart = slice(array, 0, insertAtIndex);
 const secondPart = slice(array, insertAtIndex + removeNumberOfElements);

 const removedElements = slice(array, insertAtIndex, insertAtIndex + removeNumberOfElements);

 const joinedParts = firstPart.concat(values, secondPart);
 const { length: joinedPartsLength } = joinedParts;

 for (let index = 0; index < joinedPartsLength; index += 1) {
   array[index] = joinedParts[index];
 }

 return removedElements;
}
Enter fullscreen mode Exit fullscreen mode

思路是在 x = insertAtIndex0 和x = 1 处进行两次切割insertAtIndex + removeNumberOfElements。这样,我们就把slice原始数组分成了三部分。第一部分(x = 1 firstPart)和第三部分(这里称为 x = 2 secondPart)将被保留到最终的数组中。

我们将把作为参数传递的值插入到这两个值之间。我们使用该concat方法来实现这一点。中间剩余的部分是removedElements,我们最后会将其返回。

logOperation('splice', [1, 2, 3, 4, 5], array => splice(array, 1, 3));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'splice',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 5 ],
  mutates: true,
  result: [ 2, 3, 4 ]
}
Enter fullscreen mode Exit fullscreen mode

。流行音乐

pop方法移除数组的最后一个值并返回它。

[1, 2, 3].pop(); // -> 3
Enter fullscreen mode Exit fullscreen mode

执行

function pop(array) {
 const value = array[array.length - 1];

 array.length = array.length - 1;

 return value;
}
Enter fullscreen mode Exit fullscreen mode

首先,我们将数组的最后一个值保存到一个变量中。然后,我们只需将数组的长度减一,从而删除最后一个值。

logOperation('pop', [1, 2, 3, 4, 5], array => pop(array));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'pop',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [ 1, 2, 3, 4 ],
  mutates: true,
  result: 5
}
Enter fullscreen mode Exit fullscreen mode

。推

push方法允许我们在数组末尾添加值。

[1, 2, 3, 4].push(5); // -> [1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

执行

export function push(array, ...values) {
 const { length: arrayLength } = array;
 const { length: valuesLength } = values;

 for (let index = 0; index < valuesLength; index += 1) {
   array[arrayLength + index] = values[index];
 }

 return array.length;
}
Enter fullscreen mode Exit fullscreen mode

首先,我们将原始数组的长度和要追加的值的数量分别保存到相应的变量中。然后,我们遍历提供的值,并将它们添加到原始数组中。

循环从数组的初始位置开始index = 0,因此每次迭代都会增加index数组的长度。这样,我们不会覆盖原始数组中的任何值,而是将它们添加到数组的末尾。

logOperation('push', [1, 2, 3, 4, 5], array => push(array, 6, 7));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'push',
  arrayBefore: [ 1, 2, 3, 4, 5 ],
  arrayAfter: [
    1, 2, 3, 4,
    5, 6, 7
  ],
  mutates: true,
  result: 7
}
Enter fullscreen mode Exit fullscreen mode

。充满

当我们想用占位符值填充一个空数组时,这种fill方法非常有用。如果我们想创建一个包含指定数量null元素的数组,可以这样做:

[...Array(5)].fill(null) // -> [null, null, null, null, null]
Enter fullscreen mode Exit fullscreen mode

执行

function fill(array, value, startIndex = 0, endIndex = array.length) {
 for (let index = startIndex; index < endIndex; index += 1) {
   array[index] = value;
 }

 return array;
}
Enter fullscreen mode Exit fullscreen mode

fill方法实际上只是替换数组中指定索引范围内的值。如果没有指定范围,则该方法会替换数组中的所有值。

logOperation('fill', [...new Array(5)], array => fill(array, 0));
Enter fullscreen mode Exit fullscreen mode
{
  operation: 'fill',
  arrayBefore: [ undefined, undefined, undefined, undefined, undefined ],
  arrayAfter: [ 0, 0, 0, 0, 0 ],
  mutates: true,
  result: [ 0, 0, 0, 0, 0 ]
}
Enter fullscreen mode Exit fullscreen mode

带发电机

最后三个方法比较特殊,因为它们返回的是生成器。如果您不熟悉生成器,可以跳过它们,因为您可能近期内不会用到它们。

.values

values方法返回一个生成器,该生成器会生成数组中的值。

const valuesGenerator = values([1, 2, 3, 4, 5]);

valuesGenerator.next(); // { value: 1, done: false }
Enter fullscreen mode Exit fullscreen mode

执行

function values(array) {
 const { length } = array;

 function* createGenerator() {
   for (let index = 0; index < length; index += 1) {
     const value = array[index];
     yield value;
   }
 }

 return createGenerator();
}
Enter fullscreen mode Exit fullscreen mode

首先,我们定义这个createGenerator函数。在这个函数中,我们遍历数组并返回每个值。

.keys

keys方法返回一个生成器,该生成器生成数组的索引。

const keysGenerator = keys([1, 2, 3, 4, 5]);

keysGenerator.next(); // { value: 0, done: false }
Enter fullscreen mode Exit fullscreen mode

执行

function keys(array) {
 function* createGenerator() {
   const { length } = array;

   for (let index = 0; index < length; index += 1) {
     yield index;
   }
 }

 return createGenerator();
}
Enter fullscreen mode Exit fullscreen mode

实现方式完全相同,但这次我们返回的是索引,而不是值。

条目

entries方法返回一个生成器,该生成器生成索引-值对。

const entriesGenerator = entries([1, 2, 3, 4, 5]);

entriesGenerator.next(); // { value: [0, 1], done: false }
Enter fullscreen mode Exit fullscreen mode

执行

function entries(array) {
 const { length } = array;

 function* createGenerator() {
   for (let index = 0; index < length; index += 1) {
     const value = array[index];
     yield [index, value];
   }
 }

 return createGenerator();
}
Enter fullscreen mode Exit fullscreen mode

同样,实现方式相同,但现在我们将索引和值结合起来,并将它们放在一个数组中。

概括

高效地使用数组的方法是成为优秀开发者的基础。而深入了解数组内部运作机制的复杂性,是我所知的提升数组使用效率的最佳途径。

注:我没有在这里介绍 ` sortand` 和 `or`,toLocaleString因为它们的实现过于复杂,而且在我看来,对于初学者来说过于繁琐。我也没有讨论 ` copyWithin,因为它从未被使用过——它完全没用。


编者按:发现本文有误?您可以在这里找到正确版本。

插件:LogRocket,一款用于 Web 应用的 DVR

 
LogRocket 控制面板免费试用横幅
 
LogRocket是一款前端日志工具,可让您重现问题,如同在您自己的浏览器中发生一样。无需猜测错误原因,也无需用户提供屏幕截图和日志转储,LogRocket 即可让您重现会话,快速了解问题所在。它与任何框架的应用程序完美兼容,并提供插件来记录来自 Redux、Vuex 和 @ngrx/store 的额外上下文信息。
 
除了记录 Redux 操作和状态之外,LogRocket 还会记录控制台日志、JavaScript 错误、堆栈跟踪、包含标头和正文的网络请求/响应、浏览器元数据以及自定义日志。它还会对 DOM 进行插桩,记录页面上的 HTML 和 CSS,即使是最复杂的单页应用程序,也能生成像素级精确的视频。
 
免费试用


这篇文章《通过实现数组方法来理解数组方法——所有方法》最初发表在LogRocket 博客上。

文章来源:https://dev.to/bnevilleoneill/understand-array-methods-by-implementing-them-all-of-them-iha