高性能 JavaScript - 算法和流程控制

这几天分享一下我看《高性能 JavaScript》的学习笔记,希望能对大家有所帮助。

循环

在 JavaScript中一共有以下几种循环:

  • while(i < len) { ... }
  • do { ... } while(i < len)
  • for (let i = 0; i < len; i++) { ... }
  • for (let key in obj) { ... }
  • 还有 ES6 中添加的 for (let value of obj) { ... }

这些循环方法中,前三种循环方式性能最佳。后面两种循环需要搜索对象实例和原型,所以必有更多的性能开销。

如何提高循环性能

需要循环性能的好坏主要看两个点:

  1. 每次迭代要处理的事务复杂度。
  2. 迭代的次数。

那么有哪些优化循环的方法呢?

  • 限制循环迭代中耗时操作的数量。
  • 减少对象成员及数组的查找次数,如 obj.name 或者 arr.length 。可以使用局部变量进行保存。
  • 倒序循环的效率比正序循环稍高。

减少迭代次数

有一种叫达夫设备的优化方案,它主要思想是通过展开循环体来减少迭代的次数。

var iterations = Math.floor(items.length / 8),
    startAt = items.length % 8,
    i = 0;

do {
    switch (startAt) {
        case 0: precess(items[i++]);
        case 7: precess(items[i++]);
        case 6: precess(items[i++]);
        case 5: precess(items[i++]);
        case 4: precess(items[i++]);
        case 3: precess(items[i++]);
        case 2: precess(items[i++]);
        case 1: precess(items[i++]);
    }
    startAt = 0
} while (--iterations);

forEach 循环

数组提供了 forEach() 函数让我们可以遍历数组内容。

虽然 forEach() 函数是更为便利的迭代方法,因为数组项调用外部方法回来带开销。基于循环的迭代比基于函数的迭代快 8 倍。

条件

在 JavaScript 中,主要的条件判断方式有两种:

  • if-else
  • switch

条件数量越大,越是倾向于使用 switch 来判断。因为 switch 在大量判断数据下易读性更好,而且在大多数情况下大数据量的判断 switch 的性能会更佳。

如果必须用 if-else 判断有规律的数据,可以使用二分法减少查询条件的次数:

if (value < 6) {
    if (value < 3) {
        if (value == 0) {
            return 'value is 0'
        } else if (value == 1) {
            return 'value is 1'
        } else {
            return 'value is 2'
        }
    } else {
        if (value == 3) {
            return 'value is 3'
        } else if (value == 4) {
            return 'value is 4'
        } else {
            return 'value is 5'
        }
    }
} else {
    if (value < 8) {
        if (value == 6) {
            return 'value is 6'
        } else {
            return 'value is 7'
        }
    } else {
        if (value == 8) {
            return 'value is 8'
        } else if (value == 9) {
            return 'value is 9'
        } else {
            return 'value is 10'
        }
    }
}

书中还提出了一种面对大量离散值时的优化方案 —— 通过数组或对象构建查找表,然后从数组或对象中查找数据结果。

虽然查找数组和对象的属性需要一定性能开销,但是比条件判断的速度要快。而且可读性也更好。

// switch 写法
function switchFunc(value) {
    switch (value) {
        case 0: return 'value is 0';
        case 1: return 'value is 1';
        case 2: return 'value is 2';
        case 3: return 'value is 3';
        case 4: return 'value is 4';
        case 5: return 'value is 5';
        case 6: return 'value is 6';
        case 7: return 'value is 7';
        case 8: return 'value is 8';
        case 9: return 'value is 9';
        case 10: return 'value is 10';
    }
}
// 查找表写法
function searchFunc(value) {
    var results = [
        'value is 0',
        'value is 1',
        'value is 2',
        'value is 3',
        'value is 4',
        'value is 5',
        'value is 6',
        'value is 7',
        'value is 8',
        'value is 9',
        'value is 10'
    ]

    return results[value]
}

递归

递归就是在函数中直接或间接调用函数自身的行为。一般递归都会有一个停止条件的判断,满足停止条件后停止递归行为。递归有两种模式:

  • 直接递归模式
function recurse() {
  recurse();
}

recurse();
  • 隐伏模式
function first() {
  second();
}

function second() {
  first();
}

first();

然而,如果递归次数过多过快,会触发调用栈限制错误,幸好这种错误可以通过 try ... catch 来捕获。

try {
  recurse()
} catch (ex) {
  alert("too much recursion")
}

其实,使用递归并非是最好的选择。书上提到运行一个循环的速度远快于调用函数自身(递归)。所以如果可以用循环来实现的逻辑尽量不要用递归来做。

Memoization 技术

众所周知,代码处理的逻辑越少,运行速度必然越快。所以我们应该避免做一些重复的工作。做法就是通过对象将一些重复工作的行为结果缓存下来,第一次之后的工作直接使用缓存结果。

function memoize(fundamental, cache) {
    cache = cache || {};

    var shell = function(arg) {
        if (!cache.hasOwnProperty(arg)) {
            cache[arg] = fundamental
        }
        return cache[arg]
    }

    return shell;
}

这里将添加的行为存到 cache 对象中来避免重复工作,这样做是可以提升性能的。

小结

本文主要说了一些 JavaScript 循环、条件、遍历、递归等算法的优化思路。这些知识点在实现复杂算法的时候是很有用的。下面简单整理下~

  • 不同的算法在数据量大的情况下性能差别很大。
  • 遍历行为要尽量减少迭代次数、减少行为的耗时操作。可以使用缓存对象来避免一些重复行为的发生。
  • 递归时要注意 JavaScript 有调用栈限制。
  • 查找表技术很适合大量离散表的条件判断查找工作。

对于算法和流程控制这方面,建议多学习一些算法知识。这样在真正面对复杂流程时可以知道有哪些算法可以提升性能了。

你可能感兴趣的:(高性能 JavaScript - 算法和流程控制)