Javascript性能优化(二)- 数据访问优化

来源:互联网 发布:驱老鼠超声波软件 编辑:程序博客网 时间:2024/06/07 17:19

目录

      • 目录
      • 数据访问
      • Scope
      • 优化数据检索
      • 原型
      • 原型链
      • 缓存对象成员的值
      • 总结

数据访问

数据的存储位置,关系到代码运行时数据被检索到的速度,JS中有四种数据存储位置:直接量、变量、数组、对象。其中直接量可能比较少听说,其实可以理解为表示匿名函数、匿名对象的一个变量,如var sum = function(a,b){return a+b},sum就是一个函数直接量。

四种数据存储位置中,直接量和局部变量的访问性能微不足道,性能消耗高的是全局变量、数组和对象成员。

Scope

每一个JS函数都是一个对象,函数对象和其他对象一样,拥有编程可以访问的属性和仅供JS引擎使用的属性。其中有一个虚拟属性叫做[[Scope]],[[Scope]]中包含函数作用域中对象的集合(作用域链),它表示当前函数环境可以访问的数据,以链式存在,作用域链的创建改变过程如下:

1. 声明一个函数时函数作用域链中被推入一个可变的全局变量,代表了所有全局范围内的变量。作用域链:A

2. 执行一个函数时会创建一个内部对象(执行完销毁),称其为运行期上下文,运行期上下文会拷贝函数本身的[[Scope]]对象,按照原来的顺序复制到运行期上下文的作用域链中,此时的运行期上下文作用域链:A。然后运行期上下文的作用域链中被推入一个新的对象,名叫‘激活对象’,它存储了所有的局部变量、命名参数、参数集合和this的接口。此时运行期上下文作用域链:B -> A

3. 当函数运行遇到变量时,会检索作用域链,首先会去B中查找是否存在,再去A中查找,显然A中变量访问性能要比B中的更慢。

优化数据检索

  • 既然全局变量的访问性能更慢,那么当一个函数中多次访问全局变量的时候,就应该用一个局部变量来进行缓存。
function fun(){    var doc = document;    a = doc.getElementById("a");    b = doc.getElementById("b");    c = doc.getElementById("c");}
  • 减少使用动态作用域链:with()可以临时改变函数的作用域链,把变量推到作用域链最前端,如下面的demo,运行期上下文作用域链:document -> B -> A,在使用getElementById的时候会先到document对象里查找,速度确实快了。但是问题也显而易见,因为局部变量对象B被推到了第二,所以当访问a,b,c变量的时候,多查找了一次document对象,有时候得不偿失。
// 当使用with时,document临时被放到了作用域链的最前端// 在使用getElemntById时会先到document对象里面寻找function fun(){    with(document){        a = getElementById("a");        b = getElementById("b");        c = getElementById("c");    }}
  • try-catch:当运行出错时,程序会自动把异常推到作用域链的最前端,带来性能问题,可以在catch块中运行错误处理函数,将错误对象作为参数传给错误处理函数,catch块中作用域链的改变就没什么影响了。
try{    //do something}catch(e){    handleError(e);}
  • 慎用闭包:原因有两个,一个是函数闭包访问外部变量最少经过两次查找。二是如果闭包需要访问外部变量,那么函数运行期的激活对象就会被保存,无法销毁,不仅会消耗更多内存,在IE中还会导致内存泄漏。

原型

  • 什么是原型?初学者往往很难理解原型的概念,如果你了解过OOP的编程语言,那么对继承一定不陌生,原型可以简单的理解为被继承的基类。任何时候创建一个实例,这些实例都将自动拥有一个Object作为他们的原型。
  • 一个对象拥有两种成员:实例成员和原型成员。实例成员直接存在于实例自身,原型成员则是从原型继承。
  • 下面的例子中,cat的实例成员是name和age,原型成员就是cat.proto中的成员属性,即Object中的属性(Object.prototype)。如调用toString方法,会先在cat的实例成员中查找,找不到再去原型成员中查找,所以同样导致性能问题。
// cat._proto_ = Objectvar cat = {    name:"xiaohua",    age:1}

原型链

  • 对象的原型决定了一个实例的类型,默认情况下对象都是Object的实例,但当我们使用构造器创建实例时,就创建了另外一种类型的原型。
  • 如下面的例子,cat._proto_ = Animal.prototypeAnimal.prototype._proto_ = Object。如果我们调用了toString,搜索路径如下:cat -> cat._proto_(Animal.prototype) -> cat._proto_.constructor(Animal) -> cat._proto_.constructor._proto_(Object)
function Animal(name,age){    this.name = name;    this.age = age;}// 通过prototype可以向函数对象添加成员Animal.prototype.sayHello = function(){    console.log("Hello,I am a " + this.name);}var cat = new Animal("cat",1);var dog = new Animal("dog",1);

缓存对象成员的值

由原型链的访问过程可知,当访问的对象成员处在较深处时,访问的代价还是挺大的,所以当多次访问同一个对象成员时,可以进行缓存。

function foo(ele,className1,className2) {    // 可以用一个局部变量来存储ele.className    // var eleClassName = ele.className;    return ele.className == className1 || ele.className == className2;}

总结

  • 针对数据访问导致的相关性能问题,主要的解决办法就是对数据进行暂存,比如将全局变量暂存为局部变量,减少作用域链的深入搜索;将实例的属性暂存,减少对原型链的多次深入搜索;另一个就是减少使用动态作用域和闭包。