数据存取

JavaScript中,不同存储位置,他的读取速度是不一样的,就好像一个距离你只有一米的饮料和一个距离你十米的饮料,当然是一米的你拿起来喝的速度最快。

js中有四种基本的数据存取位置:

1.字面量

字面量只代表自身,不存储在特定的位置,js的字面量有:字符串、数字、
布尔值、对象、数组、函数、正则表达式、及特殊的null和undefined值。

你可以这么理解,if(true)语句中的true布尔值就是字面量,他就是一个值,不需要命名什么的就可以用的那种。

2.本地变量

使用var定义的数据存储单元,被作用域影响读取快慢。

3.数组元素

存储在JavaScript数组中的,以数字作为索引。

4.对象成员

存储在JavaScript对象内部,已字符串作为索引。

其中字面量和本地变量读取最快,而数组和对象读取最慢,因为浏览器需要一个个遍历直到找到对应的值。虽然现在js的运行已经很快了,但是如果是大型的项目,他的代码量非常庞大,运行的效率也会被积少成多的小延迟影响到。

作用域

如果一个变量是在window下的,那么他是一个全局作用域下的变量,而如果他是一个function下的变量,那么他就是这个function作用域下的变量,那么function如何访问到他的作用域下的变量,又如何访问到全局的变量,这里就要了解到作用域链了。

首先肯定很多人都会以为这还不简单,既然这个变量在function里面,那么自然就可以获取到里面的变量啊:function ------> 变量

然后实际的原理并不是这样,function创建后,他会有一个内部属性,这个内部属性不能通过代码访问,只能通过浏览器的JavaScript引擎访问,这个属性就是[scope].

scope属性他包含一个该函数被创建的作用域中的对象的集合,也就是全局对象的集合,该集合包含了window、navigator、document...等等可访问的数据对象,函数的作用域中的每个对象也被称之为可变对象,每个可变对象都是以‘键值对’的形式存在(变量名 = 值),这种存在的形式在被查找获取的时候,称之为‘标识符解析’。

当function运行的时候,会创建一个称为执行环境的内部对象,每个执行环境都是独一无二的,多次调用同一个函数就会创建多个执行环境,这里是不是有点想法,比如:函数的独立属性为啥会独立?

当执行环境被创建后,他的作用域链被链接到对应的运行的函数scope属性下,当这个过程完成后,一个被称之为‘活动对象’的新对象也就创建好了,活动对象作为函数运行时的变量对象,包含了函数内所有的局部变量、命名参数、参数集合及this,也就可以理解为函数内所有的变量集合了,这也就是所谓的独立属性了。

这个活动对象会被推至顶端,当函数运行完毕后,这个活动对象也随之销毁。

也就是说,scope属性里的顺序从原来只有一个全局对象集合到现在的:活动对象、全局对象,活动对象顺序高于全局对象,那么这也造成了局部的属性访问速度高于全局的,也就是为什么函数里面有一个name变量,window下也有一个name变量,当你在函数里面调用的时候,会是函数里的name被输出,而不是window下的,因为函数作用域下的变量所在的对象集合优先级高于全局的。

标识符解析

在函数执行的过程中,每遇到一个变量,都会经历一次标识符解析,从而决定从哪里获取或存储数据,该过程搜索执行环境的作用域链,查找同名的标识符,从头部开始依次查询,如果找到运行函数所调用的标识符(可以理解为变量名),就使用对应的变量,如果没有找到,就继续搜索下一个作用域链,若是无法找到匹配的对象,那么这个标识符就会被视为未定义(浏览器报错not defined),也正是这个不断搜索的过程,影响了代码的性能。

标识符解析的性能

标识符解析是有代价的,事实上没有哪种计算机操作可以不产生性能开销。

在执行环境的作用域链中,一个标识符他的位置越深,他的读写速度就越慢,因此,在局部变量中读写总是最快的,而读写全局变量通常是最慢的,但是现在浏览器优化的js引擎,其实影响已经不那么明显了,但是不管浏览器如何优化他的引擎,总的趋势还是这样,一个标识符所在的位置越深,他的读写就越慢。

来个例子:

