数组方法

因为数组提供的方法很多。为了方便起见,在本章中,我们将按组讲解。

添加/移除数组元素

已知从开头或结尾添加删除元素的方法:

  • 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(str) 方法可以说是数组界的瑞士军刀。它可以做所有事情:添加,删除和插入元素。

语法是:

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

index 开始:删除 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" <-- 被删除元素的数组

我们可以将 deleteCount 设置为 0splice 方法就能够插入元素而不用删除:

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 str = "test";
let arr = ["t", "e", "s", "t"];

alert( str.slice(1, 3) ); // es
alert( arr.slice(1, 3) ); // e,s

alert( str.slice(-2) ); // st
alert( arr.slice(-2) ); // s,t

concat

arr.concat 将数组与其他数组和/或元素结合在一起。

语法:

arr.concat(arg1, arg2...)

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

结果是一个包含arrarg1arg2等元素的新数组。

如果参数是一个数组或具有 Symbol.isConcatSpreadable 属性,则其所有元素都将被复制。否则,复制参数本身。

例如:

let arr = [1, 2];

// merge arr with [3,4]
alert( arr.concat([3, 4])); // 1,2,3,4

// merge arr with [3,4] and [5,6]
alert( arr.concat([3, 4], [5, 6])); // 1,2,3,4,5,6

// merge arr with [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]
//[1, 2, arrayLike]

…但是,如果类似数组的对象具有 Symbol.isConcatSpreadable 属性,将替换其元素:

let arr = [1, 2];

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

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

查询数组

这些是在数组中查询某些内容的方法。

indexOf/lastIndexOf 和 includes

arr.indexOfarr.lastIndexOfarr.includes 方法与字符串操作具有相同的语法,只不过这里是对数组元素而不是字符进行操作:

  • arr.indexOf(item, from) 从索引 from 查询 item,如果找到返回索引,否则返回 -1
  • arr.lastIndexOf(item, from) — 和上面相同,只是从尾部开始查询。
  • arr.includes(item, from) — 从索引 from 查询 item,如果找到则返回 true

例如:

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

请注意,这些方法使用 === 比较。所以如果我们查询 false,会精确到是 false 而不是零。

如果我们想检查是否包含需要的元素,并且不想知道确切的索引,那么 arr.includes 是首选。

此外,includes 的一个非常小的差别是它能正确处理NaN,而不像 indexOf/lastIndexOf

const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1 (should be 0, but === equality doesn't work for NaN)
alert( arr.includes(NaN) );// true (correct)

find 和 findIndex

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

这时可以用 arr.find 方法。

语法:

let result = arr.find(function(item, index, array) {
  // 如果查询到返回 true
});

该函数对数组中的每个元素重复调用:

  • 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。其他参数 find 很少使用。

arr.findIndex 方法本质上是相同的,但它返回找到元素的索引而不是元素本身。

filter

find 方法查询的是使函数返回 true 的第一个元素。

如果需要匹配的有很多,我们可以使用 arr.filter(fn)

语法与 find 大致相同,但是它返回的是所有匹配元素组成的数组:

let results = arr.filter(function(item, index, array) {
  // 在元素通过过滤器时返回 true
});

例如:

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

// 返回前两个用户的数组
let someUsers = users.filter(item => item.id < 3);

alert(someUsers.length); // 2

转换数组

本节介绍转换或重新排序数组的方法。

map

arr.map 方法是最有用和经常使用的方法之一。

语法:

let result = arr.map(function(item, index, array) {
  // 返回新值而不是当前元素
})

它对数组中每个元素调用函数并返回符合结果的数组。

例如,在这里我们将每个元素转换为它的字符串长度:

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

sort(fn)

arr.sort 方法对数组进行排序

语法:

let arr = [ 1, 2, 15 ];

// 该方法重新排列 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 (a == b) return 0;
  if (a < b) return -1;
}

例如:

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 可以是由任何东西组成的数组。它可能包含数字或字符串或 html 元素或其他。我们对一组数据进行排序时,需要一个排序函数来确认如何比较这些元素。默认是按字符串排序的。

arr.sort(fn) 方法内置实现排序算法。我们不需要关心它是如何工作的(大多数情况下是优化过的快速排序算法)。它将自动遍历数组,使用提供的函数比较它的元素并对它们重新排序,我们所需要的只是提供用于比较的函数 fn

顺便说一句,如果我们想知道哪些元素进行了比较 — alert 是没有作用的:

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

该算法可以在过程中多次比较元素,但它会尽可能少地进行比较。

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

实际上,比较函数只需要返回一个正数表示更大,而负数表示更少。

通过这个原理我们可以编写更短的函数:

let arr = [ 1, 2, 15 ];

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

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

