文章目录

  • 1. 扩展运算符
    • 1.1 含义
    • 1.2 替代函数的 ES5: apply() 方法
    • 1.3 扩展运算符的应用
      • 1.3.1 复制数组
      • 1.3.2 合并数组
      • 1.3.3 与解构赋值结合,会生成 存放在新地址的数组。
      • 1.3.4 字符串 与 扩展运算符
      • 1.3.5 实现了 Iterator 接口的对象,都可以用扩展运算符转为真正的数组。
      • 1.3.6 Map 和 Set 结构,Generator 函数
  • 2. Array.from()
    • 2.1 Array.from() 转换 类似数组对象
    • 2.2 Array.from() 转换 Iterator 接口的对象
      • 2.3.1【类似数组的对象】本质:必须有 length 属性
    • 2.4 Array.from() 接收一个函数 作为第二参数:处理元素并放回原数组
    • 2.5 Array.from() 将字符串转为数组:处理Unicode字符
  • 3. Array.of() : 将一组值,转换为数组
  • 4. 【实例方法】copyWithin()
  • 5. 【实例方法】find(), findIndex(), findLast(), findLastIndex()
    • 5.1 find()
    • 5.2 findIndex()
      • 5.2.1 findIndex() 接收第二个参数(对象)绑定到 回调函数(第一个参数)的 this 对象上
  • 6. 【实例方法】fill() : 使用给定值,填充一个数组
  • 7. 【实例方法】entries(), keys(), values()
    • 7.1 ☆☆ for...of 遍历 keys(), values(), entries()
  • 8. 【实例方法】includes
    • 8.1 Array.prototype.includes
    • 8.2 indexOf()
  • 9.【实例方法】flat(), flatMap()
    • 9.1 flat() 函数 : 将嵌套的数组拉平,变成一维的数组。
    • 9.2 flatMap() 函数
  • 10. 【实例方法】at()
  • 11. 【实例方法】toReversed(), toSorted(), toSpliced(), with()
  • 12. 【实例方法】group(), groupToMap() 数组成员分组
    • 12.1 group() 函数
    • 12.2 groupBy() 函数
    • 12.3 groupMap() 函数
  • 13. 数组的空位:避免出现空位
    • 13.1 ES5 对空位的处理
    • 13.2 ES6 对空位的处理
  • 14. Array.prototype.sort() 的排序稳定性

1. 扩展运算符

1.1 含义

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为 用逗号分隔的参数序列

console.log(...[1, 2, 3])   // 转换为 console.log(1, 2, 3)
// 1 2 3console.log(1, ...[2, 3, 4], 5)  // 真的是逗号分隔
// 1 2 3 4 5[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]   // 转换为逗号分隔的参数序列

该运算符主要用于函数调用。

function push(array, ...items) {array.push(...items);
}function add(x, y) {return x + y;
}const numbers = [4, 38];
add(...numbers) // 42  // 转换为逗号分隔的参数序列:add(4, 38)

上面代码中,array.push(...items)add(...numbers)这两行,都是函数的调用,它们都使用了扩展运算符。该运算符将一个数组,变为参数序列。

扩展运算符正常的函数参数 可以结合使用,非常灵活。

function fun(v, w, x, y, z) { }
const args = [0, 1];fun(-1, ...args, 2, ...[3]);  // 真的很好用

————
扩展运算符后面还可以 放置表达式

const arr = [...(x > 0 ? ['a'] : []), 'b',
];

如果扩展运算符后面是一个空数组,则不产生任何效果。

[...[], 1]
// [1]

————
注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。

(...[1, 2])
// Uncaught SyntaxError: Unexpected numberconsole.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected numberconsole.log(...[1, 2])
// 1 2

上面三种情况,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号 不是 函数调用


1.2 替代函数的 ES5: apply() 方法

由于扩展运算符可以展开数组,所以不再需要 apply() 方法将数组转为函数的参数了

// ES5 的写法
function f(x, y, z) {// ...
}
var args = [0, 1, 2];
f.apply(null, args);// ES6 的写法
function f(x, y, z) {// ...
}
let args = [0, 1, 2];
f(...args);

下面是扩展运算符取代 apply() 方法的一个实际的例子,应用Math.max()方法,简化求出一个数组最大元素的写法。

// ES5 的写法
Math.max.apply(null, [14, 3, 77])// ES6 的写法
Math.max(...[14, 3, 77])// 等同于
Math.max(14, 3, 77);

上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用 Math.max() 函数,将数组转为一个 参数序列 ,然后求最大值。有了扩展运算符以后,就可以直接用 Math.max() 了。

另一个例子是通过 push() 函数,将一个数组添加到另一个数组的尾部。

// ES5 的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);// ES6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);   // 太牛了
// ES5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))// ES6
new Date(...[2015, 1, 1]);

1.3 扩展运算符的应用

1.3.1 复制数组

数组是 复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针而不是克隆一个全新的数组。

【直接复制数组是复制指针,指向同一块地址】

const a1 = [1, 2];
const a2 = a1;   // 这就是直接复制,复制了一个指针,指向底层数据结构的指针。他们指向同一个内存地址a2[0] = 2;  // 修改的是同一个地址的内容
a1 // [2, 2]

上面代码中,a2 并不是 a1 的克隆,而是 指向同一份数据的另一个指针。修改 a2,会直接导致 a1 的变化。

ES5 只能用变通方法来复制数组。

const a1 = [1, 2];
const a2 = a1.concat();  // ES5的变通方法复制数组a2[0] = 2;
a1 // [1, 2]

扩展运算符提供了复制数组的简便写法。

const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
// Array(2) lzl强行解释:对a2进行扩展运算后,[...a2]与a1相等,说明a2与a1相等

上面的两种写法,a2 都是 a1 的克隆(复制一样的内容,放到新的内存地址)。

1.3.2 合并数组

const arr1 = ["a","b"];
const arr2 = ["c"];
const arr3 = ["d", "e"];// ES5 的合并数组
const arr4=arr1.concat(arr2, arr3);// ES6 的合并数组
const arr5 = [...arr1, ...arr2, ...arr3];console.log(arr4);  // [ 'a', 'b', 'c', 'd', 'e' ]
console.log(arr5);  // [ 'a', 'b', 'c', 'd', 'e' ]arr1.push("ff")
console.log(arr4);  // [ 'a', 'b', 'c', 'd', 'e' ]
console.log(arr5);  // [ 'a', 'b', 'c', 'd', 'e' ]  arr2[0]="gg"
console.log(arr4);  // [ 'a', 'b', 'c', 'd', 'e' ]
console.log(arr5);  // [ 'a', 'b', 'c', 'd', 'e' ]  // 为什么浅拷贝,我更改arr2[0]也没有引起合并数组的变化,
// 是让arr2[0]指向新的内存地址了吗?
// 没有吧,应该还是arr2[0]的内存地址,我只是改变了数据而已呀!?

不过,这两种方法都是 浅拷贝,使用的时候需要注意。

const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];const a3 = a1.concat(a2);  // ES5
const a4 = [...a1, ...a2]; // ES6  合并的是内存地址console.log(a3[0] === a1[0]);  // true  内容相等,且存放内容的地址相等
console.log(a4[0] === a1[0]);  // truea1[0] = { a: 0 };  // 让 a1 指向新的内存地址了
console.log(a1);  // [{ a: 0 }]a2[0].bar = 33;  // 更改了内存中的值
console.log(a3); // [{ foo: 1 }, { bar: 33 }] ES5
console.log(a4); // [{ foo: 1 }, { bar: 33 }] ES6

上面代码中,a3a4 是用两种不同方法合并而成的新数组,但是它们的成员都是 对原数组成员的引用,这就是 【浅拷贝】。如果修改了引用指向的值,会同步反映到新数组。

1.3.3 与解构赋值结合,会生成 存放在新地址的数组。

扩展运算符可以与解构赋值结合起来,用于生成数组。【深拷贝

// ES5
a = list[0];
rest = list.slice(1)// ES6
[a, ...rest] = list

下面是另外一些例子。

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   // []

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错

1.3.4 字符串 与 扩展运算符

扩展运算符还可以将字符串转为真正的数组。

[...'hello']
// [ "h", "e", "l", "l", "o" ]

上面的写法,有一个重要的好处,那就是能够正确识别 四个字节 的 Unicode 字符。

'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3

上面代码的第一种写法,JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。

function length(str) {return [...str].length;
}length('x\uD83D\uDE80y') // 3

凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。

let str = 'x\uD83D\uDE80y';str.split('').reverse().join('')
// 'y\uDE80\uD83Dx'[...str].reverse().join('')
// 'y\uD83D\uDE80x'

上面代码中,如果不用扩展运算符,字符串的reverse()操作就不正确。

1.3.5 实现了 Iterator 接口的对象,都可以用扩展运算符转为真正的数组。

