高性能的JavaScript--数据访问(2)

动态作用域

无论是with表达式还是try-catch表达式的catch子句,以及包含()的函数,都被认为是动态作用域。一个动态作用域只因为代码运行而存在。因此无法通过静态分析(查看代码机构)来确定(是否存在作用域)。例如:

function execute(code) {
(code);
function subroutine(){
return window;
}
var w = subroutine();
//what value is w?
};

execute()函数看上去像一个动态作用域,因为它使用了()。w变量的值与code有关。大多数情况下,w将等价于全局变量window对象,但是请考虑如下情况:

execute("var window = {};")

这种情况下,()在execute()函数中创建了一个局部的window变量,所以这个w将等价于这个局部的window变量而不是全局的那个。所以说,不运行这段代码是没有办法了解具体情况的,标识符window的确切含义不能预先确定。

 

闭包,作用域,和内存

 闭包是JavaScript最强大的一个方面,它允许函数访问局部范围之外的数据。闭包的使用在当今最复杂的网页应用中无处不在,不过,有一种性能影响与闭包有关。

为了解闭包的有关性能问题,考虑下面的例子:

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

assignEvents()函数为一个dom元素制定了一个事件处理句柄,此事件处理句柄是一个闭包,当assignEvents()执行时创建,可以访问其范围内部的id变量,用这种方法封闭对id变量的访问,必须创建一个特定的作用域链。

当assignEvents()被执行时,一个激活对象被创建,并包含了一些应有的内容,其中包括id变量。它将成为运行期上下文作用域链上的第一个对象,全局对象是第二个。当闭包创建时,[[Scope]]属性与这些对象一起被初始化。

 

由于闭包的[[Scope]]属性包含与运行期上下文作用域链相同的对象引用,会产生副作用。通常,一个函数的激活对象与运行期上下文一同销毁。当涉及闭包时,激活对象就无法销毁了,因为引用任然存在于闭包的[[Scope]]属性中,这意味着脚本中的闭包与非闭包函数相比,需要更多的内存开销。在大型网页应用中,这可能是个问题,尤其在IE中更被关注。IE使用非本地JavaScript对象实现DOM对象,闭包可能导致内存泄露。

当闭包被执行时,一个运行期上下文将被创建,它的作用域链与[[Scope]]中引用的两个相同的作用域同时被初始化,然后一个新的激活对象为闭包自身被创建。

主要闭包中使用的两个标识符,id和saveDocument,存在于作用域链第一个对象之后的位置上。这是闭包最主要的性能关注点:你经常访问一些范围之外的标识符每次访问都导致一些性能损失。

在脚本中最好是小心地使用闭包,内存和运行速度都值得被关注。将常用的域外变量存入局部变量中,然后直接访问局部变量。

对象成员

 大多数JavaScript代码以面向对象的形式编写。无论通过创建自定义对象还是使用内置对象,诸如文档对象模型(DOM)和浏览器对象模型(BOM)之中的对象。因此,存在很多对象成员访问。

对象成员包括属性和方法,在JavaScript中,二者差别甚微。对象的一个命名成员可以包括任何数据类型。既然函数也是一种对象,那么对象成员除了传统的数据类型外,也可以包含一个函数。当一个成员用了一个函数时,它被称作一个“方法”,而一个非函数类型的数据则被称作“属性”。

原形

 对象成员比直接量或局部变量访问速度慢,在某些浏览器上比访问数组项还要慢。这和JavaScript中对象的性质有关。

JavaScript中的对象是基于原形的,原形是其他对象的基础,定义并实现了一个新对象所必须具有的成员。这一概念完全不同于传统面向对象编程中“类”的概念,它定义了创建新对象的进程。原形对象为给定类型的对象实例所共享,因此所有实例共享原型对象的成员。

一个对象通过一个内部属性绑定到它的原形。Firefox ,Safari和Chrome向开发人员开放这一属性,称作__proto__,其他浏览器貌似不允许脚本访问这一属性。任何时候你创建一个内置类型的实例,如object或者Arrary,这些实例自动拥有一个Object作为他们的原形。

因此,对象可以有两种类型的成员:实例成员(“own”成员)和原形成员。实例成员直接存在于实例自身,而原形成员则从对象原形继承。如:

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

如 book对象有两个实例成员:title 和publisher,注意它并没有定义tostring()接口,但是这个接口却被调用了,也没用抛出错误。toString()函数就是一个book对象继承的原形成员。

原形链

 

对象的原形决定了一个实例的类型。默认情况下,所有对象都是Object 的实例,并继承了所有基本方法。如toString()。也可以用“构造器”创建另外一种类型的原形。

 

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");
alert(book1 instanceof Book);  //true
alert(book1 instanceof Object);  //true
book1.sayTitle();  //"High Performance JavaScript"
alert(book1. toString()); //"[object Object]"

Book构造器用于创建一个新的Book实例。book1的原形(__proto__)是Book.prototype,Book.prototype的原形是Object。这就创建了一个原形链,book1和book2继承了他们的成员。
主要的是,两个Book实例共享同一个原形链。每个实例拥有自己的title和publisher属性,但是其他成员均继承自原形。当book1.toString()被调用时,搜索工作必须深入原形链才能找到对象成员"toString",深入原形链越深,搜索速度就越慢。每深入原形链一层都会增加性能损失。搜索实例成员的过程比访问直接量或者局部量负担更重,所以增加遍历原形链的开销正好放大了这种效果。

嵌套成员

由于对象成员可能包含其他成员,例如不太常见的写法window.location.href这种模式。每遇到一个点号,JavaScript引擎就要在对象成员上执行一次解析过程。成员嵌套越深,访问速度越慢。location.href总是快于window.location.href,而后者也要比window.location.href.toString()更快。如果这些属性不是对象的实例属性,那么成员解析还要在每个点上索搜原形链,这将需要更长时间。

缓存对象成员的值

由于所有这些性能问题与对象成员有关,所以如果可能的话就避免使用他们。更确切的说,只有在必要情况下使用对象成员。例如没有理由在一个函数中多次读取同一个对象成员的值:

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

element.className被访问了两次,我们可以存入一个局部变量,消除一次搜索过程:

function hasEitherClass(element, className1, className2){
var currentClassName = element.className;
return currentClassName == className1 ||  currentClassName == className2;
}

一般来说,如果在同一函数中你要多次读取同一个对象属性,最好将它存入到一个局部变量。以局部变量替代属性,避免多余的属性查找带来的性能开销。在处理嵌套对象成员时这点特别重要,他们会对运行速度产生难以置信的影响。

总结

1.在JavaScript中,数据存存储的位置可以对代码整体性能产生重要影响。有4种数据类访问类型:直接变量,变量,数组项,对象成员。他们有不同的性能考虑。

2.直接变量和局部变量访问速度非常快,数组项和对象成员需要更长时间。

3.局部变量比域变量快,因为它位于作用域链的第一个对象中。变量在作用域链中的位置越深访问所需的时间就越长。全局变量总是最慢的,因为它们总是位于作用域链的最后一环。

4.避免使用with表达式,因为它改变了运行期上下文的作用域链。而且应当小心对待try-catch表达式catch子句,因为它具有同样的效应。

5.嵌套对象成员会造成重大性能影响,尽量少用。

6.一个属性或方法在原形链中的位置越深,访问速度就越慢。

 

posted @ 2014-01-25 23:37  小汪哥写代码  阅读(1787)  评论(0编辑  收藏  举报