2023 年 12 月 31 日

数组方法

数组提供了很多方法。为了方便起见,在本章中,它们被分成了几组。

添加/移除项

我们已经知道了从开头或结尾添加和移除项的方法

  • arr.push(...items) – 添加项到结尾,
  • arr.pop() – 从结尾提取一个项,
  • arr.shift() – 从开头提取一个项,
  • arr.unshift(...items) – 添加项到开头。

以下是其他一些方法。

splice

如何从数组中删除一个元素?

数组是对象,所以我们可以尝试使用 delete

let arr = ["I", "go", "home"];

delete arr[1]; // remove "go"

alert( arr[1] ); // undefined

// now arr = ["I",  , "home"];
alert( arr.length ); // 3

元素被删除了,但数组仍然有 3 个元素,我们可以看到 arr.length == 3

这是自然的,因为 delete obj.key 通过 key 删除一个值。它就是这么做的。对于对象来说很好。但对于数组,我们通常希望其余元素移动并占据释放的空间。我们希望现在有一个更短的数组。

所以,应该使用特殊方法。

arr.splice 方法是数组的瑞士军刀。它可以做所有事情:插入、删除和替换元素。

语法是

arr.splice(start[, deleteCount, elem1, ..., elemN])

它从索引 start 开始修改 arr:删除 deleteCount 个元素,然后在其位置插入 elem1, ..., elemN。返回已删除元素的数组。

通过示例可以轻松理解此方法。

让我们从删除开始

let arr = ["I", "study", "JavaScript"];

arr.splice(1, 1); // from index 1 remove 1 element

alert( arr ); // ["I", "JavaScript"]

简单,对吧?从索引 1 开始,它删除了 1 个元素。

在下一个示例中,我们删除 3 个元素,并用另外两个元素替换它们

let arr = ["I", "study", "JavaScript", "right", "now"];

// remove 3 first elements and replace them with another
arr.splice(0, 3, "Let's", "dance");

alert( arr ) // now ["Let's", "dance", "right", "now"]

在这里,我们可以看到 splice 返回已删除元素的数组

let arr = ["I", "study", "JavaScript", "right", "now"];

// remove 2 first elements
let removed = arr.splice(0, 2);

alert( removed ); // "I", "study" <-- array of removed elements

splice 方法还可以插入元素,而无需任何删除。为此,我们需要将 deleteCount 设置为 0

let arr = ["I", "study", "JavaScript"];

// from index 2
// delete 0
// then insert "complex" and "language"
arr.splice(2, 0, "complex", "language");

alert( arr ); // "I", "study", "complex", "language", "JavaScript"
允许负索引

在此和其他数组方法中,允许使用负索引。它们指定从数组末尾开始的位置,如下所示

let arr = [1, 2, 5];

// from index -1 (one step from the end)
// delete 0 elements,
// then insert 3 and 4
arr.splice(-1, 0, 3, 4);

alert( arr ); // 1,2,3,4,5

slice

方法 arr.slice 比类似的 arr.splice 简单得多。

语法是

arr.slice([start], [end])

它返回一个新数组,将所有项目从索引 start 复制到 end(不包括 end)。startend 都可以为负数,在这种情况下,将假定从数组末尾开始的位置。

它类似于字符串方法 str.slice,但它不是子字符串,而是子数组。

例如

let arr = ["t", "e", "s", "t"];

alert( arr.slice(1, 3) ); // e,s (copy from 1 to 3)

alert( arr.slice(-2) ); // s,t (copy from -2 till the end)

我们也可以不带参数调用它:arr.slice() 创建 arr 的副本。这通常用于获取副本,以便进行不会影响原始数组的进一步转换。

concat

方法 arr.concat 创建一个新数组,其中包含来自其他数组和附加项的值。

语法是

arr.concat(arg1, arg2...)

它接受任意数量的参数——数组或值。

结果是一个新数组,其中包含来自 arrarg1arg2 等的项。

如果参数 argN 是一个数组,那么它的所有元素都将被复制。否则,将复制参数本身。

例如

let arr = [1, 2];

// create an array from: arr and [3,4]
alert( arr.concat([3, 4]) ); // 1,2,3,4

// create an array from: arr and [3,4] and [5,6]
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6

// create an array from: arr and [3,4], then add values 5 and 6
alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6

通常,它只复制数组中的元素。其他对象,即使它们看起来像数组,也会作为一个整体添加

