本文轉(zhuǎn)載自@陳錚半年前的博文,原文地址:JavaScript Array 原型方法 大盤點
數(shù)組是一個超常用的數(shù)據(jù)結(jié)構(gòu),JavaScript的數(shù)組方法都有什么怎樣的特性呢?
是時候一探究竟了。
JavaScript中數(shù)組是一個對象,默認的賦值是傳了一個引用。針對結(jié)果是引用還是拷貝,對原數(shù)組的變更與否,分為兩類方法:必寫方法、只讀方法。
必寫方法
列舉了一些會造成數(shù)組本身的內(nèi)容發(fā)生改變的方法。
splice
Array.prototype.splice(start:number,deleteCount:number,...items:any[]):any[]
arr.splice(start, deleteCount, …items) 是將arr[start, start + deleteCount) 的部分裁去,然后在這里插入items。
這個 splice 的表達能力非常強大,在數(shù)組的特定位置裁一刀,并用一個數(shù)組補上去。并返回因為被裁掉而生成的數(shù)組。雖然它看起來是塊級的操作好像可以實現(xiàn)常數(shù)時間復(fù)雜度,但是其實它是一個線性的操作,從參數(shù)列表中可以看出它是線性的。
思考:splice 對于插入?yún)?shù)的長度而言的插入效率如何?[如果Array以鏈表實現(xiàn),插入的代價最快是常數(shù)時間的]
參考:是線性時間復(fù)雜度的,而不是常數(shù)時間的。注意它的參數(shù)列表,傳參方式?jīng)Q定了它是逐一處理參數(shù)的。例如調(diào)用splice(0, 0, [1, 2])的結(jié)果是插入了一個[1, 2] 而不是1, 2 這兩個數(shù)。
copyWithin
Array.prototype.copyWithin(target:number,start:number,end:number):this
arr.copyWithin(target, start, end) 是將 arr[start, end) 這部分先做一個拷貝,然后再貼到從arr[target] 開始的位置。
思考:如何用 splice 實現(xiàn) copyWithin?
提示:使用字符串構(gòu)造函數(shù)。
fill
Array.prototype.fill(value:number,start:number,end:number):this
arr.fill(value, start, end) 是將 arr[start, end) 的部分都填充成同一個value。
這提供了一種簡單的數(shù)組初始化方式。
var arr=[1,2,3];
arr.fill(0,1,2);// [1, 0, 3]
arr.fill(0);// [0, 0, 0]
在填充對象時要注意:
var arr=[{},{}];
arr[0]==arr[1];// false
arr.fill({});// [{}, {}]
arr[0]==arr[1];// true
push, unshift, pop, shift
Array.prototype.push(...items:any[]):number
Array.prototype.unshift(...items:any[]):number
Array.prototype.pop():any
Array.prototype.shift():any
將Array看作是一個雙向隊列(deque)可能是比較恰當?shù)摹?/p>
? 從頭部插入:unshift
? 從尾部插入:push
? 從頭部刪除:shift
? 從尾部刪除:pop
提示:組合使用 push 與 pop 可以使得 Array 變成一個棧;組合使用 push 與 shift 可以使得 Array 變成一個隊列。
思考:組合使用 unshift 與 shift 是否實現(xiàn)了棧?組合使用 unshift 與 pop 是否實現(xiàn)了隊列?
參考:是,但這種方式有一個小坑點。因為從結(jié)果上說,unshift(1, 2)與先后調(diào)用 unshift(1), unshift(2)不同。可以推測,unshift與push是splice的特殊情況。unshift(…items) 與 splice(0, 0, …items) 是一致的,push(…items) 與 splice(this.length, 0, …items) 是一致的。BTW,shift() 與 splice(0, 1) 一致; pop()與splice(this.length - 1, 1) 一致;
sort
Array.prototype.sort(sortFn:(a:any,b:any)=>number):this
將數(shù)組排序,默認使用升序,會改變數(shù)組自身。
var arr=[2,1];
arr.sort();// [1, 2]
arr.sort((a,b)=>b-a);// [2, 1]
reverse
Array.prototype.reverse():this
將數(shù)組反轉(zhuǎn),會改變數(shù)組自身。
var arr=[1,2,3];
arr.reverse();// [3, 2, 1];
arr;// [3, 2, 1];
乍一想,反轉(zhuǎn)可以算是一種按照索引號反序的特殊的排序,但 sort 的比較函數(shù)不能按照索引號寫,這就比較尷尬了。
var arr=[1,2,3];
arr=arr.map((v,i)=>{return{value:v,index:i};}).sort((a,b)=>b.index-a.index).map(v=>v.value);// [3, 2, 1]
當然,這種方式看起來簡直蠢爆了,從時間、空間效率上看都不能采用,只是體現(xiàn)了一種思路。
只讀方法
forEach
提供了一種遍歷數(shù)組的方法。
Array.prototype.forEach(callbackFn:(value:any,index:number,array:this)=>undefined,thisArg:any):undefined
forEach 與 for 循環(huán)
var arr=[1,3];
arr.forEach(v=>arr.push(v));// undefined
arr;// [1, 3, 1, 3]
var arr=[1,3];
for(var i=0;i < arr.length;i++)arr.push(arr[i]);
var arr=[1,3];
for(var i in arr) arr.push(arr[i]);// 4
arr;// [1, 3, 1, 3]
var arr=[1,3];
for(var i of arr) arr.push(i);// Death Loop
這里提供了一種簡單的Hack方式(forEach 的 for…in 實現(xiàn)):
Array.prototype.forEach=function(callbackFn,thisArg){
for(var i in this)callbackFn.call(thisArg, this[i], i, this);
}
由于 for…in 循環(huán)還能遍歷對象的屬性,還可以寫一個Object版本的forEach:
Object.prototype.forEach=function(callbackFn,thisArg){
for(var i in this)callbackFn.call(thisArg,this[i],i,this);
}
映射 map
映射:準確地說是滿射(一一映射)。
Array.prototype.map(callbackFn:(value:any,index:number,array:this)=>T,thisArg:any):T[]
注意它的特性:返回與原數(shù)組長度一致的新數(shù)組。
樣例:
var arr=[1,2,3];
arr.map(v=>v*v);// [1, 4, 9]
arr;// [1, 2, 3]
for…in 寫法:
Array.prototype.map = function(callbackFn, thisArg){
?var ret = [];? ??
for(var i in this) ret.push(callbackFn.call(thisArg, this[i], ~~i, this)); ?
?return ret;
}
聚合 reduce, reduceRight, every, some, join, indexOf, lastIndexOf, find, findIndex
聚合:將數(shù)組聚合成一個值。
Array.prototype.reduce(callbackFn:(previousValue:any,currentValue:any,currentIndex:number,array:this)=>any,initialValue:any):any
Array.prototype.reduceRight(callbackFn:(previousValue:any,currentValue:any,currentIndex:number,array:this)=>any,initialValue:any):any
Array.prototype.every(callbackFn:(value:any,index:number,array:this),thisArg:any):boolean
Array.prototype.some(callbackFn:(value:any,index:number,array:this),thisArg:any):boolean
Array.prototype.join(separator:string):string
Array.prototype.find(callbackFn:(value:T,index:number,array:this)=>boolean,thisArg:any):T
Array.prototype.findIndex(callbackFn:(value:any,index:number,array:this)=>boolean,thisArg:any):number
Array.prototype.indexOf(item:any,start:number):number
Array.prototype.lastIndexOf(item:any,start:number):number
從一個數(shù)組到一個元素,此所謂聚合之意。
聚合 reduce & reduceRight
reduce 是遍歷的同時將某個值試圖不斷更新的方法。
reduceRight 功能一樣,但是從右側(cè)開始(索引號大的一側(cè))。
可以非常簡單地做到從一個數(shù)組中得出一個值的操作,如求和,求最值等。
用例:
[1,3,2].reduce((pre,cur)=>pre+cur,1); ?// 6
[1,3,2].reduce((pre,cur)=>Math.max(pre,cur),-Infinity);// 3
[1,3,2].reduce((pre,cur,i)=>pre+'+'+cur+'*x^'+i,'');// '+1*x^0+3*x^1+2*x^2'
[1,3,2].reduceRight((pre,cur,i)=>pre+'+'+cur+'*x^'+i,'');// '+2*x^2+3*x^1+1*x^0'
for…in 寫法:
Array.prototype.reduce=function(callbackFn,initialValue){
for(var i in this)callbackFn(initialValue,this[i],~~i,this);returninitialValue;
}
謂詞 every & some
every與some分別是數(shù)組中全稱、存在量詞的謂詞。
全稱謂詞 every: 是否數(shù)組中的元素全部都滿足某條件。
存在謂詞 some: 是否數(shù)組中的元素有一個滿足某條件。
[1,2,3].every(v=>v>0);// true
[1,2,3].every(v=>v==1);// false
[1,2,3].some(v=>v==1);// true
[1,2,3].some(v=>v==0);// false
注意:every與some具有邏輯運算的短路性。
在遍歷的途中:
every只要收到一個false,就會停止遍歷;
some只要收到一個true,就會停止遍歷;
var x=[];
[1,2,3].every(v=>{x.push(v);returnv<2;})// false
x;// [1, 2]
var x=[];
[1,2,3].some(v=>{x.push(v);returnv==2;})// true
x;// [1, 2]
reduce 寫法:
Array.prototype.every=function(callbackFn,thisArg){
return this.reduce(function(previousValue,currentValue,currentIndex,array){
? ? return previousValue&&callbackFn.call(thisArg,currentValue,currentIndex,array);
},true);
}
Array.prototype.some=function(callbackFn,thisArg){
return this.reduce(function(previousValue,currentValue,currentIndex,array){
? ? return previousValue||callbackFn.call(thisArg,currentValue,currentIndex,array);
},false);
}
結(jié)果對了,然而很抱歉,盡管每次邏輯運算有短路判定了,但是reduce遍歷的開銷去不掉,性能不夠。
對于10^7 長度的數(shù)組就有明顯的延遲。
for…in 寫法:
Array.prototype.every=function(callbackFn,thisArg){
var ret=true;
for(var i in this){if(ret==false)break;ret&=callbackFn.call(thisArg,this[i],~~i,this);}
return ret;
}
Array.prototype.some=function(callbackFn,thisArg){
var ret=false;
for(var i in this){if(ret==false)break;ret|=callbackFn.call(thisArg,this[i],~~i,this);}
return ret;
}
串行化 join
join可以將一個數(shù)組以特定的分隔符轉(zhuǎn)化為字符串。
join默認使用半角逗號分隔。
樣例:
[1,2,3].join();// '1,2,3'
[1,2,3].join(',');// '1,2,3'
[1,2,3].join(' ');// '1 2 3'
[1,2,3].join('\n');// '1\n2\n3' # 打印出來是換行的
[1,2,3].join('\b');// '1\b2\b3' # 打印出來只有3,\b為退格
[1,2,3].join('heiheihei');// '1heiheihei2heiheihei3'
reduce 寫法:
Array.prototype.join=function(separator{
if(separator===undefined)separator=',';
return this.reduce((pre,cur)=>pre+(pre?separator:'')+cur,'');
}
Array.prototype.toString() 可以等效于 Array.prototype.join()。當然,這兩個函數(shù)對象本身是不同的。
搜索 find, findIndex, indexOf, lastIndexOf
? 返回從頭開始第一個符合條件的元素 find
? 返回從頭開始第一個符合條件的元素的索引號 findIndex
? 返回從頭開始第一個特定元素的索引號 indexOf
? 返回從尾開始第一個特定元素的索引號 lastIndexOf
樣例:
[1,3,2,1].find(v=>v>1);// 3
[1,3,2,1].find(v=>v>3);// undefined
[1,3,2,1].findIndex(v=>v>1);// 1
[1,3,2,1].findIndex(v=>v>3);// -1
[1,3,2,1].indexOf(1);// 0
[1,3,2,1].indexOf(4);// -1
[1,3,2,1].lastIndexOf(1);// 3
[1,3,2,1].lastindexOf(4);// -1
[1,3,2,1].indexOf(1,1);// 3
[1,3,2,1].lastIndexOf(1,2);// 0
reduce 寫法:
Array.prototype.find=function(callbackFn,thisArg){
return this.reduce((pre,cur,i)=>{if(pre===undefined&&callbackFn.call(thisArg,cur,i,this))
return cur;});
}
Array.prototype.findIndex=function(callbackFn,thisArg){
return this.reduce((pre,cur,i)=>{if(pre==-1&&callbackFn.call(thisArg,cur,i,this))
return i;},-1);
}
這個reduce寫法并不具備短路優(yōu)化,與every, some的reduce寫法一樣存在性能問題。
for…in 寫法:
Array.prototype.find=function(callbackFn,thisArg{
for(var i in this)if(callbackFn.call(thisArg,this[i],~~i,this))return this[i];
}
Array.prototype.findIndex=function(callbackFn,thisArg){
for(var i in this)if(callbackFn.call(thisArg,this[i],~~i,this))return i;
}
然后,indexOf 可看作是 findIndex 的一個特例。
Array.prototype.indexOf=function(item,start){
return this.findIndex((v,i)=>i>=start&&v==item);
}
子數(shù)組 與 filter, slice
篩選:生成數(shù)組的保序子數(shù)組。
Array.prototype.filter(callbackFn:(value:any,index:number,array:this)=>boolean,thisArg:any):any[]Array.prototype.slice(start:number,end:number):any[]
過濾 filter
樣例:
[1,2,3].filter(v=>v%2==0);// [2]
[1,2,3].filter(v=>v&1);// [1, 3]
[1,2,3].filter((v,i)=>i>=1);// [2, 3]
for…in 寫法:
Array.prototype.filter=function(callbackFn,thisArg){
var ret=[];
for(var i in this)if(callbackFn.call(thisArg,this[i],~~i,this))ret.push(this[i]);
return ret;
}
如果強行把 子數(shù)組也看成一個數(shù)的話,也可以寫成reduce:
Array.prototype.filter=function(callbackFn,thisArg){
return this.reduce((pre,cur,i)=>{if(callbackFn.call(thisArg,cur,i,this))pre.push(cur);
return pre;},[]);
}
切片 slice
生成數(shù)組在區(qū)間[start, end) 中的切片。
樣例:
[1,2,3].slice();// [1, 2, 3]
[1,2,3].slice(0);// [1, 2, 3]
[1,2,3].slice(1);// [2, 3]
[1,2,3].slice(1,2);// [2]
[1,2,3].slice(2,2);// []
[1,2,3].slice(2,1);// []
filter 寫法:
Array.prototype.slice= function(start, end){returnthis.filter((v, i) => i >= start && i < end);}
超數(shù)組 與 concat
生成原數(shù)組的超數(shù)組,保持原數(shù)組在超數(shù)組中的順序不變。
Array.prototype.concat(...items: any[]): any[]
樣例:
[1,2,3].concat();// [1, 2, 3]
[1,2,3].concat(1,5);// [1, 2, 3, 1, 5]
[1,2,3].concat([1,5]);// [1, 2, 3, 1, 5]
[1,2,3].concat(1,[3],[[5,6]],6);// [1, 2, 3, 1, 3, [5, 6], 6]
[].concat({a:1});// [{a: 1}]
可見concat會嘗試一次拆數(shù)組。
for…in 寫法:
Array.prototype.concat=function(...items){
var ret=[];
for(var i in this)ret.push(this[i]);
for(var i in items)
{if(Array.isArray(items[i]))for(var j in items[i])ret.push(items[i][j]);else ret.push(items[i]);
}
return ret;
}
同 filter 的思路,也有reduce的寫法,感覺不是很優(yōu)雅,就留作日后思考吧:)
結(jié)語
寫了這么多,是時候打出一波結(jié)語了。
函數(shù)式編程使人受益匪淺,集中在“思考問題的本質(zhì)”這個角度。
比起命令式的做法,更是一種聲明式的說法。
Functional programming considers what the problem is rather than how the solution works.
比起思考解決方案如何運作,函數(shù)式編程更注重思考這個問題的本質(zhì)是什么。