文章 "function-expression" 未找到还记得吗?这里使用箭头函数会更加简洁:

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

这与上面的那些更长的其它写法是完全相同的。

reverse

arr.reverse 方法颠倒 arr 中元素的顺序。

例如:

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

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

它也在返回后返回数组 arr

split 和 join

举一个现实生活的场景的例子,我们正在编写一个消息应用程序,并且该人员输入以逗号分隔的接收者列表: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
拆分为字母

调用空的参数 split(s) 会将字符串分成一个字母数组:

let str = "test";

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

arr.join(str)split 相反。它会在它们之间创建一串由 str 粘合的 arr 项。

例如:

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

let str = arr.join(';');

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

reduce/reduceRight

当我们需要遍历一个数组时 — 我们可以使用 forEach

当我们需要迭代并返回每个元素的数据时 — 我们可以使用 map

arr.reducearr.reduceRight 和上面差不多,但有点复杂。它们用于根据数组计算单个值。

语法是:

let value = arr.reduce(function(previousValue, item, index, arr) {
  // ...
}, initial);

该函数应用于元素。从第二个参数开始你可能就会觉得很眼熟了:

  • item — 当前的数组元素。
  • index — 当前索引。
  • arr — 数组本身。

目前为止,这很像 forEach/map。但还有一个参数不同就是:

  • previousValue — 是前一个函数调用的结果,第一次调用是初始化。

我们写个例子试试。

这里我们得到一行数组的总和:

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

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

alert(result); // 15

在这里,我们使用了 reduce 的最常见类型,它只使用 2 个参数。

让我们看看发生了什么的细节。

  1. 在第一次运行时,sum 是初始值(reduce 的最后一个参数),等于 0,current 是第一个数组元素,等于 1。所以结果是 1
  2. 在第二次运行时,sum = 1,我们添加第二个数组元素(2)并返回。
  3. 在第三次运行中,sum = 3,我们再添加一个元素,等等……

计算流程:

或者以表格的形式出现,每行代表的是下一个数组元素的函数调用:

sum current result
the first call 0 1 1
the second call 1 2 3
the third call 3 3 6
the fourth call 6 4 10
the fifth call 10 5 15

正如我们所看到的,先前调用的结果成为下一个调用的第一个参数。

我们也可以省略初始值:

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

// 删除初始值
let result = arr.reduce((sum, current) => sum + current);

alert( result ); // 15

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

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

但是这种使用需要非常小心。如果数组为空,那么在没有初始值的情况下调用 reduce 会导致错误。

例如:

let arr = [];

// Error: Reduce of empty array with no initial value
// 如果初始值存在,reduce 将返回空 arr。
arr.reduce((sum, current) => sum + current);

所以建议始终指定初始值。

arr.reduceRight 也一样,但是遍历是从右到左。

迭代:forEach

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

语法:

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

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

// 为每个元素调用 alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);

而这段代码更详细地介绍了它们在数组中的位置

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

该函数的结果(如果它返回的话)被抛弃并被忽略。

Array.isArray

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

所以 typeof 无法从对象中区分出数组来:

alert(typeof {}); // object
alert(typeof []); // 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 是可选的最后一个参数

thisArg 参数的值在 func 中变为 this

例如,在这里我们使用一个对象方法作为过滤器,thisArg 派上用场:

let user = {
  age: 18,
  younger(otherUser) {
    return otherUser.age < this.age;
  }
};

let users = [
  {age: 12},
  {age: 16},
  {age: 32}
];

// 找到比 user 小的所有 users
let youngerUsers = users.filter(user.younger, user);

alert(youngerUsers.length); // 2

在上面我们使用 user.younger 作为过滤器,并提供 user 作为它的上下文。如果我们没有提供上下文,users.filter(user.younger) 会调用user.younger 作为一个独立的函数,这时 this=undefined

总结

数组方法备忘录:

  • 添加/删除元素:

    • push(...items) — 从结尾添加元素,
    • pop() — 从结尾提取元素,
    • shift() — 从开头提取元素,
    • unshift(...items) — 从开头添加元素,
    • splice(pos, deleteCount, ...items) — 从 index 开始:删除 deleteCount 元素并在当前位置插入元素。
    • slice(start, end) — 它从所有元素的开始索引 "start" 复制到 "end" (不包括 "end") 返回一个新的数组。
    • concat(...items) — 返回一个新数组:复制当前数组的所有成员并向其中添加 items。如果有任何items 是一个数组,那么就取其元素。
  • 查询元素:

    • indexOf/lastIndexOf(item, pos) — 从 pos 找到 item,则返回索引否则返回 -1
    • includes(value) — 如果数组有 value,则返回 true,否则返回 false
    • find/filter(func) — 通过函数过滤元素,返回 true 条件的符合 find 函数的第一个值或符合 filter 函数的全部值。
    • findIndexfind 类似,但返回索引而不是值。
  • 转换数组:

    • map(func) — 从每个元素调用 func 的结果创建一个新数组。
    • sort(func) — 将数组倒序排列,然后返回。
    • reverse() — 在原地颠倒数组,然后返回它。
    • split/join — 将字符串转换为数组并返回。
    • reduce(func, initial) — 通过为每个元素调用 func 计算数组上的单个值并在调用之间传递中间结果。
  • 迭代元素:

    • forEach(func) — 为每个元素调用 func,不返回任何东西。
  • 其他:  – Array.isArray(arr) 检查 arr 是否是一个数组。

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

