免費IT學習資料 加羣:272292492
數組是一個超常用的數據結構,JavaScript的數組方法都有什麼怎樣的特性呢?
JavaScript中數組是一個對象,默認的賦值是傳了一個引用。針對結果是引用還是拷貝,對原數組的變更與否,分爲兩類方法:必寫方法、只讀方法。
必寫方法
splice
Array.prototype.splice(start: number, deleteCount: number, ...items: any[]): any[]
arr.splice(start, deleteCount, …items) 是將arr[start, start + deleteCount) 的部分裁去,然後在這裏插入items。
這個 splice 的表達能力非常強大,在數組的特定位置裁一刀,並用一個數組補上去。並返回因爲被裁掉而生成的數組。雖然它看起來是塊級的操作好像可以實現常數時間複雜度,但是其實它是一個線性的操作,從參數列表中可以看出它是線性的。
思考:splice 對於插入參數的長度而言的插入效率如何?[如果Array以鏈表實現,插入的代價最快是常數時間的]
參考:是線性時間複雜度的,而不是常數時間的。注意它的參數列表,傳參方式決定了它是逐一處理參數的。例如調用splice(0, 0, [1, 2]) 的結果是插入了一個[1, 2] 而不是1, 2 這兩個數。
copyWithin
Array.prototype.copyWithin(target: number, start: number, end: number): this
arr.copyWithin(target, start, end) 是將 arr[start, end) 這部分先做一個拷貝,然後再貼到從arr[target] 開始的位置。
思考:如何用 splice 實現 copyWithin?
fill
Array.prototype.fill(value: number, start: number, end: number): this
arr.fill(value, start, end) 是將 arr[start, end) 的部分都填充成同一個 value。
var arr = [1, 2, 3];
arr.fill(0, 1, 2);
arr.fill(0);
var arr = [{}, {}];
arr[0] == arr[1];
arr.fill({});
arr[0] == arr[1];
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)可能是比較恰當的。
提示:組合使用 push 與 pop 可以使得 Array 變成一個棧;組合使用 push 與 shift 可以使得 Array 變成一個隊列。
思考:組合使用 unshift 與 shift 是否實現了棧?組合使用 unshift 與 pop 是否實現了隊列?
參考:是,但這種方式有一個小坑點。因爲從結果上說,unshift(1, 2)與先後調用 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
var arr = [2, 1];
arr.sort();
arr.sort((a, b) => b - a);
reverse
Array.prototype.reverse(): this
var arr = [1, 2, 3];
arr.reverse();
arr;
乍一想,反轉可以算是一種按照索引號反序的特殊的排序,但 sort 的比較函數不能按照索引號寫,這就比較尷尬了。
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);
當然,這種方式看起來簡直蠢爆了,從時間、空間效率上看都不能採用,只是體現了一種思路。
只讀方法
forEach
Array.prototype.forEach(callbackFn: (value: any, index: number, array: this) => undefined, thisArg: any): undefined
forEach 與 for 循環
var arr = [1, 3];
arr.forEach(v => arr.push(v));
arr;
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]);
arr;
var arr = [1, 3];
for(var i of arr) arr.push(i);
這裏提供了一種簡單的Hack方式(forEach 的 for…in 實現):
Array.prototype.forEach = function(callbackFn, thisArg){
for(var i in this) callbackFn.call(thisArg, this[i], ~~i, this);
}
由於 for…in 循環還能遍歷對象的屬性,還可以寫一個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[]
var arr = [1, 2, 3];
arr.map(v => v * v);
arr;
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
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
聚合 reduce & reduceRight
reduce 是遍歷的同時將某個值試圖不斷更新的方法。
reduceRight 功能一樣,但是從右側開始(索引號大的一側)。
可以非常簡單地做到從一個數組中得出一個值 的操作,如求和,求最值等。
[1, 3, 2].reduce((pre, cur) => pre + cur, 1);
[1, 3, 2].reduce((pre, cur) => Math.max(pre, cur), -Infinity);
[1, 3, 2].reduce((pre, cur, i) => pre + '+' + cur + '*x^' + i , '');
[1, 3, 2].reduceRight((pre, cur, i) => pre + '+' + cur + '*x^' + i , '');
Array.prototype.reduce = function(callbackFn, initialValue){
for(var i in this) callbackFn(initialValue, this[i], ~~i, this);
return initialValue;
}
謂詞 every & some
every與some分別是數組中全稱、存在量詞的謂詞。
全稱謂詞 every: 是否數組中的元素全部都滿足某條件。
存在謂詞 some: 是否數組中的元素有一個滿足某條件。
[1, 2, 3].every(v => v > 0);
[1, 2, 3].every(v => v == 1);
[1, 2, 3].some(v => v == 1);
[1, 2, 3].some(v => v == 0);
var x = [];
[1, 2, 3].every(v => {x.push(v); return v < 2;})
x;
var x = [];
[1, 2, 3].some(v => {x.push(v); return v == 2;})
x;
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);
}
結果對了,然而很抱歉,儘管每次邏輯運算有短路判定了,但是reduce遍歷的開銷去不掉,性能不夠。
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可以將一個數組以特定的分隔符轉化爲字符串。
[1, 2, 3].join();
[1, 2, 3].join(',');
[1, 2, 3].join(' ');
[1, 2, 3].join('\n');
[1, 2, 3].join('\b');
[1, 2, 3].join('heiheihei');
Array.prototype.join = function(separator){
if(separator === undefined) separator = ',';
return this.reduce((pre, cur) => pre + (pre? separator: '') + cur , '');
}
Array.prototype.toString() 可以等效於 Array.prototype.join()。當然,這兩個函數對象本身是不同的。
搜索 find, findIndex, indexOf, lastIndexOf
返回從頭開始第一個符合條件的元素的索引號 findIndex
返回從頭開始第一個特定元素的索引號 indexOf
返回從尾開始第一個特定元素的索引號 lastIndexOf
[1, 3, 2, 1].find(v => v > 1);
[1, 3, 2, 1].find(v => v > 3);
[1, 3, 2, 1].findIndex(v => v > 1);
[1, 3, 2, 1].findIndex(v => v > 3);
[1, 3, 2, 1].indexOf(1);
[1, 3, 2, 1].indexOf(4);
[1, 3, 2, 1].lastIndexOf(1);
[1, 3, 2, 1].lastindexOf(4);
[1, 3, 2, 1].indexOf(1, 1);
[1, 3, 2, 1].lastIndexOf(1, 2);
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寫法並不具備短路優化,與every, some的reduce寫法一樣存在性能問題。
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);
}
子數組 與 filter, slice
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);
[1, 2, 3].filter(v => v & 1);
[1, 2, 3].filter((v, i) => i >= 1);
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;
}
如果強行把 子數組也看成一個數的話,也可以寫成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
生成數組在區間[start, end) 中的切片。
[1, 2, 3].slice();
[1, 2, 3].slice(0);
[1, 2, 3].slice(1);
[1, 2, 3].slice(1, 2);
[1, 2, 3].slice(2, 2);
[1, 2, 3].slice(2, 1);
Array.prototype.slice = function(start, end){
return this.filter((v, i) => i >= start && i < end);
}
超數組 與 concat
生成原數組的超數組,保持原數組在超數組中的順序不變。
Array.prototype.concat(...items: any[]): any[]
[1, 2, 3].concat();
[1, 2, 3].concat(1, 5);
[1, 2, 3].concat([1, 5]);
[1, 2, 3].concat(1, [3], [[5, 6]], 6);
[].concat({a: 1});
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的寫法,感覺不是很優雅,就留作日後思考吧:)
結語
函數式編程使人受益匪淺,集中在“思考問題的本質”這個角度。
Functional programming considers what the problem is rather than how the solution works.
比起思考解決方案如何運作,函數式編程更注重思考這個問題的本質是什麼。