function initUI() {
  var bd = document.body,
      links = document.getElementsByTagName('a'),
      i = 0,
      len = links.length;

   while(i<len) {
     update(links[i++]);
   }

  document.getElementById('go-bth').onclick = function(event) {
    start();
  }

  bd.className = 'active';
}

上面这段代码,重复调用了全局作用域中document对象,三次调用代表着三次查找,每次都要遍历整个作用域链,直到在全局作用域链中找到document,那么我们可以调整一下:

function initUI() {
  var doc = document,
      bd = doc.body,
      links = doc.getElementsByTagName('a'),
      i = 0,
      len = links.length;

   while(i<len) {
     update(links[i++]);
   }

  doc.getElementById('go-bth').onclick = function(event) {
    start();
  }

  bd.className = 'active';
}

通过创建一个局部变量保存document对象,然后其他的调用只调用doc,这就相当于只查找了一次,大大的提升了性能。如果这个函数里面有十次百次这样的重复调用,那么这样写性能将大大的改善。

改变作用域链

一般来说,一个执行环境的作用域链是不糊改变的,但是有两个语句可以在执行时改变作用域链,第一个就是with语句。

with常常用来避免书写重复的代码,在js中他是这样使用的:

function initUI() {
  with(document) {
    var bd = body,
      links = getElementsByTagName('a'),
      i = 0,
      len = links.length;

     while(i<len) {
       update(links[i++]);
     }

    getElementById('go-bth').onclick = function(event) {
      start();
    }
  
    bd.className = 'active';
  
  }
}

这样写后可以避免重复书写document,看上去更高效了,实际上却没有想象中的那么美好。

首先我们要理解下with到底做了什么才可以减少重复书写?

当with运行时,执行环境的作用域链发生了改变,一个新的变量对象被创建,他包含了被传入的document对象的所有属性,这个新的可变对象被推入作用域链的头部,而函数本身的局部变量作用域链处于第二的位置,这虽然加快了document的属性访问速度,但是函数本身的局部变量却变慢了,其实得到的效果反倒并不如意。

除了with还有try-catch()语句会改变作用域链,try-catch()中的catch子句具有改变作用域链的效果,当try中的代码发生错误,会自动跳到catch语句中去,然后将异常的对象,也就是包含错误信息的对象推入作用域的头部,而函数本身的局部变量将在第二个作用域下,当catch运行完毕,作用域链就会恢复原状。

如何处理这个运行时作用域链被改变的影响呢?

我们可以通过运行一个唯一的函数,通过这个函数来处理返回的错误信息,这样就不用担心临时改变的作用域链对代码整体的影响了。

try {
  methodThatMightCauseAnError();
}catch(e) {
  handleError(e); //通过这个函数来处理错误信息
}

动态作用域

无论是with还是try-catch中的catch子句,或者是包含eval()的函数,他们都被认为是动态作用域。

动态作用域只存在于代码执行的过程中,因此无法通过静态分析(查看代码结构)检测出来。例如:

function execute(code) {
  eval(code);

  function subroutine() {
    return window;
  }
  var w = subroutine();
}

在上面那段代码中,w一般来说就是全局的window对象,但是变量w会随着code的值而发生改变,如下:

execute('var window = {}');

当我们传入一个变量为window的字面量对象时,通过eval将其运行,w其实就等于这个字面量对象了,这里也是要讲讲eval做了什么?

eval会创建一个可变对象,该对象会将传入的字符作为属性保存,然后被推入作用域的头部,此时,w获取到的其实是作用域头部的window了。

闭包、作用域和内存

我们先看一段代码:

function assignEvents() {
  var id = 'xdi9592';

  document.getElementById('sava-bth').onclick = function(event) {
    saveDocument(id);
  }
}

assignEvents函数运行后,给sava-bth元素创建了一个点击事件,这个事件处理函数就是一个闭包,他能访问assignEvents所属作用域的id变量,为了能够让这个闭包获取到id变量,必须创建一个特定的作用域链。

当assignEvents函数运行时,一个包含变量id及其他数据的活动对象被创建,他是作用域链中的第一个对象,然后全局紧随其后,而当闭包创建后,闭包的scope属性会包含和assignEvents函数一样的作用域链引用,这就导致,当assignEvents函数运行完毕后,他的执行环境会常驻内存中,没有被销毁,这也是闭包的一大特性,但这也意味着闭包会比普通函数需要更多的内存开销,这也就是内存泄漏的根本原因,在大型的web项目中,这可能是个问题。

当闭包运行时,他本身又会创建一个该作用域下对象集合的活动对象,而assignEvents函数的活动对象排在第二,全局最后,那么当我们频繁的跨作用域标识符解析,就会造成性能的损失,而解决的办法也和之前一样。

将重复使用的变量作为该作用域下的局部变量即可。

function assignEvents() {
  var id = 'xdi9592';

  document.getElementById('sava-bth').onclick = function(event) {
    var id = id;
    saveDocument(id);
  }
}

原型链带来的性能损失

JavaScript中,所有的对象都是Object的实例,他会从Object继承所有基本方法,如:

var book = {
    title = 'High Performance JavaScript',
    publisher : 'Yahoo! Press'
}

alert(book.toString())   //object Object

我们并没有给book添加toString方法,但是他还是会有,这个方法就是从Object那继承的基本方法中的一个。

我们还可以使用hasOwnProperty()方法判断对象是否含有对应的独立属性。


alert(book.hasOwnProperty('title'))   //true
alert(book.hasOwnProperty('toString'))   //false

这里可以更明确的判断tostring并不是book的独立方法而是继承而来的。

通过in操作符可以查找原型中的方法


alert(title in book)   //true
alert(toStringin book)   //true

返回的都是true,也就是都可以查找到。

那我们通过创建构造函数可以创建另一种类型的原型,该原型可以通过代码继承,这个原型链的不断继承也会不断加深变量的位置,造成性能损失。

具体代码就不写了,构造函数的原型继承而已,很简单。

嵌套成员

由于对象成员可能包含其他的成员,例如不常见的写法:window.location.href。每次遇到点操作符,嵌套的成员就会导致js引擎搜索所有的对象,也就是每次都会运行标识符解析用来查找点操作符后面的属性,那么你嵌套的越深,读取速度就越慢。

location.href的读取速度就会大于window.location.href,如果解析的不是对象的局部属性,那么还会搜索原型链中的内容,这样会花更多的时间。

缓存对象成员值

由于所有类似的性能问题都与对象成员有关,因此应该尽可能避免使用它们。更确切的说是应当注意,只要在有必要时使用。例如,在同一个函数中没必要多次读取同一个对象成员。


function hasEither(element,className1,className2) {
  return element.className == className1 || element.className == className2;

这段代码中重复读取了两次element.className,我们可以创建一个局部变量来保存这个值,从而减少查询次数。


function hasEither(element,className1,className2) {
  var className = element.className;
  return className  == className1 || className  == className2;

通常来说,在函数中如果要重复多次读取同一个对象的属性,最佳的做法就是同个一个局部变量来保存这个属性,从而减少多次查询来来的性能开销。在处理嵌套对象时,这样做会明显提升执行速度。

总结:

  1. 访问字面量和局部变量速度最快,而数组和对象则相对较慢。
  2. 变量在作用域链的位置越深,读取速度越慢,访问时间也会越长,而全局变量是在作用域链的末尾,因此访问速度也是最慢的。
  3. 避免使用with这些改变作用域链的语句,因为他会改变作用域链,同时局部变量也会移至第二的位置。
  4. 嵌套的对象成员越多越影响性能,因此尽量简短或者少用。
  5. 属性和方法在原型链中的位置越深,读取速度也就越慢
  6. 通常来说,将常用的跨作用域链的对象作为局部对象保存使用,这样有利于性能的提升,因为局部的变量访问速度更快。
0
  • 本文分类:javascript高性能
  • 本文标签:数据存取
  • 流行热度:已超过 68 人围观了本文
  • 发布日期:2019年05月26日 - 1时37分33秒
  • 版权申明:本文系作者@木灵鱼儿原创发布在木灵鱼儿站点。未经许可,禁止转载。
微信收款码
微信收款码