如何在数据访问层上提高js的执行效率

标识符解析、作用域链、运行期上下文、原型链、闭包
服务器君一共花费了223.217 ms进行了5次数据库查询,努力地为您提供了这个页面。
试试阅读模式?希望听取您的建议

本文讲到的是如何从数据访问层面上提高JS 代码的执行效率。总的来讲有以下几条原则:

  1. 函数中读写局部变量总是最快的,而全局变量的读取则是最慢的;
  2. 尽可能地少用with 语句,因为它会增加with 语句以外的数据的访问代价;
  3. 闭包尽管强大,但不可滥用,否则会影响到执行速度以及内存;
  4. 嵌套的对象成员会明显影响性能,尽量少用;
  5. 避免多次访问对象成员或函数中的全局变量,尽量将它们赋值给局部变量以缓存。

这么几句话看似简单,但要深刻理解其中的道理则需涉及到JS的 标识符解析、作用域链、运行期上下文(又称为执行环境)、原型链、闭包 等一系列概念,之前我有看过一篇网上翻译的 JavaScript 闭包,文中讲解了这些东东,但几遍下来还是似懂非懂。然而本书则是图文并茂,很好理解,不由的感慨一下,牛人就是牛~

作用域链和标识符解析

每一个JS 函数都表示为一个对象,该对象有一个内部属性[[Scope]],它包含了一个函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链(Scope chain),它决定哪些数据能被函数访问。函数作用域中的每个对象被称为一个可变对象(variable object)。当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象所填充。

例如下面这个全局函数:

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

由于此函数是在全局作用域下创建的,所以函数add() 的作用域链中只填入了一个单独的可变对象——全局对象:

而执行此函数时则会创建一个称为"运行期上下文(execution context)"的内部对象,它定义了函数执行时的环境。函数每次执行时对应的运行期上下文都是独一无二的,所以多次调用同一个函数就会导致创建多个运行期上下文。当函数执行完毕,执行上下文就被销毁。

而我们刚刚讲的函数作用域链和这个运行期上下文有什么关系呢?是这样的,每个运行期上下文都有自己的作用域链,用于标识符解析,而它的作用域链引用的则是函数的内部对象[[Scope]] 所指向的作用域链。此外还会创建一个"活动对象(Activation object)",该对象包含了函数的所有局部变量、命名参数、参数集合以及this,当函数执行时它又被当作可变对象,和前面是一回事。然后此对象会被推入作用域链的前端,就这样运行期上下文也就创建好了,当运行期上下文被销毁,活动对象也随之销毁(闭包除外,后面会讲到)。

例如前面add() 函数运行时对应的运行期上下文和作用域链:

函数执行时,每遇到一个变量都会进行一次标识符解析的过程,该过程从头至尾搜索作用域链,从当前运行函数的活动对象到全局对象,直至查找到同名的标识符,如果没找到则被认为是undefined。

标识符解析的性能

标识符解析是有代价的,在运行期上下文的作用链中,一个标识符所在的位置越深,它的读写速度也就越慢,显然对局部变量的读写是最快的,因为它所在的对象处于作用链的最前端。一个好的经验法则是:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量里。

此外代码执行时临时改变作用域链也会影响标识符解析的性能,有两个语句会造成这种情况——with 和catch,这两条语句被执行时都会将一个新的可变对象(with 的是语句中指定的对象、catch 的是异常对象)推入作用域链的头部,这样原有的可访问对象都被往后推了一个层次,这使得它们的访问代价更高了。因此对于with 语句最好避免使用,catch 语句要用的话可以定义一函数来进行错误的处理以减少catch 内的语句数量。

下图便是一个在with 语句中改变后的作用域链的例子:

闭包、作用域和内存

理解了作用域链之后,闭包也就好懂了。闭包是JavaScript 最强大的特性之一,它允许函数访问局部作用域之外的数据。然而,有一种性能问题与闭包有关,思考如下代码:

function assignEvents() {
    var id = 'xdi9592';
    document.getElementById('save-btn').onclick = function(event) {
        saveDocument(id);
    }
}

函数内部的onclick 事件处理器就是一个闭包,为了能让该闭包访问函数assignEvents() 内的数据,闭包被创建时,它的[[Scope]] 属性被初始化为其外部函数运行时的作用域链中的对象,即闭包的[[Scope]] 属性包含了与运行期上下文作用域相同的对象的引用。

下图为函数assignEvents() 运行期上下文的作用域链和闭包:

通常来说,函数的活动对象会随同运行期上下文一同销毁。但引入闭包时,由于引用仍然存在于闭包的[[Scope]] 属性中,因此激活对象无法被销毁。这意味着脚本中的闭包与非闭包函数相比,需要更多的内存开销。

而当闭包被执行时,一个活动对象会为闭包自身所创建并被置于作用域链的最前端:

此时对于闭包外数据(如id、saveDocument 等) 的访问开销就更大了,因为它们在作用域链的位置均被推后了一个层次。这就是使用闭包最主要的性能关注点:你要经常访问大量跨作用域的标识符,每次访问都会导致性能损失。