任何定义了==遍历器==(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。

因为:【扩展运算符 内部调用的是 数据结构的 Iterator 接口!

let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
// 【arguments对象】
function foo() {const args = [...arguments];
}// 【NodeList对象】
[...document.querySelectorAll('div')]

上面代码中,querySelectorAll() 方法返回的是一个 NodeList 对象。它不是数组,而是一个 类似数组的对象。

实际应用中,常见的 【实现了 Iterator 接口的】类似数组的对象 是:
(1) DOM 操作返回的 NodeList 集合
(2)函数内部的 arguments 对象。

这时,扩展运算符可以将其转为真正的数组,原因就在于:【 NodeList 对象实现了 Iterator】。(有很多 类似数组对象 身上并没有 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]

上面代码中,先定义了 Number 对象的遍历器接口,扩展运算符将 5 自动转成Number 实例以后,就会调用这个接口,就会返回自定义的结果。
————
对于那些没有 部署 Iterator 接口 的类似数组的对象,扩展运算符就无法将其转为真正的数组。

let arrayLike = {'0': 'a','1': 'b','2': 'c',length: 3
};// TypeError: Cannot spread non-iterable object.
let arr = [...arrayLike];

上面代码中,arrayLike 是一个类似数组的对象,但是没有部署 Iterator 接口,扩展运算符就会报错。这时,可以改为使用 Array.from 方法将 arrayLike 转为真正的数组。

1.3.6 Map 和 Set 结构,Generator 函数

  • 扩展运算符 内部调用的是 数据结构的 Iterator 接口!
  • 因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。
let map = new Map([[1, "one"],[2, "two"],[3, "three"],
]);
let a = [...map];
console.log(a); // [ [1, "one"], [2, "two"], [3, "three"] ]let arr = [...map.keys()];  // [1, 2, 3]
console.log(arr);

————
Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符

const go = function*(){yield 1;yield 2;yield 3;
};[...go()] // [1, 2, 3]

上面代码中,变量 go 是一个 Generator 函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。
————
如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。

const obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object

2. Array.from()

Array.from() 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和 可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

let arrayLike = {'0': 'a','1': 'b','2': 'c',length: 3
};// ES5 的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
//
// ES6 的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

----- slice(start, end):方法可从已有数组中返回选定的元素,返回一个新数组,包含从 startend(不包含该元素)的数组元素。

2.1 Array.from() 转换 类似数组对象

实际应用中,常见的【类似数组的对象】是 DOM 操作返回的 NodeList 集合,以及函数内部的 arguments 对象。Array.from() 都可以将它们转为真正的数组。

// NodeList 对象
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {return p.textContent.length > 100;
});// arguments 对象
function foo() {var args = Array.from(arguments);// ...
}

————

2.2 Array.from() 转换 Iterator 接口的对象

只要是部署了 Iterator 接口的数据结构,Array.from() 都能将其转为数组。

Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']

————
如果参数是一个真正的数组,Array.from() 会返回一个一模一样的新数组。

Array.from([1, 2, 3])
// [1, 2, 3]

————
值得提醒的是,扩展运算符(...)也可以将某些数据结构转为数组。

// 【arguments对象】
function foo() {const args = [...arguments];
}// 【NodeList对象】
[...document.querySelectorAll('div')]

扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。

2.3.1【类似数组的对象】本质:必须有 length 属性

Array.from() 方法还支持类似数组的对象。所谓【类似数组的对象】,本质特征 只有一点,即 必须有 length 属性

因此,任何有 length 属性的对象,都可以通过Array.from() 方法转为数组,而此时扩展运算符就无法转换(没有定义 Iterator)。

Array.from({ length: 3 });// [ undefined, undefined, undefined ]

上面代码中,Array.from() 返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。

对于还没有部署该方法的浏览器,可以用 Array.prototype.slice() 法替代。

// ES5
const toArray = (() =>Array.from ? Array.from : obj => [].slice.call(obj)
)();

问题:什么是 [].slice.call(arguments)
JS中 [].slice.call() 的理解
————

2.4 Array.from() 接收一个函数 作为第二参数:处理元素并放回原数组

Array.from() 还可以接受一个函数作为第二个参数,作用类似于数组的 map() 方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

下面的例子是取出一组 DOM 节点的文本内容。

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)

下面的例子将数组中布尔值为 false 的成员转为 0

Array.from([1, , 2, , 3], (n) => n || 0)
// [1, 0, 2, 0, 3]

另一个例子是返回各种数据的类型。

function typesOf () {return Array.from(arguments, value => typeof value)
}
typesOf(null, [], NaN)
// ['object', 'object', 'number']

————
如果 map() 函数里面用到了 this 关键字,还可以传入 Array.from() 的第三个参数,用来绑定 this

Array.from() 可以将各种值转为真正的数组,并且还提供 map 功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。(这就是为什么要转换为数组!为了使用数组上众多的方法)

// 太牛掰了
Array.from({ length: 2 }, () => 'jack')
// ['jack', 'jack']

