1

在计算机科学中,数据存储的位置关系到代码执行过程中数据的检索速度,有一个经典的问题即为:通过改变数据的存储位置来获得最佳的读写性能。


Javascript中四种基本的数据存储位置


字面量
字面量只代表自身,不存储在特定的位置。JavaScript中的字面量有:字符串,数字,布尔值,对象,数组,函数,正则表达式,以及null&undefined。
字面量是用于表达源代码中一个固定值的表示法,例如:string str="hello world";
hello world为字面量


本地变量
开发人员使用关键字var定义的数据存储单元


数组元素
存储在JavaScript数组对象内部,以数字作为索引,这里注意和本地变量的区别,
var arr = new Array();
arr为本地变量,arr[0]为一个数组元素


对象成员
储存在JavaScript对象内部,以字符串作为索引


每一种数据存储的位置都有不同的读写消耗,一般而言:

  • 从一个字面量中存取数据的性能约等于局部变量

  • 数组元素和对象成员成本较高,高出多少由浏览器决定


管理作用域

作用域概念是理解JavaScript的关键所在,不仅仅从性能,还包括从功能的角度。作用域对JavaScript有很多影响,从确定哪些变量可以被函数访问,到确定this的赋值。


作用域链
function可以理解为一个“制造机器的机器”,那么我们可以这样理解:每一个JavaScript函数都是一个function对象的实例
那么function对象和其他对象一样,拥有可以编程访问的属性和一系列不同通过代码访问而仅供JavaScript引擎存取的内部属性


内部属性之Scope
先放一个Scope的有趣解释;
Scope属性包含了一个函数被创建时的作用域中的对象的集合,这个集合被称为作用域链,它决定哪些数据能被函数访问。
函数作用域中的每个对象被称为一个可变对象,每个可变对象都以“键值对”的形式存在。
当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象所填充。
我说下自己的理解:作用域、作用域链、内置属性(Scope)其实可以类比权限、管理组、全局管理员,作用域中的对象以键值对的形式存在,成为可变对象,作用域链用来连接作用域和Scope,而Scope就好像一种专门管理全局对象的全局管理员;


举一个例子:
我们先创建一个函数:

function add(num1,num2){
    var sun = num1 + num2;
    return sum;
}

这里我们创建了一个add()函数,当他被创建的时候,在这个函数的内置属性Scope所包含的作用域链中插入一个对象变量,这个全局对象代表着所有在全局范围内定义的变量。 改全局对象包含像window,navigator,document等;
图片描述


当我们来执行上面的函数又会发生什么呢?
假如执行如下代码:

 var total = add(5,10);

此时函数会创建一个称为 执行环境 或者叫 执行上下文 (execution context)的内部对象。

  • 一个execution context定义了一个函数执行时的环境。

  • 函数每次执行时对应的execution context都是独一无二的,所以多次调用同一个函数就会创建多个不一样的execution context

  • 当函数执行完毕,execution context就会被销毁


  • 每个execution context都有自己的作用域链,用于解析标识符

  • execution context被创建时,它的作用域链初始化为当前运行函数的Scope属性中的对象,这些值按照他们出现在函数中的顺序,被复制到执行环境的作用域链中。

  • 前面这个过程完成之后,一个“活动对象”也为execution context创建好了,该对象作为函数运行时的变量对象,包含了所有的局部变量,参数集合以及this。

  • 然后这个对象被推入作用域链最前端。

  • execution context被销毁,活动对象也随之销毁。
    图片描述


标识符解析的性能


我们在执行过程中是怎样使用作用域链的呢?
在函数执行过程中,没遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取或存储数据。标识符解析是性能开销的,即有代价的!解析标识符实际上就是搜索execution context的作用域链,来匹配同名的标识符。

  • 搜索过程从作用域链头部开始,即作用域链中的数字越小越优先,这意味着,一个标识符所在的位置越深,它的读写速度越慢

  • 函数中读写局部变量总是最快的,而读写全局变量总是最慢的

  • 在查找过程中,如果找到,就使用这个标识符对应的变量,若没找到,则继续查找下一个对象,若整个搜索过程都没有找到匹配的对象,那么这个标识符将被视为未定义的

  • 正是这个搜索过程影响了性能


在没有优化JavaScript引擎的浏览器中,尽可能使用局部变量,一个好的经验法则就是:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储在局部变量里
我们来看一个例子:

function initUI(){
    var bd = document.body,
        links = document.getElementsByTagName("a"),
        i = 0,
        len = links.length;
        while(i<len){
            update(links[i++]);
        }
        document.getElementById("btn").onclick = function(){
            start();
        };

        bd.className = "active";
}

