读书笔记 – 浏览器端高性能JavaScript编程2

数据存取揭示了JavaScript的原理。
阅读了这一部分,将对JS性能优化的本质有了最基础的掌握。

四种基本存储位置

  1. 字面量
    只代表自身,不存在特定的地方。如,字符串、数字、布尔值、对象、数组、函数、正则表达式、null,undefined。
    比如:”hello world”、123、true、{}、[]、/\s/、null、undefined

  2. 本地变量
    使用var定义的数据存储单元。var i, j, k;

  3. 数组元素
    存储在JS数组对象内部,以数字作为索引。

  4. 对象成员
    存储在JS对象内部,以数字作为索引。

读写差异

对以上4种存储位置的读写会有差异,大多数情况:
1. 访问字面量、本地变量 性能差异微不足道。
2. 访问数组元素、对象成员代价较高,在不同浏览器中也有差异。
在原书中,作者尝试了200,000次读取变量存储位置,试验得出毫秒级别的差异。
在老旧浏览器中,字面量与对象成员的差异可能是3~20ms。在现代浏览器中,这些差异逐渐缩小。

在最新的Chrome桌面浏览器中,执行20万次对象成员读取的时间,仅仅用了1~2ms。

优化?

性能的提升不代表鼓励浪费。编写高性能的代码是一种品德~!
因为存储方式之间的差异,在开发中应鼓励使用字面量和本地变量,减少数组项和对象成员的使用。
具体方法如下:

管理作用域

管理作用域的基础是了解作用域链。

[[Scope]]是仅供JavaScript引擎存取访问的内部属性之一,所以我们没办法用JS直接访问。如图,是一种结构化的描述,假设有一个函数,

var total = add(5, 10);
function add(num1, num2) {
  var sum = num1 + num2;
  return sum;
}

JS引擎在执行add(5, 10)的时候,会创建一个“执行环境(有的人翻译为运行时上下文)”, 也就是这个Scope内部对象。而作用域链(Scope Chain)决定了这个对象能访问的数据。
函数每次执行时的Scope都是独一无二的,所以多次调用同一个函数会导致创建多个执行环境。函数执行完毕后,Scope会被销毁。
add函数的Scope拥有两个作用域链,Activation object(局部活动对象)、Global object(全局对象)。当然图中并没有列举出所有的全局对象。
请注意,Global中包含了add函数它自身,this指向 window; Activation中this同样指向window。
Activation中的对象,是按在在函数中出现的顺序确定的,如this、arguments、形参、函数体内声明的变量。
Global,顾名思义,就是全局,this、window、document、add、total…… 。

函数指向过程中,每遇到一个变量,都会经历一次标识符解析过程,并决定从哪里读/写数据。这个过程会搜索执行环境的作用域链,查找同名的标识符。
搜索顺序是从作用域链的头部开始,也就是Activation对象,如果找到了,就使用这个标识符对应的变量,如果没找到,就往下搜索。如果遍历的所有作用域链还是没找到,那么标识符被视为未定义。

以上代码中,在函数里访问num1、num2、sum时,都会产生搜索过程,对性能产生影响。

优化标识符解析

标识符解析的开销,主要是在执行环境的作用域链中,如果藏得位置太深,查找就会费时间,读写速度也就越慢。因此,在函数中读写局部变量总是最快的,读写全局变量往往是最慢的(*不同的JavaScript引擎可能会做些优化)。
看到这里,你想到了什么?

function() {
  var w = window;
  w.alert();
  w.console.log();
  // w.....
}

我们可以在需要频繁访问全局对象之前,用局部变量引用全局对象。

避免改变作用域链

with、try-catch中的catch,都会改变作用域链。这些语句执行时在Scope中会临时插入一个新的作用域链,那么遍历活动对象和全局对象的路径就会变得更深。
通常的做法,不用with。
而try-catch是比较有用的,得当的用法是:

function handleError(exception) {
  //...
}
try {
  //....
} catch (exception) {
   handleError(exception);
}

利用委托机制,使用函数 handleError()代理exception。
而在函数handleError执行时,scope的作用域链就没那么深了。

动态作用域

书中写到了一种比较特殊的状况

function execute(code) {
  var w;
  eval(code);
  w = window;
}

with、try-catch中的catch、eval函数都被认为是动态作用域。但eval的存在使这种情况更加特殊。
比如code=”var window = {};”,执行eval(code)时,window的作用域会被动态修改。
这与with、try-catch是不同的,因为无法预知code到底是什么值,所以JS引擎无法优化,只能按遍历的方式检查作用域链。
因此,编码时尽量避免这种情况是为上策。

待续。。。

参考

  1. 英文原版链接
    《高性能JavaScript》Nicholas C. Zakas著,由丁琛翻译,赵泽欣校准,2015。英文原版的第一版时间是2010年。

  2. Wiki-火狐浏览器JS引擎

  3. 性能测试工具Speedometer

  4. 最详细·浏览器工作过程讲解

发表评论

电子邮件地址不会被公开。 必填项已用*标注