学写提高性能的代码之数据存取

244 阅读4分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

前言

无论是哪种计算机编程语言,说到底它们的作用都是对数据的存取与处理,JavaScrpt也不例外。若能在处理数据之前,更快速地读取到数据,那么必然会对程序执行性能产生积极的作用。本文将从数据的存取及作用域链的角度,记录一些在 JavaScrpt编程中能提升性能的方式。


实战优化

对局部变量的使用

如果一个非局部变量在函数中的使用次数不止一次,那么最好使用局部变量进行存储。举个例子,代码如下:

function process() {
  const target = document.getElementById('target');  
  const imgs = documentetElementByClassName('img');  
  for(let i = 0; i < imgs.length ;i++)(  
    const img = imgs[i];  
    //省略相关处理流程  
    //...
    target.appendChild(img) 
  )
}

在函数process()中,我们首先通过document的两个不同的成员函数分别获取了特定的元素和元素列表,然后进行一些省略相关处理流程的操作。值得注意的是,document属于全局作用域的对象,位于作用域链的最深处,在标识符解析过程中会被最后解析到。由于它在此函数中使用了不止一次,所以可以考虑将其声明为一个局部变量,以提升其在作用域链中的查找顺序。

另外还有一点需要注意的是,计算类名为img的所有DOM节点数量的语句 imgs.length执行了不止一遍。当查询所得的DOM节点列表存储到imgs中后,每次通过属性名或索引读取imgs的属性时,DOM都会重复执行一次对页面元素的查找,这个过程本身就会很缓慢。

现代前端框架普遍都采用虚拟DOM的方式来优化对DOM的处理。这里举这个例子仅为了说明,若使用局部变量则可以显著降低对象索引值的查找时间,将上述代码优化后的写法如下:

function process() {
  const doc = document;
  const target = doc.getElementById('target');  
  const imgs = doc.ɡetElementByClassName('img');  
  const len = imgs.length;
  for(let i = 0; i < len ;i++)( 
    const img = imgs[i];  
    //省略相关处理流程  
    //...
    target.appendChild(img) 
  )
}

作用域链的增长

前面讲到可以通过将频繁使用的位于较深作用域链层级中的数据,声明为局部变量来提升标识符解析与访问的速度。若能将全局变量提升到局部变量的访问高度,是否还能提升到比局部变量更高的位置呢?答案是可以的,在当前局部变量作用增前
增加新的活动变量作用域,但这种增长了作用域链的做法用多了会造成过犹不及的效果。
比如with语句,它能将函数外层的变量,提升到比当前函数局部变量还要高的作用域链访问级别上,如下代码由于使用with的缘故,在语句中可直接访问param中的属性值,虽然方便但却降低了show()函数原本局部变量的访问速度,所以应尽量少用。

const param = {
  name: 'Liu',
  value: 25
}
function show(){
  const cnt = 2
  with(param){
    console.log(`name is ${name}`)
  }
}

另一个例子就是经常用来进行异常捕获的try-catch语句,catch代码块被用来处理捕获到的异常,但其中包含错误信息error的作用域高于当前局部变量所在代码块,所以建议不要再catch语句中处理过多复杂的业务逻辑,这样会降低数据的访问速度。

警惕闭包的使用

闭包的特性使函数能够访问局部变量之外的数据,例如下面的代码:

function mkFunc(){
  const name = 'Liu'
  return function showName(){
    console.log(name)
  }
}
const myFunc = mkFunc()
myFunc()

showName()函数就是一个闭包,它在mkFunc()函数执行时被创建,并能访问mkFunc()函数的局部变量name,为此便需要创建一个独立于mkFunc()函数的作用域链。

一般的函数执行完后,其中局部变量所占用的空间会被释放,但闭包的这种特性会延长复函数中局部变量的生命周期。这也就意味着使用闭包可能会带来更大的内存开销及内存可能泄漏的影响。如果闭包内逻辑较为复杂,则与mkFunc和name之间会发生较频繁的跨作用域访问,因此出于对性能的考虑,在使用闭包时要注意其副作用。