我们看到上面这个函数用了三次document对象,而很不巧,他又是个全局变量,搜索document需要遍历整个作用域链,那么现在有一种解决方案来减少对性能的影响:先将全局变量的引用存储在一个局部变量里面,然后用这个局部变量来替代全局变量;
那么上述代码可以重写为:

function initUI(){
    var doc = document,
        bd = doc.body,
        links = doc.getElementsByTagName("a"),
        i = 0,
        len = links.length;
        while(i<len){
            update(links[i++]);
        }
        doc.getElementById("btn").onclick = function(){
            start();
        };

        bd.className = "active";
}

我们将访问document的次数由三次变成了一次,如果这个访问次数足够大的话,那么我们的性能将得到极大的改善!
学到这里,我认为改善标识符的解析性能可以从提高解析速度和减少使用次数两方面入手,前者通过优化JavaScript引擎来进行,后者我们在编程过程中可以进行实践,两者的前提都是搜索能够正确进行!


改变作用域链

一般来说,一个execution context的作用域链是不会被改变的,但是在JavaScript中有两个语句是可以在执行时临时改变作用域链的,为动态作用域。


NO.1 With语句
With语句用来在作用域链中新创建一个变量对象,这个可变对象包含了参数指定的对象的所有属性。先看看With在编程中怎么使用:

function initUI(){
    with (document){
    var bd = body,
        links = getElementsByTagName("a"),
        i = 0,
        len = links.length;
        while(i<len){
            update(links[i++]);
        }
        getElementById("btn").onclick = function(){
            start();
        };
        bd.className = "active";
    }
}

从代码中可以很直观看到,它也只在全局对象中执行一次搜索,从而避免了多次书写document,但是这样会更加高效吗?
我们来看看执行with语句时,作用域链中发生了什么:
with


当执行with语句时,它的execution context被临时改变了,一个新的对量对象被创建,它包含了参数指定的对象的所有属性,并且这个对象被推入了作用域链的首位;
在上面的例子中,通过把document对象传递给with语句,一个包含了document对象所有属性的新的可变变量被置于作用域的头部,这样就出现了一个问题:我访问document对象的属性非常快,但是当我想访问活动对象(也就是局部变量)或者全局对象的属性时,我的解析标识符速度反而降低了,所以,在减少全局对象属性这方面的性能优化,将document储存在一个局部变量中比用with语句改变作用域链更加可靠!


NO.2 try-catch语句
try-catch语句中的catch字句也具有临时改变作用域链的效果。
try代码块中发生错误,执行过程会自动跳转到catch字句,然后将异常对象推入一个变量对象并置于作用域的首位,也就是说,在catch代码块内部,函数所有的局部变量都会放在第二个作用域链对象中,但是,一旦catch代码块执行完毕,作用域链就会返回到之前的状态。


try-catch 语句不应该被用来解决JavaScript错误,如果某个错误重现率很高,最好是尽快修复。其实作用域链的改变是发生在catch代码块执行的过程中,那么我们如果在catch代码块内没有对局部变量和全局变量的访问,就可以使catch字句对性能的影响最小化!
这种思想的一种实现方法就是将错误委托给一个函数来处理!


闭包的作用域

闭包是JavaScript最强大的特性之一,它允许函数访问局部作用域之外的数据,但是闭包在使用过程种可能会导致性能问题。

我们先来看一个闭包的例子:

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

assignEvents()函数给一个DOM元素设置事件处理函数,这个事件处理函数就是一个闭包,它在assignEvents()执行时创建,并且可以访问所属作用域的id变量。
闭包


如图所示,当assignEvents()函数执行时,一个包含了变量id以及其他数据的活动对象被创建,这个活动对象成为execution context作用域链中的第一个对象,紧接着就是全局对象,然后闭包被创建,并且它的Scope属性被初始化为这些对象。


至此,出现了第一个问题:内存问题。
一般来说,一个函数执行完了之后,函数作用域链中的活动对象会随着execution context一起被销毁,但是引入了闭包之后,由于引用仍然存在于闭包的Scope属性中,所以此时活动对象没法被销毁,这意味着脚本中闭包与非闭包函数相比,需要更多的内存开销。


然后在闭包代码执行时,又会创建一个闭包的execution context,它的作用域链与自身Scope中所引用的两个相同的作用域链对象一起被初始化,然后创建一个闭包的活动对象,并且放在首位;
闭包


可以看到闭包内代码所用的id & savaDocument,他们的位置分列2,3,就里就是我们在使用闭包过程中所需要关注的性能点:在频繁地访问跨作用域的标识符的时候,每次访问都会带来性能损失。


最后划一下重点:将常用的跨作用域变量存储到局部变量中,然后直接通过局部变量来访问,是一个可行的方法。


--END--


未来nan朋友
153 声望1 粉丝

developer live, programmer die!