let arr = [1, 2];

let arrayLike = {
  0: "something",
  length: 1
};

alert( arr.concat(arrayLike) ); // 1,2,[object Object]

…但如果类数组对象具有特殊的 Symbol.isConcatSpreadable 属性,那么它将被 concat 视为数组:它的元素将被添加

let arr = [1, 2];

let arrayLike = {
  0: "something",
  1: "else",
  [Symbol.isConcatSpreadable]: true,
  length: 2
};

alert( arr.concat(arrayLike) ); // 1,2,something,else

迭代:forEach

方法 arr.forEach 允许为数组的每个元素运行一个函数。

语法

arr.forEach(function(item, index, array) {
  // ... do something with an item
});

例如,这显示数组的每个元素

// for each element call alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);

而这段代码更详细地说明了它们在目标数组中的位置

["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
  alert(`${item} is at index ${index} in ${array}`);
});

函数的结果(如果它返回任何结果)将被丢弃并忽略。

在数组中搜索

现在让我们介绍在数组中搜索的方法。

indexOf/lastIndexOf 和 includes

方法 arr.indexOfarr.includes 具有相似的语法,并且与它们对应的字符串方法执行本质上相同的操作,但它们针对的是项而不是字符

  • arr.indexOf(item, from) – 从索引 from 开始查找 item,并返回找到它的索引,否则返回 -1
  • arr.includes(item, from) – 从索引 from 开始查找 item,如果找到,则返回 true

通常,这些方法仅与一个参数一起使用:要搜索的 item。默认情况下,搜索是从开头开始的。

例如

let arr = [1, 0, false];

alert( arr.indexOf(0) ); // 1
alert( arr.indexOf(false) ); // 2
alert( arr.indexOf(null) ); // -1

alert( arr.includes(1) ); // true

请注意,indexOf 使用严格相等 === 进行比较。因此,如果我们查找 false,它会准确找到 false,而不是零。

如果我们希望检查 item 是否存在于数组中,并且不需要索引,那么首选 arr.includes

方法 arr.lastIndexOfindexOf 相同,但从右向左查找。

let fruits = ['Apple', 'Orange', 'Apple']

alert( fruits.indexOf('Apple') ); // 0 (first Apple)
alert( fruits.lastIndexOf('Apple') ); // 2 (last Apple)
includes 方法正确处理 NaN

includes 的一个次要但值得注意的特性是,它与 indexOf 不同,可以正确处理 NaN

const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1 (wrong, should be 0)
alert( arr.includes(NaN) );// true (correct)

这是因为 includes 稍后才添加到 JavaScript 中,并在内部使用更先进的比较算法。

find 和 findIndex/findLastIndex

想象一下,我们有一个对象数组。我们如何找到具有特定条件的对象?

这里 arr.find(fn) 方法派上用场。

语法是

let result = arr.find(function(item, index, array) {
  // if true is returned, item is returned and iteration is stopped
  // for falsy scenario returns undefined
});

该函数被依次调用以处理数组的元素。

  • item 是元素。
  • index 是其索引。
  • array 是数组本身。

如果它返回 true,则停止搜索,返回 item。如果什么也没找到,则返回 undefined

例如,我们有一个用户数组,每个用户都有字段 idname。让我们找到 id == 1 的用户

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

let user = users.find(item => item.id == 1);

alert(user.name); // John

在现实生活中,对象数组是一件很常见的事情,所以 find 方法非常有用。

请注意,在示例中,我们向 find 提供了一个参数为 item => item.id == 1 的函数。这是典型的,此函数的其他参数很少使用。

方法 arr.findIndex 具有相同的语法,但返回找到元素的索引,而不是元素本身。如果什么也没找到,则返回 -1 的值。

方法 arr.findLastIndexfindIndex 类似,但从右向左搜索,类似于 lastIndexOf

这里有一个示例

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"},
  {id: 4, name: "John"}
];

// Find the index of the first John
alert(users.findIndex(user => user.name == 'John')); // 0

// Find the index of the last John
alert(users.findLastIndex(user => user.name == 'John')); // 3

filter

find 方法查找使函数返回 true 的单个(第一个)元素。

如果可能有很多,我们可以使用 arr.filter(fn)

语法与 find 类似,但 filter 返回一个包含所有匹配元素的数组

let results = arr.filter(function(item, index, array) {
  // if true item is pushed to results and the iteration continues
  // returns empty array if nothing found
});