这些方法是最常用的方法,它们覆盖 99% 的用例。但是还有其他几个:

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

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

  • arr.fill(value, start, end) — 从 startendvalue 重复填充数组。

  • arr.copyWithin(target, start, end) — copies its elements from position start till position end into itself, at position target (overwrites existing).将其元素从 startendtarget 位置复制到 本身(覆盖现有)。

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

从第一眼看来,似乎有很多方法,很难记住。但实际上这比看起来要容易得多。

为了您有数组方法的经验,请仔细查看备忘单了解它们。然后解决本章的任务练习。

以后,当你需要对某个数组操作,而无从下手 — 可以来到这,查看备忘录并找到正确的方法。示例将帮助您正确编写它。很快你会自动记住这些方法,而无需你额外的努力。

任务

重要程度: 5

写函数 camelize(str) 将诸如 “my-short-string” 之类的由短划线分隔的单词变成骆驼式的 “myShortString”。

即:删除所有短横线,短横线后的第一个单词变为大写。

例如:

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

提示:使用 split 将字符串拆分成数组,然后将其转换 join 并返回。

打开带有测试的沙箱。

重要程度: 4

写一个函数 filterRange(arr, a, b) 获取一个数组 arr,查找 ab 之间的元素并返回它们的数组。

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

例如:

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

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

alert( filtered ); // 3,1(匹配值)

alert( arr ); // 5,3,8,1(未修改)

打开带有测试的沙箱。

重要程度: 4

写一个函数 filterRangeInPlace(arr, a, b) 获取一个数组 arr,并从中除去 ab 区间以外的所有值。测试:a ≤ arr[i] ≤ b

该函数只应修改数组。它不应该返回任何东西。

例如:

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

filterRangeInPlace(arr, 1, 4); // 删除了从 1 到 4 之外的数字

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

打开带有测试的沙箱。

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

// ... 倒序
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(没有变化)

我们可以使用 slice() 来创建一个副本并对其进行排序:

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

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

let sorted = copySorted(arr);

alert( sorted );
alert( arr );
重要程度: 5

你有一个 user 对象数组,每个对象都有 user.name。编写将其转换为 names 数组的代码。

例如:

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 属性并对它进行排序。

例如:

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 ? 1 : -1);
}

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);

// 现在排序是:[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 是一个可能是正数或负数的随机数,所以排序函数会随机地重新排序元素。

但是因为排序函数并不意味着以这种方式使用,所以并不是所有的排列都具有相同的概率。

例如,请考虑下面的代码。它运行 100 万次 shuffle 并计算所有可能结果:

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

// 出现所有可能排列的次数
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('')]++;
}

// 显示所有可能的排列的数量
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

示例结果(V8,2017年七月):

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

我们可以清楚地看到这种偏见:123213 比其他人更频繁出现。

JavaScript 引擎的代码结果可能会有所不同,但我们已经可以看到这种方法是不可靠的。

为什么它不起作用?一般来说,sort 是一个“黑匣子”:我们向其中抛出一个数组和一个比较函数,并期望数组被排序。由于比较的完全随机性,黑盒子变得复杂,它究竟发生了什么取决于引擎之间的具体实现。

还有其他很好的方法来完成这项任务。例如,有一个很好的算法叫做 Fisher-Yates shuffle。其思路是:逆向遍历数组,并将每个子项与前面随机的一个子项互相交换:

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1)); // r 从 0 到 i 的随机索引
    [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]];
  }
}

// 出现所有可能排列的次数
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('')]++;
}

// 出现所有可能排列的次数
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 = 100 百万的比较。好多啊。

所以该解决方案仅适用于小型数组。

进一步,在 Map、Set、WeakMap 和 WeakSet 我们将看到如何优化它。

使用沙箱的测试功能打开解决方案。

教程路线图

评论

在评论之前先阅读本内容…
  • 欢迎你在文章下添加补充内容、提出你的问题或回答提出的问题。
  • 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 建议使用沙箱(plnkrJSBincodepen 等)。
  • 如果你无法理解文章中的内容 — 请详细说明。