likes
comments
collection
share

盘点一些硬核JavaScript代码优化

作者站长头像
站长
· 阅读数 54

前言

大家好这里是阳九,一个文科转码的野路子全栈码农,热衷于研究和手写前端工具.

很久没有重学JS了, 最近巩固了一下JS基础, 整理了一些平日大家容易忽略又稍稍有点硬核的JS代码优化方案,属于JS基础篇。

同时附上我的github求个赞哈 github.com/lzy19926

面向对象相关优化

隐藏类优化思想

  1. 在我们new 一个对象实例时, 在V8底层,会创建一个该Object对应的隐藏类, 如果利用一个构造函数创建多个Object实例,且实例的属性和方法相同时,会复用这个隐藏类。
  2. 如果我们对实例动态添加了其他属性,那么V8在创建对象时就会重新创建一个隐藏类, 占用更多内存,速度变慢。
  3. 解决方案 避免"创建再补充" 或者创建再删除 在类一开始的时候就声明好所有属性
class Article{ 
    constructor(){ 
      this.title = "我是title";
    }
}
// 此时在V8底层 a1和a2会共用一个隐藏类  
let a1 = new Article()
let a2 = new Article()
let a3 ...
// 如果动态添加了其他属性,则会给a2单独创建一个隐藏类
a2.author = "张三"

使用对象池

  1. 有时候我们需要频繁创建销毁同一种类型的对象, 如果在外部频繁的创建对象,对象生命周期会很短,会导致垃圾回收频繁执行
  2. 可以使用对象池,统一管理一组可回收的对象
  3. 核心思路是,通过手动控制垃圾回收,减少垃圾回收执行的次数,提高运行速度(空间换时间)
  4. 同时对象池也可以作为缓存,实现对象复用
// 对象
class Vector {
    constructor() { }
}

// 对象池
class VectorPool {
    constructor() {
        this.instances = new Set()
    }

    create() {
        const instance = new Vector()
        this.instances.add(instance)
        return instance
    }

    clear() {
        this.instances.clear()
    }
}

let pool = new VectorPool()

// 创建多个实例
let v1 = pool.create();
let v2 = pool.create();
let v3 = pool.create();

pool.clear()

JS API优化

多用Math进行数字计算

  1. Math 对象上提供的计算要比直接在 JavaScript 实现的快得多,因为 Math 对象上的计算使用了 JavaScript 引擎中更高效的实现和处理器指令。
// 使用math获取数组最小值(快)
const arr = [1,2,3,4,5,6,7,8]  
Math.min(...arr)

// 自己写函数获取最小值(慢)
function getMin(){...}

定型数组+ArrayBuffer替代JS数组

我们知道 JS数组是通过对象包装而来,并不是真正的数组,而真正的数组, 在内存中线性存储, 需要定义数组长度,数组元素类型等等。

JS本身提供了定型数组类型,设计目的是为了提高与 WebGL 等原生库交换二进制数据的效率

  1. 定型数组的二进制表示对操作系统而言是一种容易使用的格式,速度极快
  2. 需要注意 定型数组支持普通数组的一些方法和属性 sort,slice等 迭代器[Symbol.iterator], 但是由于无法调整大小 因此pop push等方法不适用
  3. 如果有大量存储基本数据类型,并不需要调整大小的场景,使用定型数组会更快
// 创建一个 12 字节的缓冲
const buf = new ArrayBuffer(12);
// 创建一个引用该缓冲的 Int32Array (当然还有Int8等  可以进行选择   减少空间占用)
const ints = new Int32Array(buf);
console.log(ints.length) // 长度为3   4*3 = 12

// 创建一个包含[2, 4, 6, 8]的 Int32Array 
const ints3 = new Int32Array([2, 4, 6, 8]);

使用Map替代Object

如果要追求极致的性能, 在一些场景下需要好好考虑使用Map还是Object数据类型, 以下是Map相对于Object的对比

  1. 给出相同的内存空间, Map 大约可以比 Object 多存储 50%的键/值对(节约空间)
  2. Map (在大多浏览器中,插入和删除字段的性能更佳,性能差距呈线性增长)
  3. Object的查找性能更好(好的不多) (特别是属性少的情况下)

JS代码优化

循环优化 达夫循环设备

  1. 想必大家都对比过一些迭代方法API的速度 forEach for循环等, for循环语句一般来说肯定是最快的。
  2. 但实际上,使用循环语句也是有消耗的,展开循环比使用循环语句要快的多
  3. 核心思路: 分组将循环展开
// 循环调用
for (let i = 0; i < 3; i++) {
    foo()
}
// 多次调用函数 更快
foo()
foo()
foo()

根据此理论,衍生出了达夫设备(Daff Divice), 以8的倍数作为迭代次数从而将循环展开为一系列语句 尽可能在代码长度和循环语句消耗之间达成平衡。在循环执行大量且简单的逻辑时很好用