例如

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

// returns array of the first two users
let someUsers = users.filter(item => item.id < 3);

alert(someUsers.length); // 2

转换数组

让我们继续研究转换和重新排序数组的方法。

map

方法 arr.map 是最有用的方法之一,也是最常用的方法。

它为数组的每个元素调用该函数,并返回结果数组。

语法是

let result = arr.map(function(item, index, array) {
  // returns the new value instead of item
});

例如,我们在此处将每个元素转换为其长度

let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5,7,6

sort(fn)

arr.sort() 的调用对数组进行就地排序,更改其元素顺序。

它还返回已排序的数组,但通常会忽略返回值,因为 arr 本身已修改。

例如

let arr = [ 1, 2, 15 ];

// the method reorders the content of arr
arr.sort();

alert( arr );  // 1, 15, 2

您是否注意到结果中的任何异常情况?

顺序变为 1, 15, 2。不正确。但为什么?

默认情况下,将项目作为字符串进行排序。

从字面上讲,所有元素都转换为字符串以进行比较。对于字符串,应用词典顺序,并且确实 "2" > "15"

要使用我们自己的排序顺序,我们需要将函数作为 arr.sort() 的参数提供。

该函数应比较两个任意值并返回

function compare(a, b) {
  if (a > b) return 1; // if the first value is greater than the second
  if (a == b) return 0; // if values are equal
  if (a < b) return -1; // if the first value is less than the second
}

例如,按数字排序

function compareNumeric(a, b) {
  if (a > b) return 1;
  if (a == b) return 0;
  if (a < b) return -1;
}

let arr = [ 1, 2, 15 ];

arr.sort(compareNumeric);

alert(arr);  // 1, 2, 15

现在按预期工作。

让我们退一步,思考一下正在发生的事情。arr 可以是任何内容的数组,对吧?它可能包含数字、字符串、对象或其他任何内容。我们有一组某些项目。要对其进行排序,我们需要一个排序函数,该函数知道如何比较其元素。默认值为字符串顺序。

arr.sort(fn) 方法实现了一个通用排序算法。我们不必关心其内部工作方式(大多数情况下是经过优化的 快速排序Timsort)。它将遍历数组,使用所提供的函数比较其元素并重新排序它们,我们所需要做的就是提供执行比较的 fn

顺便说一下,如果我们想知道比较了哪些元素,没有什么可以阻止我们发出警报

[1, -2, 15, 2, 0, 8].sort(function(a, b) {
  alert( a + " <> " + b );
  return a - b;
});

该算法可能会在过程中将一个元素与多个其他元素进行比较,但它会尝试进行尽可能少的比较。

比较函数可以返回任何数字

实际上,比较函数只需要返回一个正数来说“更大”,返回一个负数来说“更小”。

这允许编写更短的函数

let arr = [ 1, 2, 15 ];

arr.sort(function(a, b) { return a - b; });

alert(arr);  // 1, 2, 15
箭头函数最棒

记住 箭头函数?我们可以在此处使用它们进行更简洁的排序

arr.sort( (a, b) => a - b );

这与上述较长的版本完全相同。

对字符串使用 localeCompare

还记得 字符串 比较算法吗?它默认按字母的代码比较字母。

对于许多字母表,最好使用 str.localeCompare 方法来正确排序字母,例如 Ö

例如,让我们用德语对几个国家进行排序

let countries = ['Österreich', 'Andorra', 'Vietnam'];

alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (wrong)

alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam (correct!)

反转

方法 arr.reverse 反转 arr 中元素的顺序。

例如

let arr = [1, 2, 3, 4, 5];
arr.reverse();

alert( arr ); // 5,4,3,2,1

它还返回反转后的数组 arr

拆分和连接

以下是现实生活中的情况。我们在编写一个消息传递应用程序,而该人输入了以逗号分隔的收件人列表:John, Pete, Mary。但对我们来说,一个名称数组比一个字符串更方便。如何获取它?

str.split(delim) 方法正是这样做的。它按给定的分隔符 delim 将字符串拆分为一个数组。

在下面的示例中,我们按逗号后跟空格进行拆分

let names = 'Bilbo, Gandalf, Nazgul';

let arr = names.split(', ');

for (let name of arr) {
  alert( `A message to ${name}.` ); // A message to Bilbo  (and other names)
}

split 方法有一个可选的第二个数字参数 - 数组长度限制。如果提供了它,则会忽略额外的元素。但在实践中很少使用它

