最近做一個專案中做一個競猜遊戲介面,遊戲規則和彩票是一樣的。在實現“機選一注”,“機選五注”的時候遇到陣列的一些操作,例如產生['01', '02' ... '35']這樣的陣列,隨機抽取不重複的元素重新組成陣列等問題。回想這類問題在平時專案中遇到的機會非常多,何不歸納一下JavaScript陣列的一些知識點,以供平時工作參考。
JavaScript提供的陣列非常靈活,相關的api也很豐富,例如fill,map,filter,sort等等,極大地方便了程式編寫。這裡不介紹這些基本的api,而是通過工作中常用的使用場景來展示陣列的強大。
1.歸納計算
在一個分頁表格中比如訂單表,要求根據訂單金額展示這一頁的訂單總金額。很多時候後端開發偷懶,把這種計算推給前端,可以使用reduce輕鬆實現個功能,程式碼如下。
var orders = [ { userName: 'Anna', books: 'Bible', money: 21.2 }, { userName: 'Bob', books: 'War and peace', money: 26.5 }, { userName: 'Alice', books: 'The Lord of the Rings', money: 18.4 } ]; let total = orders.reduce((acc, curr) => {return acc + curr.money}, 0); console.log(total);
在vue元件中,可以直接使用reduce表示式計算表格某一列的歸納總和,很方便,示例程式碼如下:
<tbody> <tr class="header-tr"> <th>品名</th> <th>批號</th> <th>規格</th> <th>等級</th> <th>生產入庫(KG)</th> <th>退貨入庫(KG)</th> <th>返修入庫(KG)</th> <th>返修投料(KG)</th> <th>出庫(KG)</th> <th>庫存結存(件)</th> <th>庫存結存重量(KG)</th> <th>期初結存(件)</th> <th>期初結存重量(KG)</th> </tr> <template v-for="(item, key) in tableData"> <template v-for="obj in item"> <tr> <td>{{key}}</td> <td>{{obj.batchNo}}</td> <td>{{obj.spec}}</td> <td>{{obj.level}}</td> <td>{{obj.productionInbound}}</td> <td>{{obj.refundInbound}}</td> <td>{{obj.reworkInbound}}</td> <td>{{obj.reworkFeeding}}</td> <td>{{obj.outbound}}</td> <td>{{obj.monthlyBalanceCount}}</td> <td>{{obj.monthlyBalanceWeight}}</td> <td>{{obj.preMonthlyBalanceCount}}</td> <td>{{obj.preMonthlyBalanceWeight}}</td> </tr> </template> <tr> <th colspan="3">{{key}}小計</th> <th> </th> <th>{{ item.reduce((acc, curr) => acc + curr.productionInbound, 0) }}</th> <th>{{ item.reduce((acc, curr) => acc + curr.refundInbound, 0) }}</th> <th>{{ item.reduce((acc, curr) => acc + curr.reworkInbound, 0) }}</th> <th>{{ item.reduce((acc, curr) => acc + curr.reworkFeeding, 0) }}</th> <th>{{ item.reduce((acc, curr) => acc + curr.outbound, 0) }}</th> <th>{{ item.reduce((acc, curr) => acc + curr.monthlyBalanceCount, 0) }}</th> <th>{{ item.reduce((acc, curr) => acc + curr.monthlyBalanceWeight, 0) }}</th> <th>{{ item.reduce((acc, curr) => acc + curr.preMonthlyBalanceCount, 0) }}</th> <th>{{ item.reduce((acc, curr) => acc + curr.preMonthlyBalanceWeight, 0) }}</th> </tr> </template>
2.快速生成陣列
工作中前端進度一般是先於後端的,前端畫頁面的時候後端服務一般還沒有寫好,這時前端要自己生成一些資料把頁面先做起來。有人可能會說用mock,但是小專案引用mock就太麻煩了。這時就要自己先生成一些資料,最常見的就是生成一個物件列表。下面就來討論生成資料的方式。
2.1 Array(length)&Array.fill()&Array.map()
建構函式Array()有兩個過載:
new Array(element0, element1[, ...[, elementN]]):根據給定元素生成一個javascript陣列,這些元素是逗號分割的。
new Array(arrayLength):arrayLength是一個範圍在0到232-1之間的整數,這時方法返回一個長度為arrayLength的陣列物件,注意陣列此時沒有包含任何實際的元素,不是undefined,也不是null,使用console.log()列印出來是empty。如果傳入的arrayLength不滿足上面條件,丟擲RangeError錯誤。
使用Array(arrayLength)獲取到空陣列之後,使用Array.fill()方法給陣列填充初始值,再使用Array.map()方法給陣列元素生成有意義的值。
console.time("arr1"); let arr1 = Array(10).fill(0).map((value, index) => { return ++index; }); console.timeEnd("arr1"); console.log(arr1);
輸出結果如下:
2.2 Array()&Array.from()
2.3 使用遞迴
使用了遞迴和立即執行函式來生成陣列。
console.time("arr3") let arr3 = (function wallace(i) { return (i < 1) ? [] : wallace(i - 1).concat(i); })(10); console.timeEnd("arr3");
執行結果如下:
2.4 使用尾遞迴
相對遞迴來說,尾遞迴效率更高。
console.time("arr4") let arr4 = (function mistake(i, acc) { return (i < 10) ? mistake(i + 1, acc.concat(i)) : acc; })(1, []); console.timeEnd("arr4") console.log(arr4);
執行結果如下:
2.5 使用ES6中的Generator
console.time("arr5"); function* mistake(i) { yield i; if (i < 10) { yield* mistake(i + 1); } } let arr5 = Array.from(mistake(1)); console.timeEnd("arr5"); console.log(arr5);
執行結果如下:
2.6 使用apply和類陣列物件
console.time("arr6"); let arr6 = Array.apply(null, {length: 10}).map((value, index) => index + 1); console.timeEnd("arr6"); console.log(arr6);
結果如下:
3.陣列去重
3.1 物件屬性
使用物件屬性不重名的特性。
var arr = ['qiang','ming','tao','li','liang','you','qiang','tao']; console.time("nonredundant1"); var nonredundant1 = Object.getOwnPropertyNames(arr.reduce(function(seed, item, index) { seed[item] = index; return seed; },{})); console.timeEnd("nonredundant1"); console.log(nonredundant1);
結果如下:
3.2 使用Set
set是一種類似陣列的結構,但是set成員中沒有重複的值。set()函式可以接受一個陣列或者類陣列的引數,生成一個set物件。而Array.from方法用於將兩類物件轉為真正的陣列:類似陣列的物件(array-like object和可遍歷iterable)的物件包括 ES6 新增的資料結構 Set 和 Map)。
var arr = ['qiang','ming','tao','li','liang','you','qiang','tao']; function unique (arr) { return Array.from(new Set(arr)) } console.time("nonredundant2"); var nonredundant2 = unique(arr); console.timeEnd("nonredundant2"); console.log(nonredundant2);
結果如下:
3.3 使用for迴圈和splice
function unique(arr) { for (var i = 0; i < arr.length; i++) { for (var j = i + 1; j < arr.length; j++) { if (arr[i] == arr[j]) { //第一個等同於第二個,splice方法刪除第二個 arr.splice(j, 1); j--; } } } return arr; } console.time("nonredundant3"); var arr = ['qiang', 'ming', 'tao', 'li', 'liang', 'you', 'qiang', 'tao']; var nonredundant3 = unique(arr); console.timeEnd("nonredundant3"); console.log(nonredundant3);
結果如下:
3.4 使用indexOf判斷去重
function unique(arr) { var array = []; for (var i = 0; i < arr.length; i++) { if (array .indexOf(arr[i]) === -1) { array .push(arr[i]) } } return array; } var arr = ['qiang', 'ming', 'tao', 'li', 'liang', 'you', 'qiang', 'tao']; console.time("nonredundant4"); var nonredundant4 = unique(arr); console.timeEnd("nonredundant4"); console.log(nonredundant4);
結果如下:
3.5 使用sort排序去重
function unique(arr) { arr = arr.sort() var arrry = [arr[0]]; for (var i = 1; i < arr.length; i++) { if (arr[i] !== arr[i - 1]) { arrry.push(arr[i]); } } return arrry; } var arr = ['qiang', 'ming', 'tao', 'li', 'liang', 'you', 'qiang', 'tao']; console.time("nonredundant5"); var nonredundant5 = unique(arr); console.timeEnd("nonredundant5"); console.log(nonredundant5);
結果如下:
3.6 使用filter
function unique(arr) { var obj = {}; return arr.filter(function(item, index, arr){ return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true) }) } var arr = ['qiang', 'ming', 'tao', 'li', 'liang', 'you', 'qiang', 'tao']; console.time("nonredundant6"); var nonredundant6 = unique(arr); console.timeEnd("nonredundant6"); console.log(nonredundant6);
結果如下:
3.7 使用Map資料結構去重
function unique(arr) { let map = new Map(); let array = new Array(); // 陣列用於返回結果 for (let i = 0; i < arr.length; i++) { if (map.has(arr[i])) { // 如果有該key值 map.set(arr[i], true); } else { map.set(arr[i], false); // 如果沒有該key值 array.push(arr[i]); } } return array; } var arr = ['qiang', 'ming', 'tao', 'li', 'liang', 'you', 'qiang', 'tao']; console.time("nonredundant7"); var nonredundant7 = unique(arr); console.timeEnd("nonredundant7"); console.log(nonredundant7);
結果如下:
3.8 使用reduce和include去重
function unique(arr){ return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]); } var arr = ['qiang', 'ming', 'tao', 'li', 'liang', 'you', 'qiang', 'tao']; console.time("nonredundant8"); var nonredundant8 = unique(arr); console.timeEnd("nonredundant8"); console.log(nonredundant8);
結果如下:
4. 陣列隨機選取
這個需求在實際開發中也很常見,比如彩票隨機一注,隨機五注,機動車號牌隨機選一個等等。
4.1 使用Math.random()
這種方式是使用Array.sort()和Math.random()結合的方法,Math.random()返回的是一個0-1之間(不包括1)的偽隨機數,注意這不是真正的隨機數。
var letter = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; function shuffle1(arr) { return arr.sort(() => 0.5 - Math.random()) } console.time("shuffle1"); letter = shuffle1(letter); console.timeEnd("shuffle1"); console.log(letter);
這種方式並不是真正的隨機,來看下面的例子。對這個10個字母陣列排序1000次,假設這個排序是隨機的話,字母a在排序後的陣列中每個位置出現的位置應該是1000/10=100,或者說接近100次。看下面的測試程式碼:
let n = 1000; let count = (new Array(10)).fill(0); for (let i = 0; i < n; i++) { let letter = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; letter.sort(() => Math.random() - 0.5); count[letter.indexOf('a')]++ } console.log(count);
結果如下:
可以看出元素a的位置在0到9出現的次數並不是接近100的。
原因有兩點:
- Math.random()方法產生的偽隨機數並不是在0到1之間均勻分佈,不能提供像密碼一樣安全的隨機數字。
- Array.prototype.sort(compareFunction)方法中的compareFunction(a, b)回撥必須總是對相同的輸入返回相同的比較結果,否則排序的結果將是不確定的。
這裡sort(() => 0.5 - Math.random())沒有輸入,跟談不上返回相同的結果,所以這個方法返回的結果不是真正的陣列中的隨機元素。
4.2 隨機值排序
既然(a, b) => Math.random() - 0.5 的問題是不能保證針對同一組 a、b 每次返回的值相同,那麼我們不妨將陣列元素改造一下,比如將元素'a'改造為{ value: 'a', range: Math.random() },陣列變成[{ value: 'a', range: 0.10497314648454847 }, { value: 'b', range: 0.6497386423992171 }, ...],比較的時候用這個range值進行比較,這樣就滿足了Array.sort()的比較條件。程式碼如下:
let letter = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; function shuffle2(arr) { let new_arr = arr.map(i => ({value: i, range: Math.random()})); new_arr.sort((a, b) => a.r - b.r); arr.splice(0, arr.length, ...new_arr.map(i => i.value)); } console.time("shuffle2"); letter = shuffle2(letter); console.timeEnd("shuffle2"); console.log(shuffle2);
輸出結果如下:
我們再使用上面的方式測試一下,看看元素a元素是不是隨機分佈的。
let n = 1000, count = (new Array(10)).fill(0); for (let i = 0; i < n; i++) { let letter = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; letter = shuffle2(letter) count[letter.indexOf('a')]++ } console.log(count);
結果如下:
從這裡可以看出,元素a在位置0到9出現的次數是接近100的,也就是說元素a是隨機分佈的,其他的元素也是,這時再從這個新陣列中擷取前幾個元素就是想要的陣列了。
4.3 洗牌演算法
上面的sort演算法,雖然滿足了隨機性的需求,但是效能上並不是很好,很明顯為了達到隨機目的把簡單陣列變成了物件陣列,最後又從排序後的陣列中獲取這個隨機陣列,明顯走了一些彎路。
洗牌演算法可以解決隨機性問題,洗牌演算法的步驟如下:
- 陣列arr,有n個元素,存放從1到n的數值;
- 生成一個從0到n-1的隨機數x;
- 輸出arr下標為x的元素,即第一個隨機數;
- 將arr的尾元素和下標為x的元素值互換;
- 同步驟2,生成一個從0到n-2的隨機數x;
- 輸出arr下標為x的陣列,第二個隨機數;
- 將arr倒數第二個元素和下標為x的元素互換;
- 重複執行直至輸出m個數為止;
洗牌演算法是真的隨機的嗎,換言之洗牌演算法真的可以隨機得到n個元素中m個嗎?下面拿一個只有5個元素的陣列來說明。
陣列有5個元素,如下圖。
從5個元素隨機抽出一個元素和最後一個換位,假設抽到3,概率是1/5,如下圖。注意其他任意4個元素未被抽到的概率是4/5。最終3出現在最後一位的概率是1/5。
將抽到的3和最後一位的5互換位置,最後一位3就確定了,如下圖:
再從前面不確定的4個元素隨機抽一個,這裡注意要先考慮這4個元素在第一次未被抽到的概率是4/5,再考慮本次抽到的概率是1/4,然後乘一下得到1/5。注意其他任意3個未被抽到的概率是3/4。5出現在倒數第二位的概率是4/5*1/4=1/5如下圖:
現在最後2個元素確定了,從剩下的3個元素中任意抽取一個,概率是1/3,身下任意2個未被抽到的概率是2/3,但是要考慮上一次未被抽到的概率是3/4,以及上上一次未被抽到的概率是4/5,於是最終1出現在倒數第三位的概率是1/3*3/4*4/5=1/5。
現在倒數3個元素已經確定,剩下的2個元素中任意取一個,概率是1/2,但是要考慮上一次未被抽到的概率是2/3,上上一次未被抽到的概率是3/4,上上上一次未被抽到的概率是4/5,最終4出現在倒數第4位的概率是1/2*2/3*3/4*4/5=1/5。
最後還剩下一個2,它出現在倒數第5位的概率肯定也是1/5。不嫌囉嗦的話可以繼續看下去。
現在倒數4個元素已經確定,剩下1個元素中任意取一個,概率是1,但要考慮上一次未被抽中的概率是1/2,上上一次未被抽中的概率是2/3,上上上一次未被抽中的概率是3/4,上上上上一次未被抽中的概率是4/5,於是2出現在倒數5位置的概率是1*1/2*2/3*3/4*4/5=1/5。
有了演算法,下面給出洗牌演算法的程式碼:
let letter = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; function shuffle3(arr) { let i = arr.length, t, j; while (i) { j = Math.floor(Math.random() * (i--)); // t = arr[i]; arr[i] = arr[j]; arr[j] = t; } } console.time("shuffle3"); shuffle3(letter); console.timeEnd("shuffle3"); console.log(letter)
執行結果如下:
還有最後一個問題,我們來驗證一下,還是和上面的方法一樣,隨機排序1000次,看看字母a出現在0-9個位置的概率是多少,理論上應該是1000/10=100。來看下面的程式碼:
let n = 1000; let count = (new Array(10)).fill(0); function shuffle3(arr) { let i = arr.length, t, j; while (i) { j = Math.floor(Math.random() * (i--)); // t = arr[i]; arr[i] = arr[j]; arr[j] = t; } } for (let i = 0; i < n; i++) { let letter = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; shuffle3(letter); count[letter.indexOf('a')]++ } console.log(count);
結果如下:
可以看到基本上都是接近100的,可以說明洗牌演算法是隨機的。
5.陣列扁平化
陣列扁平化就是把一個多維陣列轉換成一維的。
5.2 flat方法
es6已經實現了陣列的flat方法,使用方法很簡單,如[1, [2, 3]].flat()。flat方法可以傳入一個參數列示最多處理多深的陣列。
var arr1 = [1, 2, [3, 4]]; arr1 = arr1.flat(); console.log(arr1); // [1, 2, 3, 4] var arr2 = [1, 2, [3, 4, [5, 6]]]; arr2 = arr2.flat(); console.log(arr2); //[1, 2, 3, 4, [5, 6]] var arr3 = [1, 2, [3, 4, [5, 6]]]; arr3 = arr3.flat(2); console.log(arr3); // [1, 2, 3, 4, 5, 6] var arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]]; arr4 = arr4.flat(Infinity); console.log(arr4); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
5.1 reduce實現
上面介紹陣列歸納的時候講到過reduce,它對陣列中的每個元素執行一個指定的函式,最終將結果彙總為單個返回值,然後配合concat方法合併兩個陣列,來實現扁平化。
let arr = [1, [2, 3, [4, 5]]]; function flatten1(arr) { return arr.reduce((prev, curr) => prev.concat(Array.isArray(curr) ? flatten1(curr) : curr), []); } console.time("flatten1"); console.log(flatten1(arr)); console.timeEnd("flatten1");
結果如下:
5.2 toString & split
呼叫陣列的toString方法,不管陣列有幾層,都可以將陣列變為字串並用逗號分隔,然後再使用split分隔,map還原為陣列。
let arr = [1, [2, 3, [4, 5]]]; function flatten2(arr) { return arr.toString().split(",").map(i => Number(i)); } console.time("flatten2"); console.log(flatten2(arr)); console.timeEnd("flatten2");
結果如下:
5.3 join & split
呼叫對陣列呼叫join方法,不管陣列有幾層,都可以將陣列變成字串並用逗號分隔,然後使用split分隔,map還原為陣列。
let arr = [1, [2, 3, [4, 5]]]; function flatten3(arr) { return arr.join().split(",").map(i => parseInt(i)); } console.time("flatten3"); console.log(flatten3(arr)); console.timeEnd("flatten3");
結果如下:
5.4 遞迴
這種方法和第一種類似,只不過是用map得到陣列。
function flatten4(arr) { let res = []; arr.map(item => { if(Array.isArray(item)) { res = res.concat(flatten4(item)); } else { res.push(item); } }); return res; } let arr = [1, [2, 3, [4, 5]]]; console.time("flatten4"); console.log(flatten4(arr)); console.timeEnd("flatten4");
結果如下:
5.6 擴充套件運算子
es6中的擴充套件運算子可以展開陣列,將陣列元素轉換成逗號分隔的物件。
function flatten5(arr) { while (arr.some(item => Array.isArray(item))) { arr = [].concat(...arr); } return arr; } let arr = [1, [2, 3, [4, 5]]]; console.time("flatten5"); console.log(flatten5(arr)); console.timeEnd("flatten5");
結果如下:
5.6 使用Generator函式
Generator函式也可以遞迴呼叫,這種方式比較新穎。
function* flatten6(array) { for (const item of array) { if (Array.isArray(item)) { yield* flatten6(item); } else { yield item; } } } let arr = [1, [2, 3, [4, 5]]]; console.time("flatten6"); console.log(flatten6(arr)); console.timeEnd("flatten6");
結果如下: