正在加载中

最后更新于 2019年05月26日

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. 通常来说,将常用的跨作用域链的对象作为局部对象保存使用,这样有利于性能的提升,因为局部的变量访问速度更快。
  • weixiao kaixin tushetou jingkong deyi fanu liezui liuhan daku ganga bishi nanguo lihai qian yiwen numu tu yi haixiu se fadai minyan hehe henkaixin huaji biyiyan kuanghan maimeng shui xiaku penqi zhangzui pen aini ye niu laji ok chigua renshi kongbu shuai xiaoxiese touxiao huaixiao jingnu chihuai kaisang xiaoku koubi zhuangbi lianhong kanbujian shafa zhijing xiangjiao dabian yaowan redjing lazhu rizhi duocang chixigua hejiu xixi xiaopen goukun xiaobuchu shenme wusuowei guancha lajing chouyan xiaochi bie zhadanzui zhadanxiao

登录