第 2 章 数据访问
《高性能 JavaScript》—— Nicholas C. Zakas
存储数据的位置关系到代码执行过程中数据的检索速速。
JS 有 4 中基本的数据存储位置:
1.字面量,代表自身,有: 字符串、数字、布尔值、对象、数组、函数、正则、 null 、 undefined
2.变量,开发人员使用 var 定义的
3.数组元素,存储在 JS 数组中,以数字作为索引
4.对象成员,存储在 JS 对象中,以字符串作为索引
字面量和局部变量的访问速度较快。
1. 作用域
在函数里,作用域决定哪些变量可以访问 以及 this 如何赋值。
1.1. 作用域链
JS 中的函数是对象(Function 类的实例), 有一个内部属性 [[Scope]] ,这个属性即为 作用域链,它决定哪些数据可以被函数访问, 作用域链中元素称为 变量对象(variable object),由键值对组成。
当函数创建时,函数所在作用域的数据将会赋值给变量对象; 也就是说,定义函数的那个作用域的所有变量都会塞到 [[Scope]] 中的变量对象上。
如下:
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
以上代码执行完后,add() 函数被创建, 该函数所在作用域为全局作用域(顶级作用域), 全局作用域变量对象用 global object 表示,该变量对象中有 window 、document 、全局变量等, 如下图:

创建函数时的作用域链将在函数调用时用到,如下代码:
var total = add(5, 10);
执行 add 函数时,会创建一个内部对象,该内部对象称为 执行上下文(execution context)。 执行上下文 就是函数执行时的环境,每次调用函数都会创建一个新的执行上下文, 函数调用完毕后 执行上下文 会被销毁。
执行上下文 有自己的作用域链,用于解析标识符; 执行上下文 一旦创建,就用函数创建时的 [[Scope]] 会拷贝到自己的作用域链, 拷贝 [[Scope]] 后,会创建一个 activation object (活动对象)变量对象,并置于作用域链的顶端, activation object 包含 局部变量、命名参数、arguments 对象、this 。 如下图所示:

1.2. 解析标识符
在函数执行时,遇到变量(标识符),会在 执行上下文 的作用域链中找, 找到了则返回,找不到则视为 undefined 。 在作用域链中查找标识符的快慢影响性能。
作用域链由一个个变量对象组成,每个变量对象包含一个作用域的标识符; 查找的标识符(所在变量对象),越靠前 查找越快。
当多个变量对象存在相同标识符时,取第一个。
标识符所在的变量对象越靠后,读写变量的速度就越慢, 局部变量最快,全局变量最慢。(Chrome 专门优化过,差别不大)。
建议尽量使用局部变量,将非当前作用域的变量存储到局部变量中,如下:
function initUI(){
var doc = document;
var bd = doc.body;
var links = doc.getElementsByTagName("a");
var i= 0;
var len = links.length;
while(i < len){
update(links[i++]);
}
doc.getElementById("go-btn").onclick = function(){
start();
};
bd.className = "active";
}
1.3. 作用域链扩充
一般 执行上下文 的作用域链不会改变,但 with 和 try catch 会扩充作用域链。
使用 with(obj) 语句会将 obj 作为变量对象插入到作用域链的顶部,如下:
function initUI(){
with (document){
var bd = body,
links = getElementsByTagName("a"),
i= 0,
len = links.length;
while(i < len){
update(links[i++]);
}
getElementById("go-btn").onclick = function(){
start();
};
bd.className = "active";
}
}
作用域链,如下:

此时,虽然访问 document 中的属性(方法)会非常快,但局部变量的访问变慢了,最好不要使用 with 语句。
而 try catch 也是一样,代码如下:
try {
methodThatMightCauseAnError();
} catch (ex){
alert(ex.message);
}
当发生错误时,会执行 catch 子句,此时会将异常对象 ex 所在的变量对象插入到作用域链的顶部; 建议不要在 catch 子句中执行大量逻辑。
1.4. 动态作用域
function execute(code) {
eval(code);
function getW(){
return window;
}
var w = getW();
};
execute("var window = {};");
with 、try catch 、函数中的 eval() 都是动态作用域, 因为在代码执行时才能确定其变量对象,所以 JS 引擎的静态代码分析也就不起作用了。 因此,在确定有必要时才使用它们。
1.5. 闭包、作用域、内存
闭包是函数的特性,(有时称函数为闭包) 闭包能使函数访问局部作用域之外的数据,(闭包的 [[Scope]] 是外层函数执行上下文的作用域链) 一般在嵌套函数中,如下:
function assignEvents(){
var id = "xdi9592";
document.getElementById("save-btn").onclick = function handler(event) {
saveDocument(id);
};
}
handler 函数就是一个闭包(函数), 当 assignEvents() 执行时,handler 函数被创建, assignEvents 的执行上下文的作用域链中的所有变量对象会添加到 handler 函数的 [[Scope]] , 如下图:

由于闭包的[[Scope]] 会包含外层函数的执行上下文的作用域链, 会导致外层函数执行完毕后执行上下文不能销毁,需要更多的内存开销。
当 handler 执行时,[[Scope]] 中的变量对象会拷贝到执行上下文的作用域中,如下图:

在闭包中,如果频繁访问外层函数的活动对象(Activation object)会有性能问题。
2. 对象成员
JS 对象包括自定义的和内置的(DOM、BOM 等)。
访问 JS 中的对象的成员的速度比字面量或变量慢。
2.1. 原型
传统面向对象语言(如 Java)通过类来定义新对象的属性和方法; 而 JS 中的新对象是基于原型对象的,类似于新对象继承原型对象的属性和方法。
对象中有一个 __proto__ 属性,用于获取(或设置)原型对象。
对象中的属性分为两类: 对象本身的(实例成员/自有属性),对象原型上的(原型成员)。 如下:
var book = {
title: "High Performance JavaScript",
publisher: "Yahoo! Press"
};
console.log(book.toString());
如下图:

解析对象成员的过程跟解析变量的非常相似, 先在对象本身上找,没找到则到对象的原型上去找,找到则返回。
可以通过 Object.prototype.hasOwnProperty(name) 判断对象上是否有指定自有成员,如下:
var book = {
title: "High Performance JavaScript",
publisher: "Yahoo! Press"
};
console.log(book.hasOwnProperty("title"));
console.log(book.hasOwnProperty("toString"));
console.log("title" in book);
console.log("toString" in book);
2.2. 原型链
(默认情况下所有对象都是 Object 类的实例。)
对象的原型决定该对象的类型,(类型即原型),可以通过构造函数来创建新的类型。 如下:
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);
alert(book1 instanceof Object);
book1.sayTitle();
alert(book1.toString());
原型链,如下图:

所有的 Book 的实例共享相同的原型链。
原型成员越深,查找速度越慢。
2.3. 嵌套成员
对象成员也是对象,则会形成嵌套成员,如 window.location.href 。
嵌套得越深,查找速度越慢。
2.4. 缓存对象成员的值
在同一个函数里,不要多次查找同一个对象的成员,如下:
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;
}
不建议保存成员方法(函数),因为方法大多依赖 this 。
3. 总结
在 JS 中,数据存储的位置有四种: 字面量、变量、数组元素、对象成员。
- 访问 字面量、局部变量 速度最快
- 局部变量位于作用域链的顶端,查找速度快;全局变量位于作用域链的低端,查找速度慢。
- 不要使用
with ,它会改变 activation object - 嵌套的对象成员,嵌套得越深查找速度越慢
- 成员属性在原型链中的位置越深,查找速度越慢
- 可以将查找速度慢的数据缓存起来
|