let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);

alert(arr); // Bilbo, Gandalf
拆分为字母

使用空 s 调用 split(s) 会将字符串拆分为一个字母数组

let str = "test";

alert( str.split('') ); // t,e,s,t

调用 arr.join(glue)split 执行相反的操作。它创建一个由 arr 项组成的字符串,它们之间用 glue 连接。

例如

let arr = ['Bilbo', 'Gandalf', 'Nazgul'];

let str = arr.join(';'); // glue the array into a string using ;

alert( str ); // Bilbo;Gandalf;Nazgul

reduce/reduceRight

当我们需要遍历一个数组时 - 我们可以使用 forEachforfor..of

当我们需要遍历并返回每个元素的数据时 - 我们可以使用 map

方法 arr.reducearr.reduceRight 也属于该类型,但更复杂一些。它们用于基于数组计算单个值。

语法是

let value = arr.reduce(function(accumulator, item, index, array) {
  // ...
}, [initial]);

该函数依次应用于所有数组元素,并将其结果“传递”给下一次调用。

参数

  • accumulator – 是上一个函数调用的结果,第一次等于 initial(如果提供了 initial)。
  • item – 是当前数组项。
  • index – 是它的位置。
  • array – 是数组。

随着函数的应用,上一个函数调用的结果作为第一个参数传递给下一个函数。

因此,第一个参数本质上是存储所有先前执行的组合结果的累加器。最后,它成为 reduce 的结果。

听起来很复杂?

最简单的理解方法是通过示例。

这里我们在一行中获得数组的总和

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

let result = arr.reduce((sum, current) => sum + current, 0);

alert(result); // 15

传递给 reduce 的函数仅使用 2 个参数,这通常就足够了。

让我们看看正在发生的事情的详细信息。

  1. 在第一次运行时,suminitial 值(reduce 的最后一个参数),等于 0current 是第一个数组元素,等于 1。因此,函数结果为 1
  2. 在第二次运行时,sum = 1,我们向其添加第二个数组元素(2)并返回。
  3. 在第 3 次运行时,sum = 3,我们向其添加另一个元素,依此类推...

计算流程

或者以表格的形式,其中每一行表示对下一个数组元素的函数调用

sum current result
第一次调用 0 1 1
第二次调用 1 2 3
第三次调用 3 3 6
第四次调用 6 4 10
第五次调用 10 5 15

在这里我们可以清楚地看到上一次调用的结果如何成为下一次调用的第一个参数。

我们还可以省略初始值

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

// removed initial value from reduce (no 0)
let result = arr.reduce((sum, current) => sum + current);

alert( result ); // 15

结果是一样的。这是因为如果没有初始值,则 reduce 将数组的第一个元素作为初始值,并从第二个元素开始迭代。

计算表与上面相同,减去第一行。

但这种用法需要极其小心。如果数组为空,则没有初始值的 reduce 调用会引发错误。

这里有一个示例

let arr = [];

// Error: Reduce of empty array with no initial value
// if the initial value existed, reduce would return it for the empty arr.
arr.reduce((sum, current) => sum + current);

因此,建议始终指定初始值。

方法 arr.reduceRight 执行相同操作,但从右到左。

Array.isArray

数组不构成单独的语言类型。它们基于对象。

因此,typeof 无法帮助区分普通对象和数组

alert(typeof {}); // object
alert(typeof []); // object (same)

…但数组使用得如此频繁,因此有专门的方法:Array.isArray(value)。如果 value 是数组,则返回 true,否则返回 false

alert(Array.isArray({})); // false

alert(Array.isArray([])); // true

大多数方法支持“thisArg”

几乎所有调用函数的数组方法(如 findfiltermap,但 sort 除外)都接受可选附加参数 thisArg

上面各节中未解释该参数,因为它很少使用。但为了完整性,我们必须介绍它。

以下是这些方法的完整语法

arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg is the optional last argument

thisArg 参数的值变为 functhis

例如,这里我们使用 army 对象的方法作为过滤器,而 thisArg 传递上下文

let army = {
  minAge: 18,
  maxAge: 27,
  canJoin(user) {
    return user.age >= this.minAge && user.age < this.maxAge;
  }
};

let users = [
  {age: 16},
  {age: 20},
  {age: 23},
  {age: 30}
];

// find users, for who army.canJoin returns true
let soldiers = users.filter(army.canJoin, army);

alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23

如果在上面的示例中,我们使用 users.filter(army.canJoin),则 army.canJoin 将作为独立函数调用,其中 this=undefined,从而导致即时错误。

可以将对 users.filter(army.canJoin, army) 的调用替换为 users.filter(user => army.canJoin(user)),它执行相同操作。后者使用得更频繁,因为大多数人更容易理解它。

总结

数组方法备忘单

  • 添加/删除元素

    • push(...items) – 将项目添加到末尾,
    • pop() – 从末尾提取一个项目,
    • shift() – 从开头提取一个项目,
    • unshift(...items) – 将项目添加到开头。
    • splice(pos, deleteCount, ...items) – 在索引 pos 处删除 deleteCount 个元素并插入 items
    • slice(start, end) – 创建一个新数组,将从索引 startend(不包括)的元素复制到其中。
    • concat(...items) – 返回一个新数组:复制当前数组的所有成员并向其中添加 items。如果任何 items 是一个数组,则取其元素。
  • 搜索元素

    • indexOf/lastIndexOf(item, pos) – 从位置 pos 开始查找 item,如果未找到,则返回索引或 -1
    • includes(value) – 如果数组包含 value,则返回 true,否则返回 false
    • find/filter(func) – 通过函数筛选元素,返回使函数返回 true 的第一个/所有值。
    • findIndexfind 类似,但返回索引而不是值。
  • 遍历元素

    • forEach(func) – 对每个元素调用 func,不返回任何内容。
  • 转换数组

    • map(func) – 根据对每个元素调用 func 的结果创建一个新数组。
    • sort(func) – 就地对数组进行排序,然后返回数组。
    • reverse() – 就地反转数组,然后返回数组。
    • split/join – 将字符串转换为数组,反之亦然。
    • reduce/reduceRight(func, initial) – 通过对每个元素调用 func 并传递调用之间的中间结果,计算数组上的单个值。
  • 此外

    • Array.isArray(value) 检查 value 是否为数组,如果是,则返回 true,否则返回 false

请注意,方法 sortreversesplice 会修改数组本身。

这些方法是最常用的方法,它们涵盖了 99% 的用例。但还有其他一些方法

  • arr.some(fn)/arr.every(fn) 检查数组。

    函数 fn 在数组的每个元素上调用,类似于 map。如果任何/所有结果为 true,则返回 true,否则返回 false

    这些方法的行为类似于 ||&& 运算符:如果 fn 返回真值,则 arr.some() 立即返回 true 并停止迭代其余项目;如果 fn 返回假值,则 arr.every() 立即返回 false 并停止迭代其余项目。

    我们可以使用 every 来比较数组

    function arraysEqual(arr1, arr2) {
      return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
    }
    
    alert( arraysEqual([1, 2], [1, 2])); // true
  • arr.fill(value, start, end) – 使用重复的 value 从索引 startend 填充数组。

  • arr.copyWithin(target, start, end) – 将其元素从位置 start 复制到位置 end,到其自身,在位置 target(覆盖现有元素)。

  • arr.flat(depth)/arr.flatMap(fn) 从多维数组创建一个新的扁平数组。

有关完整列表,请参见手册

乍一看,似乎有很多方法,很难记住。但实际上,这要容易得多。

浏览备忘单,以便了解它们。然后解决本章中的任务进行练习,以便获得使用数组方法的经验。

此后,无论何时需要对数组执行某些操作,但不知道如何执行时,请到这里,查看备忘单并找到正确的方法。示例将帮助您正确编写它。很快,您将自动记住这些方法,而无需您付出特别的努力。

任务

重要性:5

编写函数 camelize(str),它将连字符分隔的单词(如“my-short-string”)更改为驼峰式“myShortString”。

即:删除所有连字符,连字符后面的每个单词都变为大写。

示例

camelize("background-color") == 'backgroundColor';
camelize("list-style-image") == 'listStyleImage';
camelize("-webkit-transition") == 'WebkitTransition';

P.S. 提示:使用 split 将字符串拆分为数组,对其进行转换,然后使用 join 重新连接。

使用测试打开沙箱。

