专注十分钟,我们来搞定数组的正确食用姿势!

573 阅读9分钟

海阔凭鱼跃,天高任鸟飞。Hey 你好!我是秦爱德。😄

最近我和几个小伙伴组织了一个超神学霸联盟,约定好每周末做一次技术分享会(卷卷大会,目前已经进行到了第五期)。本篇文章来源于联盟小伙伴哈哥的分享😎 😎 😎

前言

数组作为一个最基础的一维数据结构,在各种编程语言中都充当着至关重要的角色,你很难想象没有数组的编程语言会是什么模样。特别是 JavaScript,它天生的灵活性,又进一步发挥了数组的特长,丰富了数组的使用场景。可以毫不夸张地说,不深入地了解数组,就不足以写好 JavaScript。

随着前端框架的不断演进,React 和 Vue 等 MVVM 框架的流行,数据更新的同时视图也会随之更新。在通过前端框架实现大量的业务代码中,开发者都会用数组来进行数据的存储和各种“增删改查”等操作,从而实现对应前端视图层的更新。可见熟练掌握数组各种方法,并深入了解数组是很有必要的。

那么,在开始前请你先思考几个问题。

  1. 数组的构造器有哪几种?

  2. 哪些是改变自身的方法?

  3. 哪些是不改变自身的方法?

  4. 遍历的方法有哪些?

数组概念的探究

截至 ES7 规范,数组共包含 33 个标准的 API 方法和一个非标准的 API 方法,使用场景和使用方案纷繁复杂,其中还有不少坑。为了方便你可以循序渐进地学习这部分的内容,下面我将从数组的概念开始讲起。

由于数组的 API 较多,很多相近的名字也容易导致混淆,所以在这里我按照“会改变自身值的”“不会改变自身值的”“遍历方法”这三种类型分开讲解,让你对这些 API 形成更结构化的认识。、、

Array的构造器

Array 构造器用于创建一个新的数组。通常,我们推荐使用对象字面量的方式创建一个数组,例如 var a = [] 就是一个比较好的写法。但是,总有对象字面量表述乏力的时候,比如,我想创建一个长度为 6 的空数组,用对象字面量的方式是无法创建的,因此只能写成下述代码这样。

//使用Array构造器,可以自定义长度
let a = Array(3); //[empty * 3]
//使用对象字面量
let b = [];
b.length = 3; //[undefined * 3]

Array 构造器根据参数长度的不同,有如下两种不同的处理方式:

new Array(arg1, arg2,…),参数长度为 0 或长度大于等于 2 时,传入的参数将按照顺序依次成为新数组的第 0 至第 N 项(参数长度为 0 时,返回空数组);

new Array(len),当 len 不是数值时,处理同上,返回一个只包含 len 元素一项的数组;当 len 为数值时,len 最大不能超过 32 位无符号整型,即需要小于 2 的 32 次方(len 最大为 Math.pow(2,32)),否则将抛出 RangeError

以上就是 Array 构造器的基本情况,我们来看下 ES6 新增的几个构造方法。

ES6新增的构造方法:Array.of和Array.from

Array.of

Array.of用于将参数依次转化为数组中的一项,然后返回这个新数组,而不管这个参数是数字还是其他。它基本上与 Array 构造器功能一致,唯一的区别就在单个数字参数的处理上。

Array.of(2.0); //[2]
Array(2.0); //[empty * 2]

Array.of(2.0, 3); //[2, 3]
Array(2.0, 3); //[2, 3]

Array.of('8'); //["8"]
Array('8'); //["8"]

Array.from

Array.from 的设计初衷是快速便捷地基于其他对象创建新数组,准确来说就是从一个类似数组的可迭代对象中创建一个新的数组实例。其实就是,只要一个对象有迭代器,Array.from 就能把它变成一个数组(注意:是返回新的数组,不改变原对象)。

从语法上看,Array.from 拥有 3 个参数:

  1. 类似数组的对象,必选;

  2. 加工函数,新生成的数组会经过该函数的加工再返回;

  3. this 作用域,表示加工函数执行时 this 的值。

let obj = {0: '热卤耙猪蹄', 1: '麻辣无骨耙鸭掌', 2: '冰镇啤酒', length: 3};
Array.from(obj, function(value, index){
	console.log(value, index, this, arguments.length)
  return value; //必须指定返回值,否则返回undefined
}, obj);

结果如下图所示:

image.png

