JavaScript數組常見用法

最近作一個項目中作一個競猜遊戲界面,遊戲規則和彩票是同樣的。在實現「機選一注」,「機選五注」的時候遇到數組的一些操做,例如產生['01', '02' ... '35']這樣的數組,隨機抽取不重複的元素從新組成數組等問題。回想這類問題在平時項目中遇到的機會很是多,何不概括一下JavaScript數組的一些知識點,以供平時工做參考。javascript

JavaScript提供的數組很是靈活,相關的api也很豐富,例如fill,map,filter,sort等等,極大地方便了程序編寫。這裏不介紹這些基本的api,而是經過工做中經常使用的使用場景來展現數組的強大。前端

1.概括計算

在一個分頁表格中好比訂單表,要求根據訂單金額展現這一頁的訂單總金額。不少時候後端開發偷懶,把這種計算推給前端,可使用reduce輕鬆實現個功能,代碼以下。vue

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表達式計算表格某一列的概括總和,很方便,示例代碼以下:java

<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>&nbsp;</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就太麻煩了。這時就要本身先生成一些數據,最多見的就是生成一個對象列表。下面就來討論生成數據的方式。es6

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()方法給數組元素生成有意義的值。api

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()

Array.from:方法從一個相似數組或可迭代對象建立一個新的,淺拷貝的數組實例。代碼以下:安全

console.time("arr2");
let arr2 = Array.from(new Array(10), (value, index) => {
    return ++index;
});
console.timeEnd("arr2");
console.log(arr2);

 

執行結果以下:

 

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的。

緣由有兩點:

  1. Math.random()方法產生的僞隨機數並非在0到1之間均勻分佈,不能提供像密碼同樣安全的隨機數字。
  2. 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算法,雖然知足了隨機性的需求,可是性能上並非很好,很明顯爲了達到隨機目的把簡單數組變成了對象數組,最後又從排序後的數組中獲取這個隨機數組,明顯走了一些彎路。

洗牌算法能夠解決隨機性問題,洗牌算法的步驟以下:

  1. 數組arr,有n個元素,存放從1到n的數值;
  2. 生成一個從0到n-1的隨機數x;
  3. 輸出arr下標爲x的元素,即第一個隨機數;
  4. 將arr的尾元素和下標爲x的元素值互換;
  5. 同步驟2,生成一個從0到n-2的隨機數x;
  6. 輸出arr下標爲x的數組,第二個隨機數;
  7. 將arr倒數第二個元素和下標爲x的元素互換;
  8. 重複執行直至輸出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的,能夠說明洗牌算法是隨機的。

4.4 機選彩票

最近開發作了一個模擬彩票遊戲的功能,彩票有機選,其實就是隨機選取,下面以雙色球爲例來看看實現的效果是什麼樣的。雙色球前區從1到33個小球,後區從1到16個小球,一注彩票中前區至少選6個,後區至少選1個。這裏使用洗牌算法實現,以下圖:

5.數組扁平化

數組扁平化就是把一個多維數組轉換成一維的。

5.1 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.2 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.3 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.4 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.5 遞歸

這種方法和第一種相似,只不過是用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.7 使用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"); 

結果以下:

6.總結

數組是平時開發時最經常使用到的數據結構,充分了解數組的技巧以及相關的api對開發工做大有幫助,首先要作到的是瞭解這些知識點,而後重要的是在開發中可以使用起來,這樣才能讓他們迸發出生命力。本文若有錯誤,請你們指正。

相關文章
相關標籤/搜索