分針網—每日分享:JavaScript Array 原型方法 大盤點

免費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); // [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)可能是比較恰當的。

從頭部插入:unshift
從尾部插入:push
從頭部刪除:shift
從尾部刪除:pop
提示:組合使用 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(); // [1, 2]
arr.sort((a, b) => b - a); // [2, 1]

reverse

Array.prototype.reverse(): this

將數組反轉,會改變數組自身。
var arr = [1, 2, 3];
arr.reverse(); // [3, 2, 1];
arr; // [3, 2, 1];

乍一想,反轉可以算是一種按照索引號反序的特殊的排序,但 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); // [3, 2, 1]

當然,這種方式看起來簡直蠢爆了,從時間、空間效率上看都不能採用,只是體現了一種思路。

只讀方法


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)); // undefined
arr; // [1, 3, 1, 3]
var arr = [1, 3];
for(var i = 0; i < arr.length; i++) arr.push(arr[i]); // Death Loop
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 實現):
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); // [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


聚合:將數組聚合成一個值。
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); // 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);
return initialValue;
}

謂詞 every & some


every與some分別是數組中全稱、存在量詞的謂詞。
全稱謂詞 every: 是否數組中的元素全部都滿足某條件。
存在謂詞 some: 是否數組中的元素有一個滿足某條件。
[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); return v < 2;}) // false
x; // [1, 2]
var x = [];
[1, 2, 3].some(v => {x.push(v); return v == 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);
}

結果對了,然而很抱歉,儘管每次邏輯運算有短路判定了,但是reduce遍歷的開銷去不掉,性能不夠。
對於10^7 長度的數組就有明顯的延遲。

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可以將一個數組以特定的分隔符轉化爲字符串。

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()。當然,這兩個函數對象本身是不同的。
搜索 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寫法並不具備短路優化,與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);
}

子數組 與 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); // [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;
}

如果強行把 子數組也看成一個數的話,也可以寫成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]
[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){
return this.filter((v, i) => i >= start && i < end);
}

超數組 與 concat


生成原數組的超數組,保持原數組在超數組中的順序不變。
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會嘗試一次拆數組。

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的寫法,感覺不是很優雅,就留作日後思考吧:)

結語


寫了這麼多,是時候打出一波結語了。

函數式編程使人受益匪淺,集中在“思考問題的本質”這個角度。

比起命令式的做法,更是一種聲明式的說法。

Functional programming considers what the problem is rather than how the solution works.
比起思考解決方案如何運作,函數式編程更注重思考這個問題的本質是什麼。

更多文章www.f-z.cn

發佈了3 篇原創文章 · 獲贊 16 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章