上面代码中,Array.from() 的 第一个参数指定了第二个参数运行的次数。这种特性可以让该方法的用法变得非常灵活
————

2.5 Array.from() 将字符串转为数组:处理Unicode字符

Array.from() 的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种 Unicode 字符,可以避免 JavaScript 将大于 \uFFFF 的 Unicode 字符,算作两个字符的 bug。

function countSymbols(string) {return Array.from(string).length;
}

3. Array.of() : 将一组值,转换为数组

Array.of() 方法用于将一组值,转换为数组。

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1

这个方法的主要目的,是弥补数组构造函数 Array() 的不足。因为参数个数的不同,会导致 Array() 的行为有差异。

Array() // []
Array(3) // [, , ,]  这是什么鬼啊,我靠,哈哈
Array(3, 11, 8) // [3, 11, 8]

上面代码中,Array() 方法没有参数、一个参数、三个参数时,返回的结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数只有一个正整数时,实际上是指定数组的长度。

Array.of() 基本上可以用来替代 Array()new Array() ,并且 不存在由于参数不同而导致的重载。它的行为非常统一。

Array.of()  // []  如果没有参数,就返回一个空数组。
Array.of(undefined)  // [undefined]
Array.of(1)  // [1]
Array.of(1, 2)  // [1, 2]

Array.of() 方法可以用下面的代码模拟实现。

function ArrayOf(){return [].slice.call(arguments);
}

4. 【实例方法】copyWithin()

数组实例的 copyWithin() 方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
……


5. 【实例方法】find(), findIndex(), findLast(), findLastIndex()

5.1 find()

————
数组实例的 find() 方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为 true 的成员,然后返回该成员。如果没有符合条件的成员,则返回 undefined

【遍历查找,返回满足条件元素】(很多回调函数都是这样的)

// 找出数组中第一个小于 0 的成员
[1, 4, -5, 10].find((n) => n < 0)
// -5
[1, 5, 10, 15].find(function(value, index, arr) {return value > 9;
}) // 10

上面代码中,find() 方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
————

5.2 findIndex()

————
数组实例的 findIndex() 方法的用法与 find() 方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

//  一个回调函数
[1, 5, 10, 15].findIndex(function(value, index, arr) {return value > 9;
}) // 2

5.2.1 findIndex() 接收第二个参数(对象)绑定到 回调函数(第一个参数)的 this 对象上

这两个方法都可以接受 第二个参数,用来 绑定回调函数 的 this 对象

function f(v){return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person);    // 26
// 将第二个参数(对象)绑定到 回调函数(第一个参数)的 this 对象上
// 回调函数中的 this 指向 第二个参数(对象)

上面的代码中,find() 函数接收了第二个参数 person 对象,回调函数中的 this 对象指向 person 对象。
————

另外,这两个方法都可以发现 NaN,弥补了数组的 indexOf() 方法的不足。

[NaN].indexOf(NaN)
// -1[NaN].findIndex(y => Object.is(NaN, y))
// 0

上面代码中,indexOf() 方法无法识别数组的 NaN 成员,但是 findIndex() 方法可以借助 Object.is() 方法做到。

find()findIndex() 都是从数组的 0 号位,依次向后检查。ES2022 新增了两个方法 findLast()findLastIndex() ,从数组的最后一个成员开始,依次向前检查,其他都保持不变。

const array = [{ value: 1 },{ value: 2 },{ value: 3 },{ value: 4 }
];array.findLast(n => n.value % 2 === 1); // { value: 3 }
array.findLastIndex(n => n.value % 2 === 1); // 2

上面示例中,findLast()findLastIndex() 从数组结尾开始,寻找第一个 value 属性为奇数的成员。结果,该成员是 { value: 3 },位置是2号位。


6. 【实例方法】fill() : 使用给定值,填充一个数组

fill 方法使用给定值,填充一个数组。

['a', 'b', 'c'].fill(7)
// [7, 7, 7]new Array(3).fill(7)
// [7, 7, 7]

上面代码表明,fill 方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
————
fill 方法还可以接受第二个和第三个参数,用于指定 填充的起始位置和结束位置

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

上面代码表示,fill 方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。
————
注意,如果填充的类型为对象,那么被赋值的是 同一个内存地址的对象,而不是深拷贝对象。

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]]

7. 【实例方法】entries(), keys(), values()

7.1 ☆☆ for…of 遍历 keys(), values(), entries()

ES6 提供三个新的方法——entries()keys()values()——用于遍历数组。它们都 返回一个遍历器对象(详见《Iterator》一章),可以用 for...of 循环进行遍历,唯一的区别是 keys() 是对键名的遍历、values() 是对键值的遍历,entries() 是对键值对的遍历。

