15

JavaScript数组常见用法

 3 years ago
source link: http://www.cnblogs.com/tylerdonet/p/12898911.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近做一个项目中做一个竞猜游戏界面,游戏规则和彩票是一样的。在实现“机选一注”,“机选五注”的时候遇到数组的一些操作,例如产生['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到2 32 -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);

输出结果如下:

nyEjym2.png!web

2.2 Array()&Array.from()

MZVru2e.png!web

2.3 使用递归

使用了递归和立即执行函数来生成数组。

console.time("arr3")
let arr3 = (function wallace(i) {
    return (i < 1) ? [] : wallace(i - 1).concat(i);
})(10);
console.timeEnd("arr3");

执行结果如下:

MfIbMnb.png!web

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

执行结果如下:

FrEJ3aV.png!web

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

执行结果如下:

MzMVVv6.png!web

2.6 使用apply和类数组对象

console.time("arr6");
let arr6 = Array.apply(null, {length: 10}).map((value, index) => index + 1);
console.timeEnd("arr6");
console.log(arr6);

结果如下:

bE73Qrv.png!web

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

结果如下:

vY36zuU.png!web

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

结果如下:

bEZbi2r.png!web

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

结果如下:

YRVVvmm.png!web

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

结果如下:

UraQRbi.png!web

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

结果如下:

nMniAr2.png!web

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

结果如下:

Yf26na6.png!web

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

结果如下:

NnmmyeY.png!web

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

结果如下:

iauaiyI.png!web

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

结果如下:

Jj6baqy.png!web

可以看出元素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); 

输出结果如下:

equyQ3j.png!web

我们再使用上面的方式测试一下,看看元素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); 

结果如下:

6Rjqiuu.png!web

从这里可以看出,元素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个元素,如下图。

eeyuY3E.png!web

从5个元素随机抽出一个元素和最后一个换位,假设抽到3,概率是1/5,如下图。注意其他任意4个元素未被抽到的概率是4/5。最终3出现在最后一位的概率是1/5。

Bju6zun.png!web

将抽到的3和最后一位的5互换位置,最后一位3就确定了,如下图:

A3aYRju.png!web

再从前面不确定的4个元素随机抽一个,这里注意要先考虑这4个元素在第一次未被抽到的概率是4/5,再考虑本次抽到的概率是1/4,然后乘一下得到1/5。注意其他任意3个未被抽到的概率是3/4。5出现在倒数第二位的概率是4/5*1/4=1/5如下图:

2E3Qjy3.png!web

现在最后2个元素确定了,从剩下的3个元素中任意抽取一个,概率是1/3,身下任意2个未被抽到的概率是2/3,但是要考虑上一次未被抽到的概率是3/4,以及上上一次未被抽到的概率是4/5,于是最终1出现在倒数第三位的概率是1/3*3/4*4/5=1/5。

AFFziez.png!web

现在倒数3个元素已经确定,剩下的2个元素中任意取一个,概率是1/2,但是要考虑上一次未被抽到的概率是2/3,上上一次未被抽到的概率是3/4,上上上一次未被抽到的概率是4/5,最终4出现在倒数第4位的概率是1/2*2/3*3/4*4/5=1/5。

bmI7juV.png!web

最后还剩下一个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) 

运行结果如下:

qmIjmmA.png!web

还有最后一个问题,我们来验证一下,还是和上面的方法一样,随机排序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); 

结果如下:

N7fiYzY.png!web

可以看到基本上都是接近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"); 

结果如下:

EVJNVvN.png!web

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"); 

结果如下:

VnMbquq.png!web

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"); 

结果如下:

VzAzArJ.png!web

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"); 

结果如下:

rqUv2aZ.png!web

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"); 

结果如下:

rAnuAzu.png!web

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"); 

结果如下:

y26N7b2.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK