数组是最常见的数据结构,常见到参考资料上都懒得去题数组的抽象数据类型实现,因为几乎所有的编程语言都内置了很多的方法,让我们很容易的就能实现数组的增添、删除、插入等操作。JavaScript中就内置了大量的方法供我们使用。
在刚开始学习的时候,我会花时间去研究每个方法是干嘛的,要传入什么参数,会返回什么。当好不容易知道了基本那些增删改查方法的用法之后,原来函数也可以作为参数传入,随后forEach
, map
, filter
, reduce
等又让人眼花缭乱,感觉数组的方法这么多,怎么掌握得了。学习一段时间之后,尤其是现在在学数据结构基本知识,让我意识到,数组和其他的数据结构一样,这些方法(基本操作)都是可以自己去实现的,并且非常简单,因为我知道数组是最简答的数据结构,其他的数据结构的实现会更复杂。
对于数组的抽象数据类型,我没找到对于它的基本操作的标准定义,但应该不外呼增删改查这些常用的操作。所以有必要先梳理一下JavaScript里内置的那些方法,然后自己去实现这些方法。
JavaScript数组方法总结
数组的方法有很多,我想把它弄全是不现实的,同时也是没必要的,这里可以参考MDN的数组方法。大致有个印象即可,这里我仅仅按照自己的理解来对它们进行分类。
- 增:unshift,push
- 删:shift,pop,slice(截取部分)
- 多功能增删:splice(这个东西功能强大,后面我手写时将其拆成了
insert
和remove
两个方法) - 查:indexOf,lastIndexOf,find(f),includes
- 排序:reverse,sort(compare)
- 数组变成字符串:join(自己加分隔符),toString(分隔符为,)
- 拼接:concat
- 迭代方法(参数为函数):every,some,filter, find,map,forEach,reduce
- 创建和初始化时:fill,Array.from,
大概就能写出这么多了,肯定不全,但是目前就经常接触这些,其他的慢慢积累吧,还有ES6新增的一些,有了解,但自己还没怎么用过。下面为数组方法的注意事项:
@@iterator
返回一个包含键值对的迭代器对象entries
,keys
,values
方法:使用集合、字典、散列表等数据结构时,能够取出键值对是很有用的。sort
传入比较函数(大于还是小于0),自定义排序
刚学JavaScript的时候,感觉数组的东西很多,现在想想,首先的明白自己的数组操作的目的,然后再朝这个想办法。诚然,一个数组,对其颠来复去方法有很多种方法,但是我们真的要全部掌握吗,剪刀能干的事情刀子未必不行。将来可能还会有各种各样新的方法出现,但万变不离其宗。我想,如果自己能够写出这些自带的数组方法,那意义应该更大吧,其中的逻辑才是最重要的。
手写数组方法
增加元素
- insertFirst开头插入,改变原数组,返回长度(同unshift)
- insertLast结尾插入,改变原数组,返回长度(同push)
- insert任意位置插入,改变原数组,返回长度(同splice的插入功能)
//增:开头插入,改变原数组,返回长度
Array.prototype.insertFirst = function (value) {
let offsetIndex = arguments.length;
this.length += offsetIndex;
for (let i = this.length - 1; i > offsetIndex - 1; i--) {
this[i] = this[i - offsetIndex];
}
for (let i = 0; i < offsetIndex; i++) {
this[i] = arguments[i];
}
return this.length;
}
//增:末尾插入,改变原数组,返回长度
Array.prototype.insertLast = function (value) {
let len = this.length;
for (let i = len; i < (len + arguments.length); i++) {
this[i] = arguments[i - len];
}
len += arguments.length;
return len
}
//增:任意地方插入(开始索引,新元素,……),返回新元素数组
Array.prototype.insert = function (value) {
let oldLen = this.length;
let addLen = arguments.length - 1;
let start = arguments[0];
this.length += addLen;
for (let i = this.length - 1; i >= start + addLen; i--) {
this[i] = this[i - addLen]
}
for (let i = start; i < start + addLen; i++) {
this[i] = arguments[i - start + 1]
}
let addArr = [];
for (let i = 0; i < addLen; i++) {
addArr[i] = arguments[i + 1];
}
return addArr;
}
删除元素
removFirst
删除开头元素,改变原数组,返回删除掉的东西(同shift
)removeLast
删除末尾元素,改变原数组,返回删除掉的东西(同pop
)remove
删除任意位置元素,改变原数组,返回删除掉的东西组成的数组(同splice
的删除功能)
//删:删除开头,改变原数组,返回删除掉的东西
Array.prototype.removeFirst = function () {
let res = this[0];
for (let i = 0; i < this.length - 1; i++) {
this[i] = this[i + 1];
}
this.length--;
return res;
}
//删:删除结尾,改变原数组,返回删除掉的东西
Array.prototype.removeLast = function () {
let res = this[this.length - 1];
this.length--;
return res;
}
//删:任意位置(开始索引,删除几个),返回删除元素组成的数组
Array.prototype.remove = function (value) {
let start = arguments[0];
let removeNum = arguments[1];
if (removeNum > (this.length - start)) {
let newArr = [];
for (let i = 0; i < (this.length - start); i++) {
newArr[i] = this[i + start];
}
this.length = start;
return newArr;
} else if (removeNum > 0) {
let newLen = this.length - removeNum;
for (let i = start; i < newLen; i++) {
this[i] = this[i + removeNum];
}
let newArr = [];
for (let i = start; i < removeNum; i++) {
newArr[i] = this[i + start];
}
this.length = newLen;
return newArr;
}
}
迭代方法
迭代方法的功能很强大,据说是函数式编程的基础(但这目前我也不懂),学过数学,我知道f(x)
,传入固定的因变量肯定会出来固定的因变量,同样的数学中允许我们写高阶函数,即f(g(x))
。在JavaScript中,函数也是一个变量,可以传入另外一个函数中作为参数,这就是迭代方法。
刚开始接触这些迭代方法的时候有点乱,其实理一理就很简单了:
- 返回一个Boolean:every,some,
- 返回一个新数组:map,filter,
- 遍历做某件事:forEach,
- 返回符合条件的第一个值:find,findIndex(返回索引)
- 高级迭代,最终返回一个值:reduce,
返回一个Boolean
allTrue
:所有元素都满足要求才返回真,同every
someTrue
:只有有一个元素满足要求就返回真,同some
//同every,全为真才行
Array.prototype.allTrue = function (f) {
let res = true;
for (let i = 0; i < this.length; i++) {
let itemRes = f(this[i]);
res = res && itemRes;
}
return res;
}
//验证
let res1 = [1, 2, 3].allTrue(x => x > 0)
console.log(res1) //true
let res2 = [-1, -2, -3].allTrue(x => x > 0)
console.log(res2) //false
//同some,有一个为真就行了
只需要把上面的allTrue里的
res = res && itemRes;
改为
res = res || itemRes;即可
返回一个新数组
newAll
:所有元素加工一下,返回新数组(个数不变),同map
newSome
:过滤一部分,自然符合要的通过,返回新数组(个数减少),同filter
//同map
Array.prototype.newAll = function (f) {
let newArr = new Array(this.length);
for (let i = 0; i < this.length; i++) {
newArr[i] = f(this[i])
}
return newArr;
}
//例子:数组里的元素平方
let arr = [1, 3, 4]
result1 = arr.map(x => x**2)
result2 = arr.newAll(x => x**2)
//都返回 [1,9,16]
//同filter
Array.prototype.newSome = function (f) {
let newArr = [];
for (let i = 0; i < this.length; i++) {
if (f(this[i])) {
newArr.push(this[i])
}
}
return newArr;
}
//例子:返回数组里的偶数
let arr = [1, 2, 3, 4, 5, 6];
let result1 = arr.filter(x => x%2===0);
let result2 = arr.newSome(x => x%2===0)
//都返回 [2, 4, 6]
遍历做某件事
forEach
是把数组里的每个元素都让它出来做某件事,这不就是一个for
循环的事情吗,是的。就是这样的,不过JavaScript把我们封装好了,只暴露出来一个接口(API、方法、函数……怎么说都行,总是是让我们很容易做某件事),而不用去管这个接口里面是怎么实现的,这就慢慢理解抽象数据类型的抽象。最好的例子就是插座,我们不用管里面的线路如何复杂,我们只知道插上这个接口就能用,那就行了。目前,这也是我所理解的ADT。
只管暴露出来的接口
//定义一个allDo方法,同forEach
Array.prototype.allDo = function (f) {
for (let i = 0; i < this.length; i++) {
f(this[i]);
}
}
//测试:
let people = ["刘备", "关羽", "张飞"]
people.allDo(x => {
console.log(`hello,${x}`)
})
//控制台输出
hello,刘备
hello,关羽
hello,张飞
手写reduce
要自己写一个reduce方法,那首先就要清楚reduce是怎么工作的,reduce可以说是数组这些迭代方法中最难的一个了,所以有必要好好捋一捋,先来用reduce写一个计算平方。
let arr = [1, 2, 3];
let result = arr.reduce((x, y) => (x + y ** 2))
//14
用遍历的方式自然可以同样写出,下面的代码能实现同样的功能,但是代码量明显多了很多。向reduce这样的很多方法,像上面我手写的这些数组方法,实际上都不是必须的,没有他们完全可以写出相同功能的程序,之所以多了这么多方法,只是提高我们的代码效率,写起来更快,更方便而已。
function squSum(arr) {
let res = 0;
for (let i = 0; i < arr.length; i++) {
res += (arr[i] ** 2);
}
return res;
}
let result = squSum(arr)
reduce工作过程
reduce()
方法將一個累加器及陣列中每項元素(由左至右)傳入回呼函式,將陣列化為單一值。
let result = [1, 2, 3, 4, 5].reduce((x, y) => (x + y**2));
//过程:
(1, 2)作为参数 (x, y) => (x + y**2) = 1 + 2**2 = 5;
(5, 3)作为参数 (x, y) => (x + y**2) = 5 + 3**2 = 14;
(14, 4)作为参数 (x, y) => (x + y**2) = 14 + 4**2 = 30;
(30, 3)作为参数 (x, y) => (x + y**2) = 30 + 5**2 = 55;
最终result = 55;
每次从数组里传入两个最为参数(previous,current),这两个在callback运算之后的返回值作为第一个参数,再从数组里的下一个元素作为第二个参数,两参数再次传入callback中,一直这样下去,直到数组中所有元素都参与callback运算,最终返回一个结果(值);
理解reduce
的操作过程之后,我们完全就可以自己写一个reduce
了,我给取名叫huigun
,即中文“回滚”方法。
手写reduce方法
Array.prototype.huigun = function (f) {
let res;
for (let i = 0; i < this.length - 1; i++) {
res = f(this[i], this[i + 1])
this[i + 1] = res
}
return res;
}
其实代码非常简单,这里来验证一下,是不是达到了JavaScript中reduce
的同样功能。
//求和
let arr = [1, 2, 4, 5];
let result1 = arr.reduce((x, y) => (x + y));
let result2 = arr.huigun((x, y) => (x + y));
console.log(result1, result2) // 12, 12
理解了这个累加求和,其实相加的不仅仅有数字,String
也是可以加的,于是reduce可以充当join
方法的功能了。
let arr = ['hello', 'beautiful', 'world'];
let result1 = arr.join(','); //或者arr.toString;
let result2 = arr.huigun((x, y) => (x + ',' + y)); //或者reduce
//两者都为:
hello,beautiful,world
总结
数据是最基础的数据结构,也是其他抽象数据类型的基础。虽然任何一名编程语言都内置了很多现成的方法供我们使用,但我们应该清楚实质是什么,实质就是它们只是最简单的数据类型(原始数据类型)和最简单的操作(基本运算符、基本语句)组合实现的。那到了更复杂的数据结构,它们也不例外,都是有简单的数据结构封装起来的。
所以,最难的可能是for
循环,或者最简单的加减乘除运算符是如何实现的,没有它们,那很合程序都是无从下手的。越到了上面,至于像reduce
这样的东西,它很高级,但是缺它不妨,不会影响程序的编写和实现。
这就是关于JavaScript中数组方法的一些简单实现,下面就在这基础上学一些复杂的数据结构。how hard can it be?