JavaScript中有四种基本的数据存取位置:
- 直接量
直接量只代表自身,不存储在特定位置。JS中的直接量有:字符串,数字,布尔值,对象,数组,函数,正则表达式,以及特殊的null和undefined值。
- 变量
用var定义的数据存储单元
- 数组元素
存储在JS数组对象内部,以数字作为索引
- 数据成员
存储在JS对象内部,以字符串为索引
作用域工作原理和标识符解析
关键字:内部属性[[Scope]]、可变对象、运行期上下文、执行环境、活动对象
1. 作用域工作原理
作用域链的作用:存储执行环境可以访问的数据,使用方法是标识符解析
每一个JS函数都表示为一个对象,更确切的说,是Function对象的一个实例。Function对象同其他对象一样,拥有可以编程访问的属性,和一系列不能通过代码访问而仅供JS引擎存取的内部属性。内部属性[[Scope]]指向一个函数被创建的作用域中对象的集合。这个集合被称为函数的作用域链,它决定哪些数据可以被函数访问。函数作用域中的每个对象被称为可变对象,每个可变对象都以“键值对”的形式存在。当一个函数被创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象所填充。思考下面的全局函数
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
当函数add()创建时,它的作用域链填入了一个单独的可变对象,包含了函数能访问到的所有数据。本可变对象是时刻存在的全局对象。
函数add的作用域将会在执行时用到。
var total = add(1, 2);
执行此函数时会创建一个称为”运行期上下文”的内部对象。一个运行期上下文定义了一个函数的执行环境。函数每次执行时对应的运行期上下文都是独一无二的,所以多次调用同一函数就会导致创建多个运行期上下文。当函数执行完毕,执行期上下文会被销毁。
每个运行期上下文都有自己的作用域链,用于标识符解析。当它被创建时,它的作用域链被初始化为当前运行函数的[[Scope]]属性所指向的对象。这些值按照它们出现在函数中的顺序,被复制到运行期上下文的作用域链中。这个过程一旦完成,一个被称为”活动对象”的新对象就为运行期上下文创建好了。活动对象作为函数运行期中的可变对象,包含了所有局部变量、命名参数、参数集合arguments以及this.然后此对象被推入到作用域链的前端。当运行期上下文被销毁,活动对象也随之销毁。
2. 标识符解析过程
在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取或存储数据。该过程搜索运行期上下文的作用域链,查找同名的标识符。搜索过程从作用域链头部开始,也就是当前运行函数的活动对象。如果找到了,就使用这个标识符对应的变量;如果没找到,继续搜索作用域链中的下一个对象。搜索过程会持续进行,直到标识符被找到,或者没有可用于搜索的对象为止,这种情况下标识符被认为是未定义的。函数执行过程中,每个标识符都要经历这样的搜索过程。
对于所有浏览器而言,总的趋势是,一个标识符所在的位置越深(作用域链末端),它的读写速度也就越慢。一个好的经验法则是:如果某个跨作用域值在函数中被引用一次以上,那么就把它存储在局部变量里。
闭包,作用域和内存
官方解释为: 所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
我认为闭包就是能够读/写函数内部的某些变量的子函数,并将这些变量保存在内存中.
function assignEvents() {
var id = ‘index’;
document.getElementById(“save-btn”).onclick = function(event) {
saveDocument(id);
};
}
函数给一个DOM元素设置事件处理器。这个事件处理器就是一个闭包,它在函数执行时被创建,并且能访问所属作用域的id变量。为了让这个闭包访问id变量,必须创建一个特别的作用域链。
当assignEvents()函数被执行时,一个包含变量id的以及其他一些数据的活动对象被创建。它成为运行期上下文作用域链中第一个对象,而全局对象紧随其后。当闭包被创建时,它的[[Scope]]属性被初始化为这些对象。(如图)
因为闭包的[[Scope]]属性包含了与运行期上下文作用域相同的对象的引用,因此会有一些副作用。
通常的活动对象会随同运行期上下文一同销毁。但引入闭包时,由于引用仍然存在闭包的[[Scope]]属性中,因此活动对象无法被销毁。这意味着闭包的存在,需要更多的内存开销。
当闭包被执行时,一个运行期上下文被创建,它的作用域链与属性[[Scope]]中引用的两个相同的作用域链对象同时被初始化,然后一个活动对象为闭包自身所创建。
注意在闭包中用到的两个标识符,id和saveDocument,它们存在作用域链第一个对象之后的位置。这就是使用闭包最主要的性能关注点:要经常访问大量跨作用域的标识符,每次访问都会导致性能损失。
闭包的应用环境
1. 保护函数内的变量安全
2. 在内存中维持一个变量
对象成员
访问对象成员的速度比访问直接量或变量更慢,在某些浏览器中比访问数组元素还要慢。
JS中的对象是基于原型的。原型是其他对象的基础,定义并实现了一个新对象必须包含的成员列表。这一概念完全不同于传统面向对象编程语言的‘类’的概念,‘类’定义了创建新对象的过程。而原型对象为所有对象实例所共享,因此这些实例也共享了原型对象的成员。
实例通过一个内部属性绑定它的原型__proto__,比如一旦你创建一个内置对象(Object、Array)的实例,它们就会自动拥有一个Object实例作为原型。
实例可以有两种成员类型:
实例成员(也称‘own’成员):存在对象实例中
原型成员:由对象原型继承而来。
var Book = {
title: “JavaScript”,
content: ‘JavaScript是一门博大精深的语言’
};
alert(BooK.toString()); //[object Object]
方法toString()是对象Book继承而来的原型成员。解析对象成员的过程同解析变量十分相似。当Book.toString()
被调用时,会从对象实例开始,搜索名为’toString’成员。如果实例中不存在,那么会继续搜索其原型对象,直到
toString()方法被找到并且执行。实例可以访问它原型中的每一个属性和方法。
hasOwnProperty()判断对象是否包含特定的实例成员。
in操作符判断对象是否包含特定的属性。
原型链
对象的原型决定了实例的类型。默认情况下,所有对象都是Object的实例,并继承了所有基本方法。
嵌套成员
window.localhost.href每次遇到点操作符,嵌套成员会导致JavaScript引擎搜索所有对象成员。这些属性不是对象的实例属性,那么成员解析还需要搜索原型链,这会花更多的时间。
缓存对象成员值,不适合缓存对象的方法。
小结:
- 访问直接量和局部变量速度最快,相反,访问数据元素和对象成员相对较慢。
- 由于局部变量存在于作用域的起始位置,因此访问它比访问跨作用域变量更快。变量在作用域链中的位置越深,访问时间就越长。全局变量的访问所需时间最长,因为它处于作用域链的最末端。
- 嵌套的对象成员会明显影响性能,尽量少用。
- 属性或方法在原型链中的位置越深,访问它的速度也越慢(和访问变量相似)。
- 通常来说,可以通过把常用的对象成员、数组元素、跨域变量保存在局部变量中来改善JS性能,因为局部变量访问速度更快。
摘自:高性能JavaScript