JavaScript中有四种基本的数据存取位置:直接量、变量、数组元素、对象成员。
大多数情况下,直接量、变量的读取比数组元素和对象成员的读取性能要好一些,而且不同浏览器可能有不同的性能差异。
作用域
作用域链和标识符
每个JavaScript函数都可以表示为一个对象,更确切地说是Function对象的一个实例。Function对象和其他对象一样,拥有可以编程访问的属性和一系列不能通过代码访问仅供JavaScript引擎存取的内部属性,其中一个内部属性就是[[Scope]]
内部属性[[Scope]]包含了一个函数被创建的作用域中对象的集合。这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。函数作用域中的每个对象称为一个可变对象,每个可变对象都以“键值对”的形式存在。当一个函数创建后,它的作用域链就被创建此函数的作用域中可访问的数据对象所填充。
function add (num1,num2) {
var sum = num1 + num2;
return sum;
}
在全局作用域下创建此函数,则该函数的作用域链会添加一个全局对象。如图所示。
当执行函数时
var total = add(5,10);
此时执行此函数时会创建一个称为运行期上下文的内部对象。一个运行期上下文定义了一个函数执行时的环境。函数每次执行时对应的运行期上下文都是独一无二的,所以多次调用同一个函数就会导致创建多个运行期上下文。当函数执行完毕,执行期上下文就会被销毁。
当运行期上下文被创建时,它的作用域链初始化为当前运行函数的[[Scope]]属性中所包含的对象。
在函数执行的过程中,每次遇到一个变量,都会搜索运行期上下文的作用域链,如果没找到,则继续搜索作用域链中的下一个对象。正是这样的搜索过程影响了性能。
一个标识符所在的位置越深,它的读写速度就越慢。因此,函数中读写局部变量总是最快的,而读写全局变量通常是最慢的。注意,全局变量总是存在于运行期上下文作用域链的最末端。
在没有优化的JavaScript浏览器中,建议尽可能使用局部变量。如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量里。
改变作用域链
改变作用域链有三个语句,一个是with,一个是try…catch,另一个是eval()。
当代码执行到with语句时,运行期上下文的作用域链临时被改变了。一个新的可变对象被创建,它包含了参数所指定的对象的所有属性。这个对象被推入作用域链的头部,这意味着函数此时的所有局部变量现在处于第二作用域链对象中,也就是说访问的代价更高。
当try代码块中才发生错误,执行过程会自动跳转到catch子句,然后把一场对象推入一个可变对象并之余作用链的头部。在catch代码块内部,函数所有局部变量将会放在第二个作用域链对象中。一旦catch子句执行完毕,作用域链就会返回原来的状态。
为了执行最小化catch子句对性能的影响。一个很好的模式是将错误委托给一个函数来处理。如下代码:
try {
methodThatMightCauseAnError();
} catch (ex) {
handleError(ex); // 委托给处理器方法
}
由于catch子句中只执行一条语句,而且没有局部变量的访问,作用域链的临时改变就不会影响代码的性能。
动态作用域
with语句、try-catch的catch语句、包含eval()的函数,都被认为是动态作用域。动态作用域只存在于代码执行过程中,因此无法通过静态分析(查看代码)检测出来。
例如:
function execute(code) {
eval(code);
function subroutine() {
return window;
}
var w = subroutine();
// w是什么?
}
正常情况下,w是window对象,但是如果execute(“var window = {}”);此时创建了一个局部变量window。此时w得不到想要的结果。所以,使用动态作用域时需要小心。
闭包、作用域和内存
闭包是JavaScript最强大的性能之一,它允许函数访问局部作用域之外的数据。
function assignEvents() {
var id = "dsds";
document.getElementById("save-btn").onclick = function(event) {
saveDocument(id);
}
}
这个点击事件处理器就是一个闭包,它被assignEvents()执行时创建,并且能访问所属作用域的id变量。为了让这个闭包能够访问id,需要创建一个特定的作用域链。
当assignEvents()函数被执行时,一个包含了变量id以及其他数据的活动对象被创建,它称为运行期上下文作用域链中的第一个对象,而全局对象紧随其后。当闭包被创建时,它的[[Scope]]属性被初始化为这些对象。如图:
通常来说,函数的活动对象会随运行期上下文一同销毁,但引入闭包的[[Scope]]属性中,此时激活对象无法被销毁。这意味着脚本中的闭包和非闭包函数相比,需要更多的内存开销。
当该闭包执行时,一个运行期上下文被创建,如图:
闭包的主要性能关注点:要经常访问大量跨作用域的标识符,每次访问都会导致性能损失。
在脚本中,最好小心地使用闭包,它同时关系到内存和执行速度。减轻它对执行速速的影响:将常用的跨作用域变量存储在局部变量中,然后直接访问局部变量。
对象成员
对象通过一个内部属性绑定到它的原型,这个内部属性是_proto_。
对象有两种成员类型,一种是实例成员和原型成员。实例成员存在于对象实例中,原型对象则由对象原型继承而来。
var book = {
// ...
// ...
}
book.toString();
为什么一个普通实例book可以调用book.toString(),这是由于toString()是对象book继承而来的原型对象。默认情况下,所有对象都是对象(Object)的实例,并继承了所有基本方法。
为什么访问对象成员速度比访问直接量或变量要慢?
解析对象成员的过程和解析变量十分相似,当book.toString()被调用时,会从对象实例开始,搜索名为toString()的成员。一旦book没有该成员,则会搜索book的原型对象,一直沿着原型链找下去,直到找到该成员函数并执行。也就是说对象book可以访问它原型的每一个属性和方法。这也就是访问对象成员比较慢的原因。
如何区分实例成员属性和原型成员属性?
可以使用in 和 hasOwnproperty()区别,代码如下:
var book = {
title: "title",
publisher: "publisher"
};
console.log(book.hasOwnproperty("title")); // true
console.log(book.hasOwnproperty("toString"));// false
console.log("title" in book);//true
console.log("publisher" in book);//true
使用构造器创建另一种类型的原型
function Book(title,publisher) {
this.title = title;
this.publisher = publisher;
}
Book.prototype.sayTitle = function() {
alert(this.title);
};
var book1 = new Book("title1","publisher1");
var book2 = new Book("title2","publisher2");
alert(book1 instanceof Book); // true
alert(book1 instanceof Object); // true
book1.sayTitle(); // "title1"
alert(book1.toString()); // [Object Object]
book1的原型(proto)是Book.prototype,Book.prototype的原型是Object。关系如图:
从图中可以看出,两个Book实例共享一个原型链。每个实例都有自己的title和publisher属性,其他部分都继承自原型。
对象在原型链中存在的位置越深,找到它就越慢。
嵌套成员例如window.location.href,每次遇到点操作符,JavaScript引擎就会搜索所有对象成员。
当多次访问对象成员的时候可以将值存储在局部变量中来减少一次查找。但是要注意,不能将一个方法保存在局部变量中,因为许多对象对象方法会使用this来判断上下文,把一个方法保存在局部变量会导致this绑定到window,导致程序出错。