转自:https://gitee.com/hongjilin/hongs-study-notes/tree/master
数组的拓展
对于前端而言,数组的操作是最频繁的,因为从服务端获取到的基本都是数组格式数据,其中方法最好是认真掌握
文章目录
- 数组的拓展
- Ⅰ- 概括与总结
- Ⅱ - 扩展运算符
- Ⅲ - Array.from()
- Ⅳ- Array.of ( )
- Ⅴ- 数组的实例方法
- Ⅵ - 数组的空位
- Ⅶ - Array.prototype.sort() 的排序稳定性
Ⅰ- 概括与总结
新增的拓展
- 扩展运算符(…): 转换数组为用逗号分隔的参数序列(
[...arr]
, 相当于rest/spread参数
的逆运算)- Array.from(): 转换具有 [ Iterator接口 ] 的数据结构为真正数组, 返回新数组
- 类数组对象:
包含length的对象
、Arguments对象
、NodeList对象
- 可遍历对象:
String
、Set结构
、Map结构
、Generator函数
- Array.of(): 转换一组值为真正数组, 返回新数组
- 实例方法
- copyWithin(): 把指定位置的成员复制到其他位置, 返回原数组
- find(): 返回第一个符合条件的成员
- findIndex(): 返回第一个符合条件的成员索引值
- fill(): 根据指定值填充整个数组, 返回原数组
- keys(): 返回以索引值为遍历器的对象
- values(): 返回以属性值为遍历器的对象
- entries(): 返回以索引值和属性值为遍历器的对象
- 其他:毕竟只是概述,不过多列举,详细看下方
- 其他常用方法:此处将数组常用方法在下方详细部分列出 (不仅是ES6)
- 数组空位: ES6明确将数组空位转为 undefined (空位处理规不一, 建议避免出现)
扩展运算符在数组中的应用
- 克隆数组:
const arr = [...arr1]
- 合并数组:
const arr = [...arr1, ...arr2]
- 拼接数组:
arr.push(...arr1)
- 代替apply:
Math.max.apply(null, [x, y])
=>Math.max(...[x, y])
- 转换字符串为数组:
[..."hello"]
- 转换类数组对象为数组:
[...Arguments, ...NodeList]
- 转换可遍历对象为数组:
[...String, ...Set, ...Map, ...Generator]
- 与数组解构赋值结合:
const [x, ...rest/spread] = [1, 2, 3]
- 计算Unicode字符长度:
Array.from("hello").length
=>[..."hello"].length
重点难点
- 使用[ keys() ]、[ values() ]、[ entries() ]返回的遍历器对象, 可用 [ for-of ] 自动遍历或
next()
手动遍历
Ⅱ - 扩展运算符
① 含义
扩展运算符(spread)是三个点(
...
). 它好比rest 参数的逆运算
, 将一个数组转为用逗号分隔的参数序列.console.log(...[1, 2, 3]) // 1 2 3 console.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
上面代码中,
array.push(...items)
和add(...numbers)
这两行, 都是函数的调用, 它们都使用了扩展运算符. 该运算符将一个数组, 变为参数序列.扩展运算符与正常的函数参数可以结合使用, 非常灵活.
function f(v, w, x, y, z) { } const args = [0, 1]; f(-1, ...args, 2, ...[3]);
扩展运算符后面还可以放置表达式.
const arr = [ ...(x > 0 ? ['a'] : []), 'b', ];
如果扩展运算符后面是一个空数组, 则不产生任何效果.
[...[], 1] // [1]
注意,
只有函数调用时, 扩展运算符才可以放在圆括号中
, 否则会报错.(...[1, 2]) // Uncaught SyntaxError: Unexpected number console.log((...[1, 2])) // Uncaught SyntaxError: Unexpected number console.log(...[1, 2]) // 1 2
上面三种情况, 扩展运算符都放在圆括号里面, 但是前两种情况会报错, 因为扩展运算符所在的括号不是函数调用.
② 替代函数的 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 写法中,
push
方法的参数不能是数组, 所以只好通过apply
方法变通使用push
方法. 有了扩展运算符, 就可以直接将数组传入push
方法.下面是另外一个栗子.
// ES5 new (Date.bind.apply(Date, [null, 2015, 1, 1])) // ES6 new Date(...[2015, 1, 1]);
③ 扩展运算符的应用
a) 复制数组
数组是复合的数据类型, 直接复制的话, 只是复制了指向底层数据结构的指针, 而不是克隆一个全新的数组 [
浅拷贝
].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(); a2[0] = 2; a1 // [1, 2]
上面代码中,
a1
会返回原数组的克隆, 再修改a2
就不会对a1
产生影响.扩展运算符提供了复制数组的简便写法. -->这样就不会造成影响
const a1 = [1, 2]; // 写法一 const a2 = [...a1]; // 写法二 const [...a2] = a1;
上面的两种写法,
a2
都是a1
的克隆.
注意
:如果内部是引用数据类型,是不会改动到内部的引用,不懂的继续看下面 [ 合并数组 ] 的举例
b) 合并数组
扩展运算符提供了数组合并的新写法.
const arr1 = ['a', 'b']; const arr2 = ['c']; const arr3 = ['d', 'e']; // ES5 的合并数组 arr1.concat(arr2, arr3); // [ 'a', 'b', 'c', 'd', 'e' ] // ES6 的合并数组 [...arr1, ...arr2, ...arr3] // [ 'a', 'b', 'c', 'd', 'e' ]
不过, 这两种方法都是浅拷贝 ( 指的是内部数据如 { foo: 1 } 是存地址 ) , 使用的时候需要注意.
const a1 = [{ foo: 1 }]; const a2 = [{ bar: 2 }]; const a3 = a1.concat(a2); const a4 = [...a1, ...a2]; a3[0] === a1[0] // true a4[0] === a1[0] // true
上面代码中,
[ a3 ] 和 [ a4 ] 是用两种不同方法合并而成的新数组, 但是它们的成员都是对原数组成员的引用, 这就是浅拷贝
. 如果修改了引用指向的值, 会同步反映到新数组.
c) 与解构赋值结合
扩展运算符可以与解构赋值结合起来, 用于生成数组.
// ES5 a = list[0], rest = list.slice(1) //此处是先取出第一个,然后从下标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]; // 报错
d) 字符串
扩展运算符还可以将字符串转为真正的数组.
[...'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
操作就不正确.
e) 实现了 Iterator 接口的对象
任何定义了遍历器(Iterator)接口的对象 [此处不懂可以跳过先看下方,有给出详情], 都可以用扩展运算符转为真正的数组.
let nodeList = document.querySelectorAll('div'); let array = [...nodeList];
上面代码中,
querySelectorAll
方法返回的是一个NodeList
对象. 它不是数组, 而是一个类似数组的对象. 这时, 扩展运算符可以将其转为真正的数组, 原因就在于NodeList
对象实现了 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
转为真正的数组.
<<<<<<< HEAD
f) Map 和 Set 结构,Generator 函数
扩展运算符内部调用的是数据结构的 Iterator 接口, 因此只要具有 Iterator 接口的对象, 都可以使用扩展运算符, 比如 Map 结构.
let map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); let arr = [...map.keys()]; // [1, 2, 3]
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
=======
f) Map 和 Set 结构,Generator 函数
扩展运算符内部调用的是数据结构的 Iterator 接口, 因此只要具有 Iterator 接口的对象, 都可以使用扩展运算符, 比如 Map 结构.
let map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); let arr = [...map.keys()]; // [1, 2, 3]
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
Ⅲ - Array.from()
对于还没有部署该方法的浏览器, 可以用
Array.prototype.slice
方法替代.
① 简单举例
Array.from
方法用于将两类对象转为真正的数组: 类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map).下面是一个类似数组的对象,
Array.from
将它转为真正的数组.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']
② 实际应用场景举栗
实际应用中, 常见的类似数组的对象是 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); // ... }
上面代码中,
querySelectorAll
方法返回的是一个类似数组的对象, 可以将这个对象转为真正的数组, 再使用filter
方法.只要是部署了 Iterator 接口的数据结构,
Array.from
都能将其转为数组.Array.from('hello') // ['h', 'e', 'l', 'l', 'o'] let namesSet = new Set(['a', 'b']) Array.from(namesSet) // ['a', 'b']
上面代码中, 字符串和 Set 结构都具有 Iterator 接口, 因此可以被
Array.from
转为真正的数组.如果参数是一个真正的数组,
Array.from
会返回一个一模一样的新数组.Array.from([1, 2, 3]) // [1, 2, 3]
值得提醒的是, 扩展运算符(
...
)也可以将某些数据结构转为数组 (上面有提到).// arguments对象 function foo() { const args = [...arguments]; } // NodeList对象 [...document.querySelectorAll('div')]
③ 不适用场景
扩展运算符背后调用的是遍历器接口(
Symbol.iterator
), 如果一个对象没有部署这个接口, 就无法转换.
Array.from
方法还支持类似数组的对象. 所谓类似数组的对象, 本质特征只有一点, 即必须有length
属性. 因此, 任何有length
属性的对象, 都可以通过Array.from
方法转为数组, 而此时扩展运算符就无法转换.Array.from({ length: 3 }); // [ undefined, undefined, undefined ]
上面代码中,
Array.from
返回了一个具有三个成员的数组, 每个位置的值都是 undefined . 扩展运算符转换不了这个对象.对于还没有部署该方法的浏览器, 可以用
Array.prototype.slice
方法替代.const toArray = (() => Array.from ? Array.from : obj => [].slice.call(obj) )();
④ 第二个参数的作用
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
的第一个参数指定了第二个参数运行的次数. 这种特性可以让该方法的用法变得非常灵活.
Array.from()
的另一个应用是: 将字符串转为数组, 然后返回字符串的长度. 因为它能正确处理各种 Unicode 字符, 可以避免 JavaScript 将大于\uFFFF
的 Unicode 字符, 算作两个字符的 bug.function countSymbols(string) { return Array.from(string).length; }
Ⅳ- 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 ]总是返回参数值组成的数组. 如果没有参数, 就返回一个空数组.
② 原生模拟 [ Array.of ]
Array.of
方法可以用下面的代码模拟实现.function ArrayOf(){ return [].slice.call(arguments); }
Ⅴ- 数组的实例方法
所谓实例方法,简单来说就是实例化后可以用
[数组].方法名()
的方式调用的一类方法,其中有几个很常用,可以重点理解
① 数组实例的 copyWithin()
数组实例的 [ copyWithin() ] 方法, 在当前数组内部, 将指定位置的成员复制到其他位置(会覆盖原有成员), 然后返回当前数组. 也就是说, 使用这个方法,
会修改当前数组
.Array.prototype.copyWithin(target, start = 0, end = this.length)
它接受三个参数.
- target(必需): 从该位置开始替换数据. 如果为负值, 表示倒数.
- start(可选): 从该位置开始读取数据, 默认为 0. 如果为负值, 表示从末尾开始计算.
- end(可选): 到该位置前停止读取数据, 默认等于数组长度. 如果为负值, 表示从末尾开始计算.
这三个参数都应该是数值, 如果不是, 会自动转为数值.
[1, 2, 3, 4, 5].copyWithin(0, 3) // [4, 5, 3, 4, 5]
上面代码表示将从 3 号位直到数组结束的成员(4 和 5), 复制到从 0 号位开始的位置, 结果覆盖了原来的 1 和 2.
下面是更多栗子.
// 将3号位复制到0号位 [1, 2, 3, 4, 5].copyWithin(0, 3, 4) //从三号位开始读取,到四号位结束,得到[4],将其替换到0号位 // [4, 2, 3, 4, 5] // -2相当于3号位, -1相当于4号位 [1, 2, 3, 4, 5].copyWithin(0, -2, -1) //从倒数2号位开始读取,到倒数一号位结束,得到[4],将其替换到0号位 // [4, 2, 3, 4, 5] // 将3号位复制到0号位 [].copyWithin.call({length: 5, 3: 1}, 0, 3) // {0: 1, 3: 1, length: 5} // 将2号位到数组结束, 复制到0号位 let i32a = new Int32Array([1, 2, 3, 4, 5]); i32a.copyWithin(0, 2); // Int32Array [3, 4, 5, 4, 5] // 对于没有部署 TypedArray 的 copyWithin 方法的平台 // 需要采用下面的写法 [].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4); // Int32Array [4, 2, 3, 4, 5]
② 数组实例的 find() 和 findIndex()
数组实例的 [ find ] 方法, 用于找出第一个符合条件的数组成员. 它的参数是一个回调函数, 所有数组成员依次执行该回调函数, 直到找出第一个返回值为
true
的成员, 然后返回该成员. 如果没有符合条件的成员, 则返回 undefined .[1, 4, -5, 10].find((n) => n < 0) // -5
上面代码找出数组中第一个小于 0 的成员.
[1, 5, 10, 15].find(function(value, index, arr) { return value > 9; }) // 10
上面代码中, [ find ] 方法的回调函数可以接受三个参数, 依次为
当前的值、当前的位置和原数组
.数组实例的 [ findIndex] 方法的用法与 [ find ] 方法非常类似, 返回第一个符合条件的数组成员的位置, 如果所有成员都不符合条件, 则返回
-1
.[1, 5, 10, 15].findIndex(function(value, index, arr) { return value > 9; }) // 2
这两个方法都可以接受第二个参数, 用来绑定回调函数的
this
对象.function f(v){ return v > this.age; } let person = {name: 'John', age: 20}; [10, 12, 26, 15].find(f, person); // 26
上面的代码中, [ find ] 函数接收了第二个参数
person
对象, 回调函数中的this
对象指向person
对象.另外, 这两个方法都可以发现
NaN
, 弥补了数组的 [ indexOf ] 方法的不足.[NaN].indexOf(NaN) // -1 [NaN].findIndex(y => Object.is(NaN, y)) // 0
上面代码中, [ indexOf ] 方法无法识别数组的
NaN
成员, 但是 [ findIndex] 方法可以借助 [ Object.is ] 方法做到.
③ 数组实例的 entries(),keys() 和 values()
ES6 提供三个新的方法——[ entries() ], [ keys() ] 和 [ values() ]——用于遍历数组. 它们都返回一个遍历器对象.可以用
for...of
循环进行遍历, 唯一的区别是[ keys() ]是对键名的遍历、[ values() ]是对键值的遍历, [ entries() ]是对键值对的遍历.for (let index of ['a', 'b'].keys()) { console.log(index);} // 0 // 1 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"
如果不使用
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']
④ 数组实例的 includes()
[ Array.prototype.includes ] 方法返回一个布尔值, 表示某个数组是否包含给定的值, 与字符串的 [ includes ] 方法类似. ES2016 引入了该方法.
[1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true
该方法的第二个参数表示搜索的起始位置, 默认为
0
. 如果第二个参数为负数, 则表示倒数的位置, 如果这时它大于数组长度(比如第二个参数为-4
, 但数组长度为3
), 则会重置为从0
开始.[1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true
没有该方法之前, 我们通常使用数组的 [ indexOf ] 方法, 检查是否包含某个值.
if (arr.indexOf(el) !== -1) { // ... }
[ indexOf ] 方法有两个缺点, 一是不够语义化, 它的含义是找到参数值的第一个出现位置, 所以要去比较是否不等于
-1
, 表达起来不够直观. 二是, 它内部使用严格相等运算符(===
)进行判断, 这会导致对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) ] .
⑤ 数组实例的 flat(),flatMap()
数组的成员有时还是数组,
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]
[ 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
.
⑥ 数组实例的 filter() -->常用
此方法非常常用
,一定要掌握的
filter()
方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素. 不会改变原有数组
a) 筛选对象数组中符合条件的
const Arr = [ { look: '帅', name: '@hongjilin'}, { look: '很帅', name: '努力学习的汪'} ] console.log(Arr.filter(item => item.name === '努力学习的汪' )) //{ look: '很帅', name: '努力学习的汪' }
b) 筛选对象数组中不符合条件的
同样操作上面的数组
console.log(Arr.filter(item => item.look !== '很帅' )) //{ look: '帅', name: '@hongjilin'}
c) 去除数组中的空字符串、undefined、null
const undefinedArr = ['这是undefined数组','2',undefined, '努力学习的汪',undefined] const nullArr = ['这是null数组','2',null, '努力学习的汪',null] const stringArr = ['这是空字符串数组','2','', '努力学习的汪',''] //空字符串里面不能包含空格 let newArr =[] //定义一个新数组来测试承接 //过滤 undefined newArr= undefinedArr.filter(item => item) console.log(newArr) //log: ["这是undefined数组", "2", "努力学习的汪"] //过滤 null newArr = nullArr.filter(item => item) console.log(newArr) //log: ["这是null数组", "2", "努力学习的汪"] //过滤空字符串 newArr = stringArr.filter(item => item) console.log(newArr) //log: ["这是空字符串数组", "2", "努力学习的汪"]
d) 筛选字符串、数字数组符合条件项
其实与上方对象数组筛选差不多,但稍微还是有所差别,举例出来,方便理解
const numberArr = [20,30,50, 96,50] const stringArr = ['10','12','23','44','42'] let newArr = [] //筛选数组中符合条件项 newArr= numberArr.filter(item => item>40) console.log(newArr) //log: [50, 96, 50] //过滤字符串数组符合条件项 //item.indexOf('2')是查找字符串中含有['2']的下标,当不含有时,返回-1 newArr = stringArr.filter(item => item.indexOf('2')<0) console.log(newArr) //log: ["10", "44"]
e) 数组去重
可以利用 [ filter ] 方法实现去重,当然去重方式非常多,这里也是一种思路
const arr = [1, 2, 2, 3, 4, 5, 5, 6, 7, 7,8,8,0,8,6,3,4,56,2]; let arr2 = arr.filter((x, index,self)=>self.indexOf(x)===index) console.log(arr2); //[1, 2, 3, 4, 5, 6, 7, 8, 0, 56]
这里列一个ES6提供的去重新方法
//具体详情在下方 [Set] 相关知识点笔记中会给出 const arr=[1,2,1,'1',null,null,undefined,undefined,NaN,NaN] let res=Array.from(new Set(arr));//{1,2,"1",null,undefined,NaN} //or let newarr=[...new Set(arr)]
⑦ 数组实例的 map() -->常用
定义: 对数组中的每个元素进行处理, 得到新的数组;
特点: 不改变原有数据的结构和数据
a) 常用方法举例
const array = [1, 3, 6, 9]; const newArray = array.map( value => value + 1 ); //此处用的箭头函数写法,看不懂的要回头看前方函数部分 console.log(newArray); //log: [2, 4, 7, 10] console.log(array); //log: [1, 3, 6, 9]
b) 类似方法
类似效果实现方法: for in , for , foreach
const array = [1, 3, 6, 9]; const newArray2 = []; for (var i in array) { newArray2.push(array[i] + 1)} const newArray3 = []; for (var i = 0; i < array.length; i++) { newArray3.push(array[i] + 1)} const newArray4 = []; array.forEach(function (key) { newArray4.push(key * key)}) console.log(newArray2); //log: [2, 4, 7, 10] console.log(newArray3); //log: [2, 4, 7, 10] console.log(newArray4); //log: [1, 9, 36, 81] console.log(array); //log: [1, 3, 6, 9]
与上述方法的区别:
- .map()方法使用return,进行回调;其他方法可不需要.
- .map()方法直接对数组的每个元素进行操作, 返回相同数组长度的数组;其他方法可扩展数组的长度.
- .map() 不会对空数组进行检测.
c) 与 filter() 区别
[ filter() ] 主要用作筛选,并不会对数组中元素进行处理,只会根据匹配条件返回数组中符合条件元素;
[ map() ] 常用作将符合条件的元素进行加工,再返回出去的场景
⑧ 数组实例的 reduce() -->常用
reduce()方法可以搞定的东西,for循环,或者forEach方法有时候也可以搞定,那为啥要用reduce()?
这个问题,之前我也想过,要说原因还真找不到,但我觉得是:通往成功的道路有很多,但是总有一条路是最捷径的,亦或许reduce()逼格更高…
a) 语法
arr.reduce(callback,[initialValue])
reduce
为数组中的而每一个元素一次执行回调函数,不包括数组中被删除或从未被赋值的元素,接受四个参数: 初始值(或上一次回调函数的返回值)、当前元素值、当前索引、调用reduce的数组
- callback(执行数组中每个值的函数,包括四个参数)
previousValue
: 上一次调用回调返回的值,如果是第一次则为提供的初始值(initialValue)currentValue
: 数组中当前被处理的元素index
: 当前元素在数组中的索引array
: 调用reduce的数组
- initialValue(作为第一次调用的第一个参数)
b) 实例解析initialValue
先说得出的结论:
- 如果没有提供
initialValue(初始值)
,reduce会从索引1的地方开始执行callback方法,跳过第一个索引.如果提供initialValue
,则从索引0开始- 一般来说要写上初始值更安全,否则空数组会出现报错
(1) 举个栗子1:
const arr = [1,2,3,4]; const sum = arr.reduce(function(prev,cur,index,arr){ console.log(prev,cur,index) // 每次对数据进行累加 return prev+cur }) console.log(arr, sum);
我们看到,
index
(打印结果中第三位)是从**1
**开始的,第一次的prev的值是数组的第一个值.数组长度是4,但是reduce函数循环3次[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cr2bQz8i-1655717434478)(ES全系列详细学习笔记中的图片/image-20220115142512156.png)]
(2) 接着看栗子2:
本质上就是加了第二个参数,赋予其默认值
//本质上就是加了第二个参数,赋予其默认值 const arr = [1,2,3,4]; const sum = arr.reduce(function(prev,cur,index,arr){ console.log(prev,cur,index) // 每次对数据进行累加 return prev+cur },0) console.log(arr, sum);
我们可以看到:
index
是从0开始的,第一次的eprev的值是我们设置的初始值0,数组长度是4,reduce循环4次[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GO31ceXr-1655717434492)(ES全系列详细学习笔记中的图片/image-20220115143008922.png)]
(3) 结论1:
如果没有提供
initialValue(初始值)
,reduce会从索引1的地方开始执行callback方法,跳过第一个索引.如果提供initialValue
,则从索引0开始那么有同学可能会问了:既然没给初始值,他会从索引1开始,那么如果我遍历的数组为空,那他不会报错吗?那就引出了下面的栗子3,别急我们继续往下看
(4) 栗子3:如果数组为空?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mUXoB43S-1655717434495)(ES全系列详细学习笔记中的图片/image-20220115144013667.png)]
所以一般来说我们提供初始值更安全
c) reduce的简单用法
最简单的就是我们常用的数组求和、乘积
const arr = [1, 2, 3, 4]; const sum = arr.reduce((x,y)=>x+y) const mul = arr.reduce((x,y)=>x*y) console.log( sum ); //求和,10 console.log( mul ); //求乘积,24
d) reduce的高级用法
(1) 计算数组中每个元素出现的次数
const names = ['Jelyn', '努力学习的汪', 'hong', '努力学习的汪', '努力学习的汪','Jelyn']; const nameInfo = names.reduce((pre,cur)=>{ if(cur in pre){ //如果当前项,是pre对象的属性key,则将其value+1 pre[cur]++ }else{ //如果当前项不存在对象key中,则将此项作为其对象key,且给定初始值1 pre[cur] = 1 } return pre },{})//给定初始值空对象 console.log(nameInfo);//{Jelyn: 2, 努力学习的汪: 3, hong: 1}
(2) 数组去重
includes()
:用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false。concat()
: 用于连接两个或多个数组;且不会更改现有数组,而是返回一个新数组,其中包含已连接数组的值。const names = ['Jelyn', '努力学习的汪', 'hong', '努力学习的汪', '努力学习的汪','Jelyn']; let newArr = names.reduce((pre,cur)=>{ //如果 当前项不存在于 pre中 if(!pre.includes(cur)){ //则将 当前项并入 pre数组中 return pre.concat(cur) // 也可使用push //pre.push(cur); return pre ; }else{ //如果存在于pre中,则不并入,将pre原样返回,进入下次循环 return pre } },[]) console.log(newArr);//['Jelyn', '努力学习的汪', 'hong']
(3) 将二维数组转化成一维
首先,最简单的方法是使用上面讲过的
flat()
方法,他能拉平一层数组,但这里再写个使用reduce实现的栗子[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OMd3Akdv-1655717434501)(ES全系列详细学习笔记中的图片/image-20220115155344679.png)]
(4) 将多维转换成一维
与上面的一样,可以用
flat
实现,所以遇到这种情况还是用flat好[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vsm9lBGG-1655717434504)(ES全系列详细学习笔记中的图片/image-20220115155702841.png)]
(5) 对象数组去重 -->常用
const obj = {} const arr = [ { id: '17011300', name: '努力学习的汪', age: '18' }, { id: '170113001', name: 'Jelyn', age: '18' }, { id: '17011300', name: '努力学习的汪', age: '18' }, { id: '17011300', name: '努力学习的汪', age: '18' }, { id: '999', name: 'hongjilin', age: '28' } ] const temp = arr.reduce(function (item, next) { if (obj[next.id] && next.id) { return item } else obj[next.id] = true && item.push(next) return item }, []) console.log(temp)
解析:
- 当首次进入时,必定为空,这时就进入了else中,在此处给这个属性加上一个value,同时将这个next存入数组中
- 当后续进入时,如果 id重复,则在上轮中已经给他赋值了true,所以此处就判断到重复值,此时将item直接抛出
- 当后续进入时,如果 id 不重复,就重复第一步
&& next.id
如果加了,则不会筛选没有 id 的数据,如果去除,则会过滤[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gh0HDxpa-1655717434506)(ES全系列详细学习笔记中的图片/image-20220115160957448.png)]
⑨ 数组实例的 some() 、every()
some() : 方法测试数组中是不是至少有1个元素通过了被提供的函数测试. 它返回的是一个Boolean类型的值.
every(): 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试. 它返回一个布尔值.
Ⅵ - 数组的空位
数组的空位指, 数组的某一个位置没有任何值. 比如,
Array
构造函数返回的数组都是空位.Array(3) // [, , ,]
上面代码中,
Array(3)
返回一个具有 3 个空位的数组.注意, 空位不是undefined, 一个位置的值等于undefined, 依然是有值的.
空位是没有任何值
, [ in ]运算符可以说明这一点.0 in [undefined, undefined, undefined] // true 0 in [, , ,] // false
上面代码说明, 第一个数组的 0 号位置是有值的, 第二个数组的 0 号位置没有值.
ES5 对空位的处理, 已经很不一致了, 大多数情况下会忽略空位.
- forEach(), filter(), reduce(), every() 和 some() 都会跳过空位.
- map() 会跳过空位, 但会保留这个值
- join() 和 toString() 会将空位视为 undefined , 而 undefined 和 null会被处理成空字符串.
// 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,,"
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()
会将空位视为正常的数组位置.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
由于空位的处理规则非常不统一, 所以建议避免出现空位.
Ⅶ - 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
按照首字母进行排序. 排序结果中,straw
在spork
的前面, 跟原始顺序一致, 所以排序算法stableSorting
是稳定排序.const unstableSorting = (s1, s2) => { if (s1[0] <= s2[0]) return -1; return 1; }; arr.sort(unstableSorting) // ["apple", "peach", "spork", "straw"]
上面代码中, 排序结果是
spork
在straw
前面, 跟原始顺序相反, 所以排序算法unstableSorting
是不稳定的.常见的排序算法之中, 插入排序、合并排序、冒泡排序等都是稳定的, 堆排序、快速排序等是不稳定的. 不稳定排序的主要缺点是, 多重排序时可能会产生问题. 假设有一个姓和名的列表, 要求按照“姓氏为主要关键字, 名字为次要关键字”进行排序. 开发者可能会先按名字排序, 再按姓氏进行排序. 如果排序算法是稳定的, 这样就可以达到“先姓氏, 后名字”的排序效果. 如果是不稳定的, 就不行.
早先的 ECMAScript 没有规定,
Array.prototype.sort()
的默认排序算法是否稳定, 留给浏览器自己决定, 这导致某些实现是不稳定的. ES2019 明确规定,Array.prototype.sort()
的默认排序算法必须稳定. 这个规定已经做到了, 现在 JavaScript 各个主要实现的默认排序算法都是稳定的.