function camelize(str) {
  return str
    .split('-') // splits 'my-long-word' into array ['my', 'long', 'word']
    .map(
      // capitalizes first letters of all array items except the first one
      // converts ['my', 'long', 'word'] into ['my', 'Long', 'Word']
      (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
    )
    .join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord'
}

在沙箱中使用测试打开解决方案。

重要性:4

编写一个函数 filterRange(arr, a, b),它获取一个数组 arr,查找值高于或等于 a 且低于或等于 b 的元素,并以数组的形式返回结果。

该函数不应修改数组。它应返回新数组。

例如

let arr = [5, 3, 8, 1];

let filtered = filterRange(arr, 1, 4);

alert( filtered ); // 3,1 (matching values)

alert( arr ); // 5,3,8,1 (not modified)

使用测试打开沙箱。

function filterRange(arr, a, b) {
  // added brackets around the expression for better readability
  return arr.filter(item => (a <= item && item <= b));
}

let arr = [5, 3, 8, 1];

let filtered = filterRange(arr, 1, 4);

alert( filtered ); // 3,1 (matching values)

alert( arr ); // 5,3,8,1 (not modified)

在沙箱中使用测试打开解决方案。

重要性:4

编写一个函数 filterRangeInPlace(arr, a, b),它获取一个数组 arr,并从中删除所有值,除了介于 ab 之间的值。测试为:a ≤ arr[i] ≤ b

该函数应仅修改数组。它不应返回任何内容。

例如

let arr = [5, 3, 8, 1];

filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4

alert( arr ); // [3, 1]

使用测试打开沙箱。

function filterRangeInPlace(arr, a, b) {

  for (let i = 0; i < arr.length; i++) {
    let val = arr[i];

    // remove if outside of the interval
    if (val < a || val > b) {
      arr.splice(i, 1);
      i--;
    }
  }

}

let arr = [5, 3, 8, 1];

filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4

alert( arr ); // [3, 1]

在沙箱中使用测试打开解决方案。

重要性:4
let arr = [5, 2, 1, -10, 8];

// ... your code to sort it in decreasing order

alert( arr ); // 8, 5, 2, 1, -10
let arr = [5, 2, 1, -10, 8];

arr.sort((a, b) => b - a);

alert( arr );
重要性:5

我们有一个字符串数组 arr。我们希望有一个它的已排序副本,但保持 arr 不变。

创建一个函数 copySorted(arr) 来返回这样一个副本。

let arr = ["HTML", "JavaScript", "CSS"];

let sorted = copySorted(arr);

alert( sorted ); // CSS, HTML, JavaScript
alert( arr ); // HTML, JavaScript, CSS (no changes)

我们可以使用 slice() 来制作一个副本并在其上运行排序

function copySorted(arr) {
  return arr.slice().sort();
}

let arr = ["HTML", "JavaScript", "CSS"];

let sorted = copySorted(arr);

alert( sorted );
alert( arr );
重要性:5

创建一个构造函数 Calculator 来创建“可扩展”计算器对象。

任务包含两部分。

  1. 首先,实现方法 calculate(str),它采用一个类似于 "1 + 2" 的字符串,格式为“数字运算符数字”(空格分隔),并返回结果。应该理解加号 + 和减号 -

    用法示例

    let calc = new Calculator;
    
    alert( calc.calculate("3 + 7") ); // 10
  2. 然后添加方法 addMethod(name, func),它教计算器一个新的操作。它采用运算符 name 和实现它的双参数函数 func(a,b)

    例如,让我们添加乘法 *、除法 / 和幂 **

    let powerCalc = new Calculator;
    powerCalc.addMethod("*", (a, b) => a * b);
    powerCalc.addMethod("/", (a, b) => a / b);
    powerCalc.addMethod("**", (a, b) => a ** b);
    
    let result = powerCalc.calculate("2 ** 3");
    alert( result ); // 8
  • 此任务中没有括号或复杂表达式。
  • 数字和运算符之间用一个空格分隔。
  • 如果您想添加它,可能会进行错误处理。

使用测试打开沙箱。

  • 请注意方法的存储方式。它们只是添加到 this.methods 属性中。
  • 所有测试和数字转换都在 calculate 方法中完成。将来它可能会扩展以支持更复杂的表达式。
function Calculator() {

  this.methods = {
    "-": (a, b) => a - b,
    "+": (a, b) => a + b
  };

  this.calculate = function(str) {

    let split = str.split(' '),
      a = +split[0],
      op = split[1],
      b = +split[2];

    if (!this.methods[op] || isNaN(a) || isNaN(b)) {
      return NaN;
    }

    return this.methods[op](a, b);
  };

  this.addMethod = function(name, func) {
    this.methods[name] = func;
  };
}

在沙箱中使用测试打开解决方案。

重要性:5

您有一个 user 对象数组,每个对象都有 user.name。编写将它转换为名称数组的代码。

例如

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let users = [ john, pete, mary ];

let names = /* ... your code */

alert( names ); // John, Pete, Mary
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let users = [ john, pete, mary ];

let names = users.map(item => item.name);

alert( names ); // John, Pete, Mary
重要性:5

您有一个 user 对象数组,每个对象都有 namesurnameid

编写代码从中创建另一个数组,其中包含具有 idfullName 的对象,其中 fullNamenamesurname 生成。

例如

let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };

let users = [ john, pete, mary ];

let usersMapped = /* ... your code ... */

/*
usersMapped = [
  { fullName: "John Smith", id: 1 },
  { fullName: "Pete Hunt", id: 2 },
  { fullName: "Mary Key", id: 3 }
]
*/

alert( usersMapped[0].id ) // 1
alert( usersMapped[0].fullName ) // John Smith

所以,实际上你需要将一个对象数组映射到另一个对象数组。尝试在这里使用 =>。有一个小问题。

let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };

let users = [ john, pete, mary ];

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

/*
usersMapped = [
  { fullName: "John Smith", id: 1 },
  { fullName: "Pete Hunt", id: 2 },
  { fullName: "Mary Key", id: 3 }
]
*/

alert( usersMapped[0].id ); // 1
alert( usersMapped[0].fullName ); // John Smith

请注意,在箭头函数中,我们需要使用额外的括号。

我们不能这样写

let usersMapped = users.map(user => {
  fullName: `${user.name} ${user.surname}`,
  id: user.id
});

正如我们所记得的,有两个箭头函数:没有主体的 value => expr 和有主体的 value => {...}

这里 JavaScript 会将 { 视为函数主体的开始,而不是对象的开始。解决方法是将它们包装在“普通”括号中

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

现在好了。

重要性:5

编写函数 sortByAge(users),它获取一个具有 age 属性的对象数组,并按 age 对它们进行排序。

例如

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let arr = [ pete, john, mary ];

sortByAge(arr);

// now: [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete
function sortByAge(arr) {
  arr.sort((a, b) => a.age - b.age);
}

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let arr = [ pete, john, mary ];

sortByAge(arr);

// now sorted is: [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete
重要性:3

编写函数 shuffle(array),它会对数组元素进行随机重新排序。

多次运行 shuffle 可能会导致不同的元素顺序。例如

let arr = [1, 2, 3];

shuffle(arr);
// arr = [3, 2, 1]

shuffle(arr);
// arr = [2, 1, 3]

shuffle(arr);
// arr = [3, 1, 2]
// ...

所有元素顺序都应具有相等的概率。例如,[1,2,3] 可以重新排序为 [1,2,3][1,3,2][3,1,2] 等,每种情况的概率相等。

简单的解决方案可能是

function shuffle(array) {
  array.sort(() => Math.random() - 0.5);
}

let arr = [1, 2, 3];
shuffle(arr);
alert(arr);

这在某种程度上有效,因为 Math.random() - 0.5 是一个可能是正数或负数的随机数,因此排序函数会随机重新排序元素。

但由于排序函数并非旨在以这种方式使用,因此并非所有排列都具有相同的概率。

例如,考虑以下代码。它运行 shuffle 1000000 次并统计所有可能结果的出现次数

function shuffle(array) {
  array.sort(() => Math.random() - 0.5);
}

// counts of appearances for all possible permutations
let count = {
  '123': 0,
  '132': 0,
  '213': 0,
  '231': 0,
  '321': 0,
  '312': 0
};

for (let i = 0; i < 1000000; i++) {
  let array = [1, 2, 3];
  shuffle(array);
  count[array.join('')]++;
}

// show counts of all possible permutations
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

示例结果(取决于 JS 引擎)

123: 250706
132: 124425
213: 249618
231: 124880
312: 125148
321: 125223

我们可以清楚地看到偏差:123213 出现的频率远高于其他数字。

代码的结果可能因 JavaScript 引擎而异,但我们已经可以看到这种方法不可靠。

为什么它不起作用?一般来说,sort 是一个“黑匣子”:我们将一个数组和一个比较函数放入其中,并希望数组被排序。但由于比较的完全随机性,黑匣子会发疯,而它具体如何发疯取决于引擎之间不同的具体实现。

还有其他方法可以完成此任务。例如,有一个称为 Fisher-Yates 随机置乱 的出色算法。其思想是在逆序遍历数组,并在每个元素之前将其与随机元素交换

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i

    // swap elements array[i] and array[j]
    // we use "destructuring assignment" syntax to achieve that
    // you'll find more details about that syntax in later chapters
    // same can be written as:
    // let t = array[i]; array[i] = array[j]; array[j] = t
    [array[i], array[j]] = [array[j], array[i]];
  }
}

