JS数组拷贝与查找性能分析

2,714 阅读5分钟

概述

JS对数组的循环从一开始的for, while到ES5的forEach,map在ES6中又新增了filter,includes等方法。越发展,JS对数组的操变得更加灵活,我们可以用几行代码更优雅的解决问题。本文单单从运行速度方面对各个数组方法进行比较,希望在之后的业务场景中,在数组的操作方式的选择上,能有个参考。



循环浅拷贝对比

先说结论,推荐map, while, for

浅拷贝中,涉及到的函数有

  • for
  • for in
  • for of
  • while
  • forEach
  • map
const sourceArray = new Array(10000000).fill(9)

const methodFor = () => {
    console.time('methodFor')
    const array = []
    const length = sourceArray.length
    for(let i = 0; i < length; i ++) {
        array.push(sourceArray[i])
    }
    console.timeEnd('methodFor')
}

const methodForIn = () => {
    console.time('methodForIn')
    const array = []
    for(let i in sourceArray) {
        array.push(sourceArray[i])
    }
    console.timeEnd('methodForIn')
}

const methodForOf = () => {
    console.time('methodForOf')
    const array = []
    for(let i of sourceArray) {
        array.push(sourceArray[i])
    }
    console.timeEnd('methodForOf')
}

const methodWhile = () => {
    console.time('methodWhile')
    const array = []
    let i = 0
    while(i < sourceArray.length) {
        array.push(sourceArray[i])
        i ++
    }
    console.timeEnd('methodWhile')
}

const methodForEach = () => {
    console.time('methodForEach')
    const array = []
    let reslut
    sourceArray.forEach(i => {
        array.push(i)
    })
    console.timeEnd('methodForEach')
}

const methodMap = () => {
    console.time('methodMap')
    let array = []
    let reslut
    array = sourceArray.map(i => i)
    console.timeEnd('methodMap')
}

methodFor()
methodForIn()
methodForOf()
methodWhile()
methodForEach()
methodMap()


chrome环境(已排序)
methodMap: 150.634033203125ms
methodWhile: 117.323974609375ms
methodForOf: 228.681640625ms
methodForEach: 229.51416015625ms
methodFor: 249.433837890625ms
methodForIn: 3063.458740234375ms


node环境(已排序)
methodMap: 163.163ms
methodFor: 224.809ms
methodWhile: 229.365ms
methodForOf: 333.059ms
methodForEach: 372.001ms
methodForIn: 5820.964ms


从多次结果展示,map本是ES5中用来返回一个操作之后的数组,所以在先拷贝中表现优异,另外则可考虑while的使用。


另外可以发现,for in 慢的出奇。原因有两个。首先for in中是那数组中的索引去检索,let的i的类型是string类型的,所以在取sourceArray[i]的时候,会先进行类型转换,而类型转换则相对消耗性能。另外则是for in会遍历数组或者对象的属性,而且必须按特定顺序遍历,先遍历所有数字键,然后按照创建属性的顺序遍历剩下的,所以非常非常慢,最不推荐。


查找对比

查找对比分为两部分,一种是简单的判断数组中是否存在某元素,另一种则是需要特定找到某元素

仅判断是否存在

先说结论:推荐使用includes,indexOf

涉及到的方法有

  • for
  • for of
  • while
  • find
  • some
  • includes
  • indexOf
const sourceArray = new Array(10000000).fill(9)
sourceArray[999999] = 1


const methodFor_length = () => {
    console.time('methodFor_length')
    const length = sourceArray.length
    let reslut = false
    for(let i = 0; i < length; i ++) {
        if(sourceArray[i] === 1) {
            reslut = true
            break
        }
    }
    console.timeEnd('methodFor_length')
}

const methodForOf = () => {
    console.time('methodForOf')
    let reslut = false
    for(let i of sourceArray) {
        if(sourceArray[i] === 1) {
          reslut = true
            break
        }
    }
    console.timeEnd('methodForOf')
}

const methodWhile = () => {
    console.time('methodWhile')
    let i = 0
    let reslut = false
    while(i < sourceArray.length) {
        if(sourceArray[i] === 1) {
          reslut = true
            break
        }
        i ++
    }
    console.timeEnd('methodWhile')
}