(理论上 展开的越多 性能越好 但是代码量越多)

// 将循环拆解为两部分   比如循环43次 可以拆为 3 + 5*8 次循环
let times = 43;
let iterations = Math.floor(times / 8);
let leftover = times % 8; // 循环第一部分

// 循环第一部分
if (leftover > 0) {
    do {
        process();
    } while (--leftover > 0);
}
// 循环第二部分 8次为一组(do while比普通的while和for更快  因为少一次初始条件判断)
do {
    process();
    process();
    process();
    process();
    process();
    process();
    process();
    process();
} while (--iterations > 0);

尾调用优化

想必刷过leetCode的同学都有使用过这种优化方式

尾调用:

  1. ECMAScript 6 规范新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用函数调研栈帧,这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值
  2. 这个优化在递归场景下的效果是最明显的,因为递归代码最容易在栈内存中迅速产生大量栈帧,每个函数调用都会创建一个新的函数调用栈帧,推入调用栈,使用此方式可以复用栈帧,避免了频繁创建删除栈帧
// 连续调用两个函数 会往函数调用栈内推入两个栈帧
function test1() { }
function test2() { }

test1()
test2()

// 在test1中调用test2并返回,JS引擎会优化,自始自终只有一个栈帧
function test1() { }
function test2() {
    return test1()
}

test2()

// 如果是递归函数  会不停添加栈帧,很容易导致栈溢出
function deep() {
    if (condition) {
        deep()
    }
}

// 使用尾调用 (尾递归) 则可以优化这个问题
function deep() {
    if (condition) {
        return deep()
    }
}

作用域链 变量访问加速

  1. 代码中每层作用域相互叠加,会形成作用域链
  2. 在访问变量时,作用域链越长,查找变量所需要的时间也会越多
  3. 核心思路: 避免跨层级(跨作用域链)去访问变量, 频繁访问的变量需要离调用者近一些。
  4. 一般来说 全局变量和函数跨的作用域链一定是最长的 可以适当将全局变量存入局部 比如document等全局对象
var num = 0 // 全局变量

// 跨多层函数作用域去访问变量num
function test1() {
    function test2() {
        function test3() {
            console.log(num);
        }
        test3()
    }
    test2()
}
test1()


// 实际例子  多次访问一个全局变量  
function test() {   // 循环中多次访问了document全局对象
    for (let i = 0; i < 1000; i++) {
        let div = document.getElementById("div");
    }
}

function test_2() {
    let doc = document  // 将document保存为局部变量  加速访问
    for (let i = 0; i < 1000; i++) {
        let div = doc.getElementById("div");
    }
}

Switch代替if else

  1. switch属于原生语句 比if else更快, 默认这一条大家都会,不再赘述

语句最少化

  1. 以分号为间隔计算,语句数量越少 速度越快(这一点也是我忽视的一点,以后再也不写一大堆let了)
// 插入迭代性值
let name = values[i];
i++;
// 优化为
let name = values[i++];

// 多个let声明优化为单条语句
let count = 5;
let color = "blue";

// 
let count = 5,
    color = "blue"

Dom操作相关优化

需要补充的是,一般Dom操作的优化在当下都被框架所封装好了,真正能使用的场景不多,但是需要了解。

Dom插入优化

  1. 一般来讲,插入大量的新Dom节点使用innerHTML 比使用普通DOM操作创建节点再插入来得更快(使用createElement 和appendChild)
  2. 因为 HTML 解析器会解析设置给innerHTML/outerHTML的值。解析器在浏览器中是底层代码(C++代码),比 JavaScript快得多。
  3. 这里需要注意, 限制innerHTML的使用次数 因此最好的方式是一次性创建大量html字符串,统一插入
// 效率低
for (let value of values) {
    ul.innerHTML += '<li>${value}</li>'; 
}

// 最佳实践
let itemsHtml = "";
for (let value of values) {
    itemsHtml += '<li>${value}</li>';
}
ul.innerHTML = itemsHtml;

Dom事件相关优化

  1. JavaScript页面中事件处理程序的数量与页面整体性能直接相关,如何减少事件数量在JS性能优化中至关重要
  2. 优化方案也就我们常说的事件代理,利用事件冒泡特性代理到上层元素,或者是利用事件合成减少事件数量.(比较常见简单 不过多赘述)
  3. 这里需要注意的点是, 需要及时垃圾回收掉不需要的事件处理程序。

减少HTMLCollection的访问

  1. HTMLCollection, Dom集合,伪数组。最常见的情况是使用getElementByTagName获取到。
  2. 只要访问HTMLCollection就会触发查询文档,这个查询相当耗时, 尽量不访问HTMLCollection元素
转载自:https://juejin.cn/post/7246570594991407159
评论
请登录