for (let index of ['a', 'b'].keys()) {console.log(index);
}
// 0
// 1for (let elem of ['a', 'b']) {  // for in遍历key,for of遍历value,for of只能用于数组console.log(elem);
}
// 'a'
// 'b'
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"

(1)for of 不同与 forEach, 它可以与 breakcontinuereturn 配合使用,也就是说 for of 循环可以随时退出循环。
(2)推荐在循环对象属性的时候使用 for…in(现在使用 Object.keys() 代替),在 遍历数组 的时候的时候使用 for...of。【for of 不能用于对象】
(3)for...in 遍历 keyfor...of 遍历 value

————
如果不使用 for...of 循环,可以手动调用 遍历器对象next 方法,进行遍历。

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']

8. 【实例方法】includes

8.1 Array.prototype.includes

Array.prototype.includes 方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 includes 方法类似。ES2016 引入了该方法。

// 返回一个 Boolean 值
[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

该方法的第二个参数表示搜索的起始位置,默认为 0
如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

8.2 indexOf()

没有该方法之前,我们通常使用数组的 indexOf 方法,检查是否包含某个值

if (arr.indexOf(el) !== -1) {// ...
}

indexOf 方法有两个缺点:
(1)不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于 -1,表达起来不够直观。
(2)它内部使用严格相等运算符(===)进行判断,这会导致对 NaN 的误判。

[NaN].indexOf(NaN)
// -1

includes 使用的是不一样的判断算法,就没有这个问题。

[NaN].includes(NaN)
// true

————
下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。

const contains = (() =>Array.prototype.includes? (arr, value) => arr.includes(value): (arr, value) => arr.some(el => el === value)
)();contains(['foo', 'bar'], 'baz'); // => false

————
另外,Map 和 Set 数据结构有一个 has 方法,需要注意与 includes 区分。

  • Map 结构的 has方法,是用来查找 键名 的,比如 Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
  • Set 结构的 has 方法,是用来查找 的,比如 Set.prototype.has(value)WeakSet.prototype.has(value)

9.【实例方法】flat(), flatMap()

9.1 flat() 函数 : 将嵌套的数组拉平,变成一维的数组。

数组的成员有时还是数组,Array.prototype.flat() 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

[1, 2, [3, 4]].flat()  // 我直接用扩展运算符不就行了吗
// [1, 2, 3, 4]

上面代码中,原数组的成员里面有一个数组,flat() 方法将子数组的成员取出来,添加在原来的位置。
————
flat() 默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将 flat() 方法的参数写成一个整数,表示想要拉平的层数,默认为1。

[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]][1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

上面代码中,flat() 的参数为2,表示要“拉平”两层的嵌套数组。
————
如果不管有多少层嵌套,都要转成一维数组,可以用 Infinity 关键字作为参数。

[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]

————
如果原数组有空位,flat() 方法会跳过空位。

[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]

9.2 flatMap() 函数

flatMap() 方法 对原数组的每个成员 执行一个函数(相当于执行Array.prototype.map()),函数的返回值 替换掉 原值,然后 对返回值组成的数组 执行 flat() 方法。该方法返回一个新数组,不改变原数组。

// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

flatMap() 只能展开一层数组。

// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])  // 还能这样玩
// [[2], [4], [6], [8]]

上面代码中,遍历函数返回的是一个双层的数组,但是默认只能展开一层,因此 flatMap() 返回的还是一个嵌套数组。
————
flatMap() 方法的参数是一个遍历函数,该函数可以接受三个参数,分别是 当前数组成员、当前数组成员的位置(从零开始)、原数组。

arr.flatMap(function callback(currentValue[, index[, array]]) {// ...
}[, thisArg])

flatMap() 方法还可以有第二个参数,用来绑定遍历函数里面的this。


10. 【实例方法】at()

长久以来,JavaScript 不支持数组的负索引,如果要引用数组的最后一个成员,不能写成 arr[-1],只能使用 arr[arr.length - 1]

这是因为方括号运算符 [] 在 JavaScript 语言里面,不仅用于数组,还用于对象。

对于对象来说,方括号里面就是键名,比如 obj[1] 引用的是 键名 为字符串1的键,同理 obj[-1] 引用的是 键名 为字符串-1的键。

由于 JavaScript 的数组是特殊的对象,所以方括号里面的负数无法再有其他语义了,也就是说,不可能添加新语法来支持负索引。

为了解决这个问题,ES2022 为数组实例增加了 at() 方法,接受一个整数作为参数,返回对应位置的成员,并支持负索引。这个方法不仅可用于数组,也可用于字符串和类型数组(TypedArray)

const arr = [5, 12, 8, 130, 44];
arr.at(2) // 8
arr.at(-2) // 130

如果参数位置超出了数组范围,at() 返回 undefined

const sentence = 'This is a sample sentence';sentence.at(0); // 'T'
sentence.at(-1); // 'e'sentence.at(-100) // undefined
sentence.at(100) // undefined

11. 【实例方法】toReversed(), toSorted(), toSpliced(), with()

很多数组的传统方法会改变原数组,比如 push()pop()shift()unshift()等等。数组只要调用了这些方法,它的值就变了。现在有一个提案,允许对数组进行操作时,不改变原数组,而返回一个原数组的拷贝。

这样的方法一共有四个。

  • Array.prototype.toReversed() -> Array
  • Array.prototype.toSorted(compareFn) -> Array
  • Array.prototype.toSpliced(start, deleteCount, ...items) -> Array
  • Array.prototype.with(index, value) -> Array

它们分别对应数组的原有方法。

  • toReversed() 对应 reverse(),用来颠倒数组成员的位置。
  • toSorted() 对应 sort(),用来对数组成员排序。
  • toSpliced() 对应 splice(),用来在指定位置,删除指定数量的成员,并插入新成员。
  • with(index, value) 对应 splice(index, 1, value),用来将指定位置的成员替换为新的值。

上面是这四个新方法对应的原有方法,含义和用法完全一样,唯一不同的是不会改变原数组,而是返回 原数组操作后的拷贝

const sequence = [1, 2, 3];
sequence.toReversed() // [3, 2, 1]
sequence // [1, 2, 3]const outOfOrder = [3, 1, 2];
outOfOrder.toSorted() // [1, 2, 3]
outOfOrder // [3, 1, 2]const array = [1, 2, 3, 4];
array.toSpliced(1, 2, 5, 6, 7) // [1, 5, 6, 7, 4]
array // [1, 2, 3, 4]const correctionNeeded = [1, 1, 3];
correctionNeeded.with(1, 2) // [1, 2, 3]
correctionNeeded // [1, 1, 3]

12. 【实例方法】group(), groupToMap() 数组成员分组

  • 按照字符串分组就使用 group() ,按照对象分组就使用 groupToMap()

12.1 group() 函数

数组成员分组 是一个常见需求,比如 SQL 有 GROUP BY子句和函数式编程有 MapReduce 方法。现在有一个提案,为 JavaScript 新增了数组实例方法 group()groupToMap(),它们可以根据分组函数的运行结果,将数组成员分组。

group() 的参数是一个分组函数,原数组的每个成员都会依次执行这个函数,确定自己是哪一个组。

const array = [1, 2, 3, 4, 5];array.group((num, index, array) => {return num % 2 === 0 ? 'even': 'odd';
});
// { odd: [1, 3, 5], even: [2, 4] }  // 返回的是一个对象

group() 的分组函数可以接受三个参数,依次是数组的当前成员、该成员的位置序号、原数组(上例是numindexarray)。分组函数的返回值应该是 字符串(或者可以自动转为字符串),以作为分组后的组名。

group() 的返回值是一个对象,该对象的 键名 就是每一组的组名,即分组函数返回的每一个字符串(上例是evenodd);该对象的 键值一个数组,包括所有产生当前键名的原数组成员。

group() 还可以接受一个对象,作为第二个参数。该对象会绑定分组函数(第一个参数)里面的 this,不过如果分组函数是一个箭头函数,该对象无效,因为箭头函数内部的 this 是固化的。

12.2 groupBy() 函数

下面是另一个例子。

[6.1, 4.2, 6.3].groupBy(Math.floor)
// { '4': [4.2], '6': [6.1, 6.3] }

上面示例中,Math.floor 作为分组函数,对原数组进行分组。它的返回值原本是数值,这时会自动转为字符串,作为分组的组名。原数组的成员根据分组函数的运行结果,进入对应的组。

12.3 groupMap() 函数

groupToMap() 的作用和用法与 group() 完全一致,唯一的区别是 返回值是一个 Map 结构,而不是对象。

Map 结构的 键名 可以是各种值,所以不管分组函数返回什么值,都会直接作为组名(Map 结构的键名)不会强制转为字符串

这对于 分组函数返回值(组名)是对象 的情况,尤其有用。

const array = [1, 2, 3, 4, 5];const odd  = { odd: true };
const even = { even: true };
array.groupToMap((num, index, array) => {return num % 2 === 0 ? even: odd;  // 函数返一个对象作为组名
});
//  Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }

上面示例返回的是一个 Map 结构,它的键名就是分组函数返回的两个对象 oddeven

总之,按照字符串分组就使用 group() ,按照对象分组就使用 groupToMap()


13. 数组的空位:避免出现空位

数组的空位指的是,数组的某一个位置没有任何值,比如 Array() 构造函数返回的数组都是空位。

Array(3)  // [, , ,]   // Array(3)返回一个具有 3 个空位的数组。

空位不是 undefined,某一个位置的值等于 undefined,依然是有值的。空位是没有任何值,in 运算符可以说明这一点。

0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false

上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值
————

13.1 ES5 对空位的处理

【ES5】 对空位的处理,已经很不一致了,大多数情况下会忽略空位。

  • forEach(), filter(), reduce(), every()some() 都会跳过空位。
  • map() 会跳过空位,但会保留这个值
  • join()toString() 会将空位视为undefined,而 undefinednull 会被处理成空字符串。
// ES5
// forEach方法
[,'a'].forEach((x, i) => console.log(i)); // 1// filter方法
['a',,'b'].filter(x => true) // ['a','b']// every方法
[,'a'].every(x => x==='a') // true// reduce方法
[1,,2].reduce((x, y) => x+y) // 3// some方法
[,'a'].some(x => x !== 'a') // false// map方法
[,'a'].map(x => 1) // [,1]// join方法
[,'a',undefined,null].join('#') // "#a##"// toString方法
[,'a',undefined,null].toString() // ",a,,"

————

13.2 ES6 对空位的处理

【ES6】 则是 明确将空位转为 undefined

Array.from() 方法会将数组的空位,转为 undefined,也就是说,这个方法不会忽略空位。

Array.from(['a',,'b'])
// [ "a", undefined, "b" ]

扩展运算符(...)也会将空位转为 undefined

[...['a',,'b']]
// [ "a", undefined, "b" ]

copyWithin() 会连空位一起拷贝。

[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]

fill() 会将 空位 视为正常的数组位置。

Array(3)  // [, , ,]   // Array(3)返回一个具有 3 个空位的数组。
new Array(3).fill('a')  // ["a","a","a"]

for...of 循环也会遍历空位。

let arr = [, ,];
for (let i of arr) {console.log(1);
}
// 1
// 1

上面代码中,数组arr有两个空位,for...of 并没有忽略它们。如果改成 map() 方法遍历,空位是会跳过的。
————
entries()keys()values()find()findIndex() 会将空位处理成 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

【由于空位的处理规则非常不统一,所以建议避免出现空位。】


14. Array.prototype.sort() 的排序稳定性

排序稳定性(stable 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"]

上面代码对数组arr按照首字母进行排序。排序结果中,strawspork 的前面,跟原始顺序一致,所以排序算法stableSorting是稳定排序。

const unstableSorting = (s1, s2) => {if (s1[0] <= s2[0]) return -1;return 1;
};arr.sort(unstableSorting)
// ["apple", "peach", "spork", "straw"]

上面代码中,排序结果是 sporkstraw前面,跟原始顺序相反,所以排序算法unstableSorting 是不稳定的。

常见的排序算法之中,插入排序、合并排序、冒泡排序等都是稳定的,堆排序、快速排序等是不稳定的。

不稳定排序的主要缺点是,多重排序时可能会产生问题。假设有一个姓和名的列表,要求按照 “姓氏为主要关键字,名字为次要关键字” 进行排序。开发者可能会先按名字排序,再按姓氏进行排序。如果排序算法是稳定的,这样就可以达到 “先姓氏,后名字” 的排序效果。如果是不稳定的,就不行。

早先的 ECMAScript 没有规定,Array.prototype.sort() 的默认排序算法是否稳定,留给浏览器自己决定,这导致某些实现是不稳定的。

ES2019 明确规定,Array.prototype.sort()默认排序算法必须稳定。这个规定已经做到了,现在 JavaScript 各个主要实现的默认排序算法都是稳定的。


END

九、数组的扩展(扩展运算符)相关推荐

  1. 数组超过预设的最大数组大小_工作表数组大小的扩展及意义

    朋友们好,今日给大家继续讲解VBA数组与字典解决方案的第17讲,数组大小的扩充问题.这一讲的内容相对比较简单,在之前的章节中讲了数组与数组的计算规律,也是利用了数组的扩展原理. 其实,两个数组计算时, ...

  2. linux扩展两个桌面,经验分享:九大GNOME Shell扩展助您定制桌面Linux

    原标题:经验分享:九大GNOME Shell扩展助您定制桌面Linux 每位用户在首次设置新计算机时,都会进行有针对性的自定义--包括切换桌面环境.安装终端shell.选择自己喜欢的浏览器或者更改壁纸 ...

  3. 征战蓝桥 —— 2015年第六届 —— C/C++A组第5题——九数组分数

    九数组分数 1,2,3-9 这九个数字组成一个分数,其值恰好为1/3,如何组法? 下面的程序实现了该功能,请填写划线部分缺失的代码. #include <stdio.h>void test ...

  4. 29、数据结构笔记之二十九数组之硬币抛掷模拟

    29.数据结构笔记之二十九数组之硬币抛掷模拟 "人生是各种不同的变故.循环不已的痛苦和欢乐组成的.那种永远不变的蓝天只存在于心灵中间,向现实的人生去要求未免是奢望.-- 巴尔扎克" ...

  5. 【译】Chrome 扩展 : 扩展是什么?

    引子 按照 Chrome 扩展 : 欢迎中的引导,接触到的相关介绍. 原文:What are extensions? 版本:Last updated: Friday, March 12, 2021 源 ...

  6. spring成神之路第二十九篇:BeanFactory 扩展(BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor)...

    Spring中有2个非常重要的接口:BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor,这2个接口面试中也会经常问到,本文我们一起 ...

  7. 微信小程序开发(九):使用扩展组件库

    前端开发中离不开各种组件库,我最先接触的组件库还是Bootstrap,后来工作中又陆续使用了inoic.ng-zorro等各种不同的库. 在微信小程序开发中也有多种组件库,这里记录其中几种不同组件库的 ...

  8. 数组方法深入扩展(遍历forEach,filter,reduce等)

    注:先写入数据API,并在html中创建了5个li,接下来的数组方法都是通过这个数组来应用,并通过模拟每个方法来明白其实现的原理. <ul><li></li>< ...

  9. 从易于扩展扩展的角度来设计FizzBuzzWhizz

    序言 最近FizzBuzzWhizz比较热,很多OSCER们也写出了自己的版本,有写的最快的,有写的最短的. 前面写过一篇文章叫 悠然乱弹:拉钩网FizzBuzzWhizz试题之悠然版解答,是悠然闲来 ...

  10. Shell教程(三):数组/Arrays、基本运算符

    定义数组值: 一个数组变量和标量变量之间的差异可以解释如下. 说,你正试图表示各种学生为变量集的名字.每一个单个变量是一个标量变量,如下所示: NAME01="Zara" NAME ...

最新文章

  1. 企业级Python开发大佬利用网络爬虫技术实现自动发送天气预告邮件
  2. 把9999的各个2进制位置上的1的 值打印出来? 如何实现? 1的数量 二机制位 x(x-1)
  3. Django中一个项目使用多个数据库(原生sql 的使用,亲测)
  4. 解决子线程操作UI的方法
  5. mysql fetch next from_MySql 存储过程 动态sql
  6. 在C++中反射调用.NET(二)
  7. ZOJ 3228(AC自动机+修改的匹配)
  8. c语言sort函数排序二维数组,c++ - 如何使用stl sort函数根据第二列对二维数组进行排序? - 堆栈内存溢出...
  9. IndexNotReadyException: Please change caller according to com.intellij.openapi.project.IndexNotReady
  10. 之江学院第0届 A qwb与支教 容斥与二分
  11. 非常漂亮的后台登录页面
  12. 将本地项目上传到github详解
  13. winrar linux 安装目录,rarLinux 安装及使用
  14. 锂电池容量电压对照表_商业化磷酸铁锂电池PK三元锂电池 谁更胜一筹?
  15. Mex HDU - 4747(递推, 思维)
  16. 最小公倍数用c语言,如何用C语言求最小公倍数。。。
  17. 高德地图的画图表,加文字,画线,测距
  18. 【PC工具】更新在线图片文字识别工具,OCR免费文字识别工具
  19. 圣斗士星矢重生服务器维护,11月26日全服停机维护公告
  20. iphone12android在线,【苹果iPhone12评测】安卓机吃尽高刷红利,为何iPhone 12还是缺席?(全文)_苹果 iPhone 12_手机评测-中关村在线...

热门文章

  1. matlab三体运动(仿真动画)
  2. 转自cnblogs 淘宝技术发展
  3. 论文阅读”Ada-nets: Face clustering via adaptive neighbour discovery in the structure space“
  4. 【Pandas】Pandas数据分析题
  5. Python初学者必看(1)
  6. iphone开发的几个Apple官方中文教程地址
  7. 计算机没有a3打印,打印机如何设置打印a3,打印机如何打印a3纸
  8. 阿里云盘诚邀“个人云种子用户”
  9. 【论文翻译】卷积神经网络研究综述
  10. 恳请李开复老师创建软件培训学院的一封信