这表明了通过 Array.from 这个方法可以自己定义加工函数的处理方式,从而返回想要得到的值;如果不确定返回值,则会返回 undefined,最终生成的也是一个包含若干个 undefined 元素的空数组。

实际上,如果这里不指定 this 的话,加工函数完全可以是一个箭头函数。上述代码可以简写为如下形式。

Array.from(obj, (value, index) => value)
// 控制台输出 ['热卤耙猪蹄', '麻辣无骨耙鸭掌', '冰镇啤酒']

除了上述 obj 对象以外,拥有迭代器的对象还包括 StringSetMap 等,Array.from 统统可以处理,请看下面的代码。

//String
Array.from('abc');   //["a", "b", "c"]

//Set
Array.from(new Set(['a', 'b']));   //["a", "b"]

//Map
Array.from(new Map([['a'], ['b']]));   //[["a"], ["b"]]

Array的判断

在 ES5 提供该方法之前,我们至少有如下 5 种方式去判断一个变量是否为数组。

let a = [];
// 1.基于instanceof
a.instanceof Array;
// 2.基于constructor
a.constructor === Array;
// 3.基于Object.prototype.isProtoTypeOf
Array.prototype.isProtoTypeOf(a);
// 4.基于getProtoTypeOf
Object.getProtoTypeOf(a) === Array.prototype
// 5.基于Object.prototype.toString
Object.prototype.toString(a) === '[object Array]'

上面这 5 个判断全部为 True

ES6 之后新增了一个 Array.isArray 方法,能直接判断数据类型是否为数组,但是如果 isArray 不存在,那么 Array.isArraypolyfill 通常可以这样写:

if(!Array.isArray) {
	Array.isArray = function(arg) {
  	return Object.prototype.toString.call(arg) === '[object Array]'
  }
}

数组的基本概念到这里基本讲得差不多了,下面我们就来看看让人眼花缭乱的 30 多个数组的基本方法。

改变自身的方法

基于 ES6,会改变自身值的方法一共有 9 个,分别为 poppushreverseshiftsortspliceunshift,以及两个 ES6 新增的方法 copyWithinfill

// pop方法

var array = ["cat", "dog", "cow", "chicken", "mouse"];

var item = array.pop();

console.log(array); // ["cat", "dog", "cow", "chicken"]

console.log(item); // mouse

// push方法

var array = ["football", "basketball",  "badminton"];

var i = array.push("golfball");

console.log(array); 

// ["football", "basketball", "badminton", "golfball"]

console.log(i); // 4

// reverse方法

var array = [1,2,3,4,5];

var array2 = array.reverse();

console.log(array); // [5,4,3,2,1]

console.log(array2===array); // true

// shift方法

var array = [1,2,3,4,5];

var item = array.shift();

console.log(array); // [2,3,4,5]

console.log(item); // 1

// unshift方法

var array = ["red", "green", "blue"];

var length = array.unshift("yellow");

console.log(array); // ["yellow", "red", "green", "blue"]

console.log(length); // 4

// sort方法

var array = ["apple","Boy","Cat","dog"];

var array2 = array.sort();

console.log(array); // ["Boy", "Cat", "apple", "dog"]

console.log(array2 == array); // true

// splice方法

var array = ["apple","boy"];

var splices = array.splice(1,1);

console.log(array); // ["apple"]

console.log(splices); // ["boy"]

// copyWithin方法

var array = [1,2,3,4,5]; 

var array2 = array.copyWithin(0,3);

console.log(array===array2,array2);  // true [4, 5, 3, 4, 5]

// fill方法

var array = [1,2,3,4,5];

var array2 = array.fill(10,0,3);

console.log(array===array2,array2); 

// true [10, 10, 10, 4, 5], 可见数组区间[0,3]的元素全部替换为10

不改变自身的方法

基于 ES6,不会改变自身的方法也有 9 个,分别为 concatjoinslicetoStringtoLocaleStringindexOflastIndexOf、未形成标准的 toSource,以及 ES7 新增的方法 includes

// concat方法

var array = [1, 2, 3];

var array2 = array.concat(4,[5,6],[7,8,9]);

console.log(array2); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

console.log(array); // [1, 2, 3], 可见原数组并未被修改

// join方法

var array = ['We', 'are', 'Chinese'];

console.log(array.join()); // "We,are,Chinese"

console.log(array.join('+')); // "We+are+Chinese"

// slice方法

var array = ["one", "two", "three","four", "five"];

console.log(array.slice()); // ["one", "two", "three","four", "five"]

console.log(array.slice(2,3)); // ["three"]

// toString方法

var array = ['Jan', 'Feb', 'Mar', 'Apr'];

var str = array.toString();

console.log(str); // Jan,Feb,Mar,Apr

// tolocalString方法

var array= [{name:'zz'}, 123, "abc", new Date()];

var str = array.toLocaleString();

console.log(str); // [object Object],123,abc,2016/1/5 下午1:06:23

// indexOf方法

var array = ['abc', 'def', 'ghi','123'];

console.log(array.indexOf('def')); // 1

// includes方法

var array = [-0, 1, 2];

console.log(array.includes(+0)); // true

console.log(array.includes(1)); // true

var array = [NaN];

console.log(array.includes(NaN)); // true

数组遍历的方法

基于 ES6,不会改变自身的遍历方法一共有 12 个,分别为 forEacheverysomefiltermapreducereduceRight,以及 ES6 新增的方法 entriesfindfindIndexkeysvalues

// forEach方法

var array = [1, 3, 5];

var obj = {name:'cc'};

var sReturn = array.forEach(function(value, index, array){

  array[index] = value;

  console.log(this.name); // cc被打印了三次, this指向obj

},obj);

console.log(array); // [1, 3, 5]

console.log(sReturn); // undefined, 可见返回值为undefined

// every方法

var o = {0:10, 1:8, 2:25, length:3};

var bool = Array.prototype.every.call(o,function(value, index, obj){

  return value >= 8;

},o);

console.log(bool); // true

// some方法

var array = [18, 9, 10, 35, 80];

var isExist = array.some(function(value, index, array){

  return value > 20;

});

console.log(isExist); // true 

// map 方法

var array = [18, 9, 10, 35, 80];

array.map(item => item + 1);

console.log(array);  // [19, 10, 11, 36, 81]

// filter 方法

var array = [18, 9, 10, 35, 80];

var array2 = array.filter(function(value, index, array){

  return value > 20;

});

console.log(array2); // [35, 80]

// reduce方法

var array = [1, 2, 3, 4];

var s = array.reduce(function(previousValue, value, index, array){

  return previousValue * value;

},1);

console.log(s); // 24

// ES6写法更加简洁

array.reduce((p, v) => p * v); // 24

// reduceRight方法 (和reduce的区别就是从后往前累计)

var array = [1, 2, 3, 4];

array.reduceRight((p, v) => p * v); // 24

// entries方法

var array = ["a", "b", "c"];

var iterator = array.entries();

console.log(iterator.next().value); // [0, "a"]

console.log(iterator.next().value); // [1, "b"]

console.log(iterator.next().value); // [2, "c"]

console.log(iterator.next().value); // undefined, 迭代器处于数组末尾时, 再迭代就会返回undefined

// find & findIndex方法

var array = [1, 3, 5, 7, 8, 9, 10];

function f(value, index, array){

  return value%2==0;     // 返回偶数

}

function f2(value, index, array){

  return value > 20;     // 返回大于20的数

}

console.log(array.find(f)); // 8

console.log(array.find(f2)); // undefined

console.log(array.findIndex(f)); // 4

console.log(array.findIndex(f2)); // -1

// keys方法

[...Array(10).keys()];     // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

[...new Array(10).keys()]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

// values方法

var array = ["abc", "xyz"];

var iterator = array.values();

console.log(iterator.next().value);//abc

console.log(iterator.next().value);//xyz

其中,要注意有些遍历方法不会返回处理之后的数组,比如 forEach;有些方法会返回处理之后的数组,比如 filter

image.png

以上,数组的各方法基本讲解完毕,这些方法之间存在很多共性,如下:

  • 所有插入元素的方法,比如 pushunshift 一律返回数组新的长度;
  • 所有删除元素的方法,比如 popshiftsplice 一律返回删除的元素,或者返回删除的多个元素组成的数组;
  • 部分遍历方法,比如 forEacheverysomefiltermapfindfindIndex,它们都包含 function(value,index,array){}thisArg 这样两个形参。

感谢

欢迎关注我的个人公众号前端秦爱德每天给你推送新鲜的优质好文。回复 “福利” 即可获得我精心准备的前端知识大礼包。愿你一路前行,眼里有光!

感兴趣的小伙伴还可以加我微信:webqinaid一起交流前端技术,一起玩耍!

往期推荐

【真香】我开发了一款提醒各位 [ 倔友 ] 喝水的 [ chrome插件 ]

来!和秦爱德一起聊聊工作中遇到的那些“问题”

一个老生常谈的问题,什么是面向对象?