《高性能JavaScript》读书笔记(二)

《高性能JavaScript》第二章数据存取,涉及很多优化点,需要花点时间吸收。下面就开始吧。

数据存储不同方式之间的性能比较

第二章一开始就提到了数据存储的问题。js的数据存储分四类:字面量,本地变量,数组元素及对象成员。字面量跟本地变量在存取数据的性能消耗差不多,而数组元素跟对象成员消耗要大一些,但是也跟浏览器的优化有关。所以建议是尽量使用字面量和局部变量。

作用域链

实际开发中,我们会根据项目需求使用各种变量跟方法。为了了解如何去优化代码的性能,就要了解作用域链。

关于作用域链,我在我的第一篇博文《JavaScript作用域》 中有提到过,当时是用链表的形式对全部作用域跟函数作用域做了模拟。这里就不再赘述。

书中的图示(下图1.1)应该是更接近真实JavaScript作用域链的表现形式的,这里需要做一下说明。

图1 书中举例的add()的作用域链
图1 书中举例的add()的作用域链

什么叫作用域链

书中给出的定义,作用域链是一个包含一个函数被创建的作用域中的对象集合,决定了哪些数据能被函数访问。作用域链在函数实例中是以一个内部属性[[Scope]]被储存的。当一个函数被创建时,它的作用域链会先被创建该函数的作用域中可访问的数据对象所填充。作用域链在创建时所填充的数据对象为全局对象。

在函数创建完成并开始执行时,会对执行的变量按照出现在函数中的顺序,被复制(中文是写复制,但是我有个问题,按照变量提升的规则,函数中的局部变量应该在这个时候进行声明,那是不是就不仅仅是复制全局中的变量,还有声明了的变量呢?)到“活动对象”的新对象中。这个对象会被推入作用域链的最前端。如

然后当函数执行完毕,执行环境被销毁,作用域链也会被销毁。如下图1.2

图2 书中举例函数add()执行期的作用域链
图2 书中举例函数add()执行期的作用域链

作用域链涉及的性能问题

当函数在对变量操作时,每一次执行都会需要进行标识符的解析,而这就是性能优化的点。标识符(变量)的位置越深,它的读写速度就越慢。参考图1.1到图1.2的过程,在函数创建完毕,先创建“活动对象”时,如arguments是局部变量,则是直接声明,执行时不需要到全局对象中查找;而window则需要到最顶层的全局对象中查找。

所以我们的性能优化就在于,尽可能的使用局部变量。使用局部变量来赋值全局变量的值,执行的时候只要找局部变量的标识符即可完成查询的性能优化。

with & try-catch

with语句跟catch语句是书中提及的两个可以改变函数作用域链的语句。with语句可以创建一个新的变量对象,这个对象包含了参数指定的对象的所有属性。这个变量对象会推入作用域链的首位,即在活动对象的前面。这个语句使函数中可以避免多次书写参数,同时可以最优先访问参数的所有属性。问题在于函数的局部变量则降低了优先级,导致访问的代价变高了。

如果函数没有什么局部变量而都是操作某个对象的属性为主,那么使用with语句有可取之处,但是如果函数以操作局部变量为主,则建议不要使用with语句来影响查询性能。

同样的道理,catch语句有同样的问题。当try代码捕获错误是,代码的执行会直接跳转到catch语句,并把异常对象推入一个变量对象置于作用域的首位,同样的,catch代码块中,局部变量会降低优先级。

如果要降低查询性能的影响,最好的方案是将这个异常对象作为参数直接委托给一个函数进行处理,这样,在catch代码块内,不会出现局部变量,也不会出现由于作用域链的变化而导致的性能问题。

闭包

闭包的特点我在写《JavaScript作用域》的时候有提到,简单的说,就是可以在函数的外部调用内部的局部变量,并且闭包在执行完之后不会被马上销毁(换句话说,这个也是闭包的弊端:内存泄漏,内存开销)。

实际上,因为闭包会一直保持在内存中,所以其在执行时,闭包创建的活动对象会一直在作用域链的最顶层,而闭包所调用的变量一般是所在执行环境或者更顶层环境的全局变量。这样就出现了查询的性能影响。

如果要降低闭包对代码的性能影响,其一是要慎重的使用闭包,其二是在闭包中使用局部变量复制全局变量。

对象

因为JavaScript很多操作都是对对象的操作,所以对JavaScript对象的了解能对优化代码是有很大的帮助的。

属性及方法

JavaScript对象包括了属性跟方法,区分的点在于对象的成员引用的是一个函数还是非函数类型。

原型及原型链

JavaScript中的对象是基于原型的。原型是所有对象的基础,它已经定义并实现了一个对象所必需包含的成员列表,而且原型对象是为所有对象实例所共享。

下图1.3及代码是书中举的一个例子

1
2
3
4
5
6
var book = {
title: "High Performance JavaScript",
publisher: "Yahoo! Press"
};

alter(book.toString()); // "[object object]"

图3 书中实例book与原型的关系
图3 书中实例book与原型的关系

图中可以看到,实例book虽然创建的时候只声明了两个实例成员,但是并没有定义toString(),然而toString()方法还是被正确执行了,是因为虽然实例book没有定义,但是book的原型对象已经定义了toString()方法。

book.toString()被调用时,先会从实例成员开始搜索,如果实例成员中没有搜索到,则从原型成员搜索,如果能搜索到,则能成功执行,否则才会报错。

JavaScript有两个方案可以检测对象的成员,其中in操作符可以检测对象是否包含某个成员,不管这个成员是属于实例成员还是原型成员;而如果需要检测对象是否包含某个实例成员,则需要用hasOwnProperty()方法。

JavaScript有原型对象,开发者也可以根据项目的需求通过构造函数来创建另一种类型的原型。下图1.4及代码是书中举的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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("JavaScipt: 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]"

图4 书中列举的原型链
图4 书中列举的原型链

代码中,先是定义了一个 构造函数Book,然后在通过Book来创建实例book1book2。其中,book1book2的成员titlepublisher都是各自的成员,拥有不同的值,而两个实例的原型__proto__都是指向Book.prototype,而Book.prototype__proto__又指向原型对象object。整个就是原型链。两个实例共享着一个原型链。

从对原型链的认识,我们可以看到,当执行实例对象的成员时,明显的,成员的位置越深,搜索并访问的时间越长。

嵌套成员

简单的说,对象的成员本身可能就是一个对象,所以会包含有自己的成员,也就是嵌套成员。这种结构会导致访问嵌套成员是,需要从最底层对象的成员开始搜索,而且成员的嵌套得越深,搜索读取的速度就越慢。

因此,优化的方案,主要是在调用对象成员及嵌套成员时,可以把需要多次读取的对象成员复制到局部变量中进行使用。

最后

本章涉及都是JavaScript的一些数据处理。虽然每种数据存储的方式都有访问速度的快慢区别,但是并不代表开发中就应该避免使用访问速度慢的数据存储方式,二是应该根据项目要求进行慎重的使用。而最常见的优化方案仍然是利用局部变量来复制搜索访问比较慢但又需要经常调用的数据。

文中用到的图片跟代码参考《高性能JavaScript》第二章。