让我们用相同的方式对其进行测试

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
}

// counts of appearances for all possible permutations
let count = {
  '123': 0,
  '132': 0,
  '213': 0,
  '231': 0,
  '321': 0,
  '312': 0
};

for (let i = 0; i < 1000000; i++) {
  let array = [1, 2, 3];
  shuffle(array);
  count[array.join('')]++;
}

// show counts of all possible permutations
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

示例输出

123: 166693
132: 166647
213: 166628
231: 167517
312: 166199
321: 166316

现在看起来不错:所有排列出现的概率相同。

此外,Fisher-Yates 算法在性能方面要好得多,没有“排序”开销。

重要性:4

编写函数 getAverageAge(users),它获取一个包含属性 age 的对象数组,并返回平均年龄。

平均值的公式为 (age1 + age2 + ... + ageN) / N

例如

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };

let arr = [ john, pete, mary ];

alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28
function getAverageAge(users) {
  return users.reduce((prev, user) => prev + user.age, 0) / users.length;
}

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };

let arr = [ john, pete, mary ];

alert( getAverageAge(arr) ); // 28
重要性:4

arr 为一个数组。

创建一个函数 unique(arr),它应返回一个包含 arr 的唯一项的数组。

例如

function unique(arr) {
  /* your code */
}

let strings = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(strings) ); // Hare, Krishna, :-O

使用测试打开沙箱。

让我们遍历数组项

  • 对于每个项,我们将检查结果数组是否已包含该项。
  • 如果是,则忽略,否则添加到结果中。
function unique(arr) {
  let result = [];

  for (let str of arr) {
    if (!result.includes(str)) {
      result.push(str);
    }
  }

  return result;
}

let strings = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(strings) ); // Hare, Krishna, :-O

该代码有效,但其中存在潜在的性能问题。

方法 result.includes(str) 在内部遍历数组 result,并将每个元素与 str 进行比较以查找匹配项。

因此,如果 result 中有 100 个元素,并且没有一个与 str 匹配,那么它将遍历整个 result 并执行恰好 100 次比较。如果 result 很庞大,例如 10000,那么将会有 10000 次比较。

这本身并不是问题,因为 JavaScript 引擎非常快,所以遍历 10000 个数组只需几微秒。

但是,我们在 for 循环中对 arr 的每个元素执行此类测试。

因此,如果 arr.length10000,我们将得到类似 10000*10000 = 1 亿次的比较。这太多了。

因此,该解决方案只适用于小数组。

在本章的后面部分 Map 和 Set 中,我们将了解如何优化它。

在沙箱中使用测试打开解决方案。

重要性:4

假设我们收到一个用户数组,其形式为 {id:..., name:..., age:... }

创建一个函数 groupById(arr),该函数从中创建一个对象,其中 id 为键,数组项为值。

例如

let users = [
  {id: 'john', name: "John Smith", age: 20},
  {id: 'ann', name: "Ann Smith", age: 24},
  {id: 'pete', name: "Pete Peterson", age: 31},
];

let usersById = groupById(users);

/*
// after the call we should have:

usersById = {
  john: {id: 'john', name: "John Smith", age: 20},
  ann: {id: 'ann', name: "Ann Smith", age: 24},
  pete: {id: 'pete', name: "Pete Peterson", age: 31},
}
*/

在使用服务器数据时,此类函数非常方便。

在此任务中,我们假设 id 是唯一的。可能没有两个具有相同 id 的数组项。

请在解决方案中使用数组 .reduce 方法。

使用测试打开沙箱。

function groupById(array) {
  return array.reduce((obj, value) => {
    obj[value.id] = value;
    return obj;
  }, {})
}

在沙箱中使用测试打开解决方案。

教程地图

评论

在评论之前先阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 – 请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行代码 – 将其包装在 <pre> 标签中,对于超过 10 行的代码 – 使用沙箱 (plnkrjsbincodepen…)