const methodForEach = () => {
    console.time('methodForEach')
    let reslut = false
    sourceArray.forEach(i => {
        if(i === 1) {
          reslut = true
            return
        }
    })
    console.timeEnd('methodForEach')
}


const methodFind = () => {
    console.time('methodFind')
    const reslut = sourceArray.find(i => i === 1)
    console.timeEnd('methodFind')
}


const methodIndexOf = () => {
    console.time('methodIndexOf')
    const reslut = sourceArray.indexOf(1) === -1
    console.timeEnd('methodIndexOf')
}

const methodIncludes = () => {
    console.time('methodIncludes')
    const reslut = sourceArray.includes(1)
    console.timeEnd('methodIncludes')
}

const methodSome = () => {
    console.time('methodSome')
    const reslut = sourceArray.some(i => i === 1)
    console.timeEnd('methodSome')
}

methodFor_length()
methodForOf()
methodWhile()
methodForEach()
methodIncludes()
methodIndexOf()
methodFind()
methodSome()

chrome(已排序)
methodIncludes: 1.006103515625ms
methodIndexOf: 1.002197265625ms
methodWhile: 2.119873046875ms
methodFor_length: 2.72705078125ms
methodFind: 11.995849609375ms
methodSome: 11.60009765625ms
methodForEach: 116.1689453125ms
methodForOf: 145.723876953125ms

node环境(已排序)
methodIncludes: 1.132ms
methodIndexOf: 1.137ms
methodWhile: 2.359ms
methodFor_length: 3.448ms
methodFind: 11.441ms
methodSome: 11.330ms
methodForEach: 104.478ms
methodForOf: 122.109ms



从结果看,如果单是用于判断是否存在的话,includes与indexOf几乎完胜。另外从语义上来讲,includes似乎也更胜一筹。

由于forEach,map等无法跳出循环,会将所有对象都循环一遍,耗时很厉害。

find与some则差的不多,更多的应用场景是对数组对象中属性的判断,毕竟includes与indexOf不适合判断复杂数据类型。find返回的是数组中找到的第一个符合要求的值(或者是对象的引用),some则返回的是布尔值,所以结合业务场景,各取所需。



查找并返回具体值的对比

涉及到的方法有

  • for
  • for of
  • while
  • indexOf
  • forEach
  • filter
  • find
  • findIndex
const sourceArray = new Array(10000000).fill(9)
sourceArray[999999] = 1

const methodFor_length = () => {
    console.time('methodFor_length')
    let reslut
    const length = sourceArray.length
    for(let i = 0; i < length; i ++) {
        if(sourceArray[i] === 1) {
            reslut = sourceArray[i]
            break
        }
    }
    console.timeEnd('methodFor_length')
}

const methodForOf = () => {
    console.time('methodForOf')
    let reslut
    for(let i of sourceArray) {
        if(i === 1) {
            reslut = i
            break
        }
    }
    console.timeEnd('methodForOf')
}

const methodWhile = () => {
    console.time('methodWhile')
    let reslut
    let i = 0
    while(i < sourceArray.length) {
        if(sourceArray[i] === 1) {
            reslut = sourceArray[i]
            break
        }
        i ++
    }
    console.timeEnd('methodWhile')
}

const methodForEach = () => {
    console.time('methodForEach')
    let reslut
    sourceArray.forEach(i => {
        if(i === 1) {
            reslut = i
            return
        }
    })
    console.timeEnd('methodForEach')
}

const methodFind = () => {
    console.time('methodFind')
    const reslut = sourceArray.find(i => i === 1)
    console.timeEnd('methodFind')
}

const methodFindIndex = () => {
    console.time('methodFindIndex')
    const index = sourceArray.findIndex(i => i === 1)
    let reslut = sourceArray[index]
    console.timeEnd('methodFindIndex')
}

const methodIndexOf = () => {
    console.time('methodIndexOf')
    const index = sourceArray.indexOf(1)
    let reslut = sourceArray[index]
    console.timeEnd('methodIndexOf')
}