对象成员解析、原型、原型链

对象成员指的是对象的属性或方法,当我们要访问TestObj.abc 这样一个对象成员时,首先在标识符解析的过程中找到了TestObj 对象,接下来要访问abc 属性(或是方法)则要进行对象成员解析的过程了。

JavaScript 中的对象是基于原型的,原型是其他对象的基础,它定义并实现一个新对象必须包含的成员列表。对象通过一个内部属性__proto__(这个属性在Firefox、Safari 和Chrome 中对开发者可见) 绑定到它的原型。

对象可以有两种成员类型:实例成员和原型成员。实例成员存在于对象实例中,原型成员则由对象原型继承而来。一旦创建了一个内置对象(如Object 和Array) 的实例,它们就会自动拥有一个Object 实例作为原型,如下面的代码:

var book = {
    title : 'High Performance JavaScript',
    publisher : 'Yahoo! Press'
}
alert(book.toString());  //"[object Object]"

这个例子中book 并没有定义toString() 方法,然而却能被顺利执行,原因是方法toString() 是由对象book 继承而来的原型成员:

注意book 原型中也有__proto__ 属性,前面也说到了JavaScript 的对象是基于原型的,既然每个对象都具有原型,这自然便形成了一个"链",我们称之为原型链,原型链终止于原型为null 的那个对象上。而对象成员的解析实际上就是原型链的遍历过程,从实例成员开始查找到原型成员。

此外也可以定义并使用构造器来创建另一种类型的原型,这样则插入了新定义的原型对象至原型链中,考虑下面这个例子:

function Book(title, publisher) {
    this.title = title;
    this.publisher = publisher;
}
Book.prototype.sayTitle = function() {
    alert(this.title);
}
var book1 = new Book('High Performance JavaScript', 'Yahoo! Press');
var book2 = new Book('JavaScript: The Good Parts', 'Yahoo! Press');

这里为Book 对象手动创建了一个原型,并定义了一个方法sayTitle(),对于实例book1 来说原型链是这样的:book1 的原型 -> Book.prototype, Book.prototype 的原型 -> Object, Object 的原型 -> null。

当要访问一个book1 中的一个成员时,检索其原型链的过程则是:首先查找book1 的实例成员(title, publisher),若没找到则接着查找book1 的原型Book.prototype 中的原型成员(sayTitle),若还没到则继续查找Book.prototype 的原型Object 的原型成员(toString, valueOf ...),若仍然没找到,则继续查找Object 的原型,但Object 的原型为null,则查找终止,此时该成员判定为undefined,若该过程中有查找到的话则立即中止查找并返回。原型链的关系如图:

对象成员解析的性能

和标识符解析一样,对象成员的解析也是有开销的,原型链的遍历过程中,每深入一层都会增加性能的损失,于是对象在原型链中存在的位置越深,找到它就越慢。

另外由于对象成员可能包含其他成员,例如window.location.href,每次遇到点操作符,该嵌套成员都会导致JavaScript 引擎搜索所有对象成员,显然对象成员嵌套得越深,访问速度就会越慢,因此尽量少用,例如执行location.href 总是要比window.location.href 要快。

很显然,当要频繁地访问对象成员时,最好用变量将它们缓存起来。

本文地址:http://www.nowamagic.net/librarys/veda/detail/1011,欢迎访问原出处。

不打个分吗?

转载随意,但请带上本文地址:

http://www.nowamagic.net/librarys/veda/detail/1011

如果你认为这篇文章值得更多人阅读,欢迎使用下面的分享功能。
小提示:您可以按快捷键 Ctrl + D,或点此 加入收藏

大家都在看

阅读一百本计算机著作吧,少年

很多人觉得自己技术进步很慢,学习效率低,我觉得一个重要原因是看的书少了。多少是多呢?起码得看3、4、5、6米吧。给个具体的数量,那就100本书吧。很多人知识结构不好而且不系统,因为在特定领域有一个足够量的知识量+足够良好的知识结构,系统化以后就足以应对大量未曾遇到过的问题。

奉劝自学者:构建特定领域的知识结构体系的路径中再也没有比学习该专业的专业课程更好的了。如果我的知识结构体系足以囊括面试官的大部分甚至吞并他的知识结构体系的话,读到他言语中的一个词我们就已经知道他要表达什么,我们可以让他坐“上位”毕竟他是面试官,但是在知识结构体系以及心理上我们就居高临下。

所以,阅读一百本计算机著作吧,少年!

《C程序设计语言(第2版新版)》 克尼汉 (作者), 等 (作者, 译者), 徐宝文 (译者)

《C程序设计语言》(第2版新版)是由C语言的设计者Brian W.Kernighan和Dennis M.Ritchie编写的一部介绍标准C语言及其程序设计方法的权威性经典著作。全面、系统地讲述了C语言的各个特性及程序设计的基本方法,包括基本概念,类型和表达式、控制流、函数与程序结构、指针与数组、结构、输入与输出、UNIX系统接口、标准库等内容。

更多计算机宝库...