重学JavaScript 篇的目的是回顾基础,方便学习框架和源码的时候可以快速定位知识点,查漏补缺,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
Array
是ECMAScript中最常用的类型之一,和其他编程语言不同的是,ECMAScript里的数组每个槽位可以存储任意类型的数据。
创建
一般来说,创建数组的方式为 Array构造函数 和 数组字面量。
构造函数
使用构造函数创建一个数组:
let array = new Array();
上例初始化了一个数组,并且array的长度为0,如果要声明一个长度为10的数组,写法如下:
let array = new Array(10);
当然也可以给Array构造函数传入要保存的元素,比如:
let array = new Array(12, "abc");
这里要注意一下:如果在创建数组的时候给一个值,值为数字,则会创建一个长度为该数值的数组,否则就创建一个只包含该特定值的数组:
let array1 = new Array(3); // 创建一个包含3个元素的数组 let array2 - new Array("abc"); // 创建一个长度为1的数组,内容是“abc”
也可以省略new操作符来声明一个数组:
let array = Array(3);
数组字面量
数组字面量的意思是在中括号中包含以逗号分隔的元素列表:
let array1 = ["a", "b", "c"]; let array2 = [1, 2, 3];
from() 和 of()
在ES6中,多了两个创建数组的静态方法:from() 和 of(),from()用于将类数组结构转换为数组,of()用于将数组参数转换为数组实例。
from的参数有三个:
- arrayLike:想要转换成数组的伪数组对象或可迭代对象。
- mapFn:如果指定了该参数,新数组中的每个元素会执行该回调函数。
- thisArg:可选参数,执行回调函数
mapFn
时this
对象。
Array.from()
方法有一个可选参数 mapFn
,让你可以在最后生成的数组上再执行一次 map
方法后再返回。也就是说 Array.from(obj, mapFn, thisArg)
就相当于 Array.from(obj).map(mapFn, thisArg),
除非创建的不是可用的中间数组。
// 从String生成数组 Array.from("foo"); // ["f", "o", "o"] //从Set生成数组 Array.from(new Set(["foo", "bar", "foo"])); // ["foo", "bar"] //从Map生成数组 Array.from(new Map([[1, 2], [2, 4], [4, 8]])); // [[1, 2], [2, 4], [4, 8]] //从类数组对象(arguments)生成数组 function f(){ return Array.from(argumemts); } f(1, 2, 3) // [1, 2, 3] //使用第二个参数 Array.from([1, 2, 3], x => x**2); // [1, 4, 9]
of方法用于替代之前常用的 Array.prototype.slice.call(arguments),它生成的数组的值就是传入的参数:
Array.of(); // [] Array.of(5); // [5]
数组空位
使用数组字面量创建时,可以使用一串逗号来创建空位:
const options = [,,,,,]; //创建包含5个元素的数组 option.length // 5 option // [,,,,,]
在ES6中,将这些空位当成存在的元素,值为undefined:
const options = [1,,,,5]; for(const option of options){ console.log(option === undefined) }; //false true true true false
数组索引
获取数组的值是通过索引来获得的,超过数组长度的索引将返回undefined:
let array = [1, 2, "a"]; array[2]; // "a" array[5]; // undefined
这里的数组length特殊在于:可以添加和删除元素
let array = [1, 2 , "a"]; array[array.length] = "b"; //[1, 2, "a", "b"]; array.length = 1; // [1]
检测数组
一个经典的ECMAScript问题就是判断一个对象是不是数组,在只有一个全局作用域的情况下,直接使用 instanceof 操作符就可以了:
if(value instanceof Array){ //do something }
但是有一种情况,如果在网页里有多个框架,并且有多个不同的全局执行上下文的话,就会有两个不同版本的 Array 构造函数。为了解决这一问题,新增了 Array.isArray() 方法,该方法的目的就是确定一个值是否为数组,而不用管它在哪个全局执行上下文中创建的:
if(Array.isArray(value)){ // do something }
迭代器
在ES6中,Array 的原型上暴露了3个用于检索数组内容的方法:keys(), values() 和 entries()。
let a = ["foo", "bar"]; Array.from(a.keys()); // [0, 1] Array.from(a.values()); // ["foo", "bar"] Array.from(a.entries()); // [[0, "foo"], [1, "bar"]]
复制和填充
在ES6中,新增了两个方法:copyWithin() 和 fill()。
fill()方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素,不包括终止索引。fill()方法接收三个参数:value,start,end。
const array = [0, 0, 0]; // 用5填充整个数组 array.fill(5); // [5, 5, 5] //用6填充索引>=2的元素 array.fill(6, 3); // [0, 0, 6] //用7填充索引>=1且<2的元素 array.fill(7, 1, 2); // [0, 7, 0]
copyWithin()方法和它不同的是,copyWithin会按照指定范围浅复制数组中的内容,然后将他们插入到指定索引开始的位置,两个索引和fill一样:
const array = [1, 2, 3]; //复制索引0开始的内容,插入到索引2开始的位置 array.copyWithin(2); // [1, 2, 1] //复制索引1开始的内容,插入到索引0开始的位置 array.copyWithin(0, 1); // [2, 2, 3] //复制索引1到索引2开始的内容,插入到索引0开始的位置 array.copyWithin(0, 1, 2); // [2, 2, 3]
栈和队列
栈
ECMAScript中数组可以像栈一样,是一种后进先出(LIFO)的结构,数据项的插入和删除只在栈的一个地方发生,ECMAScript提供了 push() 和 pop() 方法,以实现类似栈的行为。
push可以接受单个或者多个参数,返回新数组长度:
let array = new Array(); array.push("a"); // ["a"] array.push("b", "c"); // ["a", "b", "c"]
pop方法不接受参数,删除最后一项,返回删除的值:
let array = new Array("a", "b"); array.pop(); // ["a"]
队列
队列以先进先出(FIFO)进行,类似的栈操作push和pop有了,自然有他们的反向方法:unshift() 和shift()。
unshift可以接受单个或者多个参数,返回新数组长度:
let array = new Array("a", "b"); array.unshift("c", "d"); // ["c", "d", "a", "b"]
shift不接收参数,删除第一个元素,返回删除的值:
let array = new Array(1, 2, 3); array.shift(); // 1
排序
数组有两个方法可以用来对元素重新排序:reverse() 和 sort()。
reverse用来翻转数组,也就是反向排列:
let value = [1, 2 ,3]; value.reverse(); // [3, 2, 1]
sort用来对数组元素进行大小排序(从小到大),它会先调用String(),将内容转为字符串,再排序:
let value = [5, 2 ,3]; value.sort(); // [2, 3, 5]
这里有个小坑,字符串10是小于字符串5的,所以直接使用sort会排序错误,所以最好自己封装一个方法,类似于:
function compare(v1, v2){ if(v1 < v2){ return -1; } else if(v1 > v2){ return 1; } else { return 0; } } let value = ["5", "10", 500, 50]; value.sort(compare); // ["10", "5", 50, 500]
操作
concat()
concat可以在现有数组全部元素基础上创建一个新数组。他首先会创建一个当前数组的副本,然后将参数添加到副本末尾,最后返回新构建的数组:
let a1 = [1, 2, 3]; a1.concat("a", ["b", "c"]); // [1, 2, 3, "a", "b", "c"]
slice()
slice切割一个原有数组中某些元素,并返回一个新数组,接收一个或者两个参数:开始和结束索引。如果只有一个,就截取到最后,如果有两个,就截取开始到第二个参数(不包含第二个):
let a1 = [1, 2, 3]; a1.slice(1); // [2, 3]; a1.slice(1, 2); [2]
splice()
splice应该是最强大的了,有三个参数,第一个元素为索引,第二个元素为数量,第三个及后面的元素为新的值,可以有多个,返回的是操作了的值:
//删除 [1, 2, 3].splice(0, 2); // [1, 2] //插入 [1, 2, 3].splice(0,0,5); // [5, 1, 2, 3] //替换 [1, 2, 3].splice(0, 1, "a"); // ["a", 2, 3]
搜索和位置
严格相等
ECMAScript提供了3个严格相等的搜索方法:indexOf(),lastIndexOf() 和 includes()。
indexOf和lastIndexOf都返回要查找的元素在数组中的位置,没有找到就返回-1,includes返回布尔值。
let number = [1, 2, 3, 4, 5, 4, 3, 2, 1]; number.indexOf(4); // 3 number.lastIndexOf(4); // 5 number.includes(2); // true
断言函数
断言函数的返回值决定了相应索引的元素是否被认为匹配,不匹配返回一个false,在JS中 find() 和 findIndex() 使用了断言函数。find返回第一个匹配的元素,没有则返回undefined,findIndex返回第一个匹配元素的索引,没有则返回-1:
[1, 2, 3].find(x => x == 2); // 2 [1, 2, 3].findIndex(x => x == 2); // 1
迭代
ECMAScript 为数组定义了 5 个迭代方法。每个方法接收两个参数:以每一项为参数运行的函数, 以及可选的作为函数运行上下文的作用域对象(影响函数中 this 的值)。传给每个方法的函数接收 3 个参数:数组元素、元素索引和数组本身。因具体方法而异,这个函数的执行结果可能会也可能不会影 响方法的返回值。
- every
如果每一项都返回true,则返回true
[1, 2, 3].every(x => x < 100); // true [1, 2, 3].every(x => x < 2); // false
- some
如果有一项返回true,则返回true
[1, 2, 3].some(x => x < 100); // true [1, 2, 3].some(x => x < 2); // true
- filter
函数返回true的项会组成数组后返回
[1, 2, 3].filter(x => x == 2); // [2]
- forEach
自定义进行操作数组,无返回值
[1, 2, 3].forEach(x => { // do something })
- map
自定义进行操作数组,返回每次结果构成的新数组
[1, 2, 3].map(x => { // do something })
归并
ECMAScript 为数组提供了两个ॅ并方法:reduce() 和 reduceRight()。这两个方法都会迭代数 组的所有项,并在此基础上构建一个最终返回值。reduce()方法从数组第一项开始遍历到最后一项。 而 reduceRight()从最后一项开始遍历至第一项。
这两个方法都接收2个参数,归并函数和初始值,里面的归并函数接收4个参数,上一个归并值,当前项,当前项的索引和数组本身。这个函数返回的任何值都会作为下一次调用同一个函数的第一个参数。如果没有给这两个方法传入可选的第二个参数(作为归并起点值),则第一次迭代将从数组的第二项开始,因此传给归并函数的第一个参数是数组的第一项,第二个参数是数组的第二项:
let values = [1, 2, 3, 4, 5]; values.reduce((prev, cur, index, arr) => prev + cur); // 15
这样就可以进行一个简单的累加操作。
以下为reduce的一些进阶用法:
- 计算数组中每个元素出现的次数
let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice']; let nameNum = names.reduce((pre,cur) => { if(cur in pre) { pre[cur]++ } else { pre[cur] = 1 } return pre }, {}) console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}
- 数组去重
let arr = [1,2,3,4,4,1] let newArr = arr.reduce((pre,cur) => { if(!pre.includes(cur)) { return pre.concat(cur) } else { return pre } },[]) console.log(newArr);// [1, 2, 3, 4]
- 二维数组转化为一维
let arr = [[0, 1], [2, 3], [4, 5]] let newArr = arr.reduce((pre,cur) => { return pre.concat(cur) },[]) console.log(newArr); // [0, 1, 2, 3, 4, 5]
- 多维数组转化为一维
let arr = [[0, 1], [2, 3], [4,[5,6,7]]] const newArr = function(arr){ return arr.reduce((pre,cur) => pre.concat(Array.isArray(cur) ? newArr(cur) : cur), []) } console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]
- 对象里属性求和
var result = [ { subject: 'math', score: 10 }, { subject: 'chinese', score: 20 }, { subject: 'english', score: 30 } ]; var sum = result.reduce(function(prev, cur) { return cur.score + prev; }, 0); console.log(sum) //60