const methodFilter = () => {
    console.time('methodFilter')
    const reslut = sourceArray.filter(i => i === 1)
    console.timeEnd('methodFilter')
}

methodFor_length()
methodForOf()
methodWhile()
methodForEach()
methodFind()
methodFindIndex()
methodIndexOf()
methodFilter()

chrome环境(已排序)
methodIndexOf: 1.01171875ms
methodWhile: 3.039794921875ms
methodFor_length: 3.203125ms
methodFindIndex: 11.323974609375ms
methodFind: 11.60498046875ms
methodForOf: 27.98193359375ms
methodFilter: 112.635009765625ms
methodForEach: 116.5859375ms

node环境(已排序)
methodIndexOf: 1.399ms
methodWhile: 3.489ms
methodFor_length: 4.273ms
methodFind: 11.101ms
methodFindIndex: 11.513ms
methodForOf: 24.731ms
methodForEach: 107.206ms
methodFilter: 108.067ms

从数据上看,indexof与while, for 从性能上来说,速度非常突出。forEach依旧是最不推荐的。


总结

但从速度上来说,for, while, indexOf这些当数据量庞大的时候,速度优势非常很明显。但是从代码量上来讲旧方法定义更多的变量,容易造成冗余的代码。新特性所定义的方法,语义更强,书写更优雅。特别是返回的数据格式,更容易把控。所以从业务上来说,孰优孰劣则需要前端自己选择。

另外提一句,对于循环数组的顺序,

For循环

最后简单比较下for循环在我最早期接触代码的时候,就看到过文章对for循环的优化方式之一便是讲length提出或者将循环对象缓存起来。如下代码

const sourceArray = new Array(10000000).fill(9)

const methodFor = () => {
    console.time('methodFor')
    for(let i = 0; i < sourceArray.length; i ++) {
    }
    console.timeEnd('methodFor')
}

const methodFor_length = () => {
    console.time('methodFor_length')
    const length = sourceArray.length
    for(let i = 0; i < length; i ++) {
    }
    console.timeEnd('methodFor_length')
}

const methodFor_item = () => {
    console.time('methodFor_item')
    for(let i = 0, item; item = sourceArray[i]; i++) {
    }
    console.timeEnd('methodFor_item')
}

methodFor()
methodFor_length()
methodFor_item()

chrome环境
methodFor: 6.444091796875ms
methodFor_length: 4.895751953125ms
methodFor_item: 9.989013671875ms

node环境
methodFor: 7.448ms       
methodFor_length: 6.159ms
methodFor_item: 19.123ms

从实验结果看,在for循环中,将length缓存,明显的提高了运行速率。以为在循环中,没有对子元素读取,方法methodFor_item将item缓存起来,多了每次循环的缓存步骤,所以变慢了


如果我们在循环当中,对子元素进行了读取赋值操作,则结果不同

const sourceArray = new Array(10000000).fill(9)

const methodFor = () => {
    console.time('methodFor')
    const arr = []
    for(let i = 0; i < sourceArray.length; i ++) {
        arr.push(sourceArray[i])
    }
    console.timeEnd('methodFor')
}

const methodFor_length = () => {
    console.time('methodFor_length')
    const length = sourceArray.length
    const arr = []
    for(let i = 0; i < length; i ++) {
        arr.push(sourceArray[i])
    }
    console.timeEnd('methodFor_length')
}

const methodFor_item = () => {
    console.time('methodFor_item')
    const arr = []
    for(let i = 0, item; item = sourceArray[i]; i++) {
        arr.push(item)
    }
    console.timeEnd('methodFor_item')
}

methodFor()
methodFor_length()
methodFor_item()

chrome环境
methodFor: 138.826171875ms
methodFor_length: 114.406982421875ms
methodFor_item: 116.247802734375ms

node环境下
methodFor: 225.091ms
methodFor_length: 207.214ms
methodFor_item: 226.828ms

在chrome下,有缓存则明显提高运行速率。而在node环境下,缓存子元素似乎效果不大。

综上所述,将循环中的length缓存起来,对提高循环速率有蛮大影响,并且从阅读来说,也是有益的