举例详细说明javascript作用域、闭包原理以及性能问题(转)

时间:2021-07-15 17:35:05

转自:http://www.cnblogs.com/mrsunny/archive/2011/11/03/2233978.html

这可能是每一个jser都曾经为之头疼的却又非常经典的问题,关系到内存,关系到闭包,关系到javascript运行机制。关系到功能,关系到性能。

文章内容主要参考自《High Performance JavaScript》,这本书对javascript性能方面确实讲的比较深入,大家有空都可以尝试着阅读一下,下载地址:中英电子版

复习,笔记,更深入的理解。

欢迎拍砖指正。

作用域:

下面我们先搞明白这样几个概念:

  • 函数对象的[[scope]]属性、ScopeChain(作用域链)
  • Execution Context(运行期上下文)、Activation Object(激活对象)

[[scope]]属性:

javascript中每个函数都是一个函数对象(函数实例),既然是对象,就有相关的属性和方法。[[scope]]就是每个函数对象都具有的一个仅供javascript引擎内部使用的属性,该属性是一个集合(类似于链表结构),集合中保存了该函数在被创建时的作用域中的所有对象,而这个作用域集合形成的链表则被称为ScopeChain(作用域链)。

该作用域链中保存的作用域对象,就是该函数可以访问的所有数据。例如(例子引用自《High Performance JavaScript高性能javascript》):

function add(num1, num2){
    var sum = num1 + num2;
    return sum;
}
举例详细说明javascript作用域、闭包原理以及性能问题(转)
图 1

当add函数被创建时,函数所在的全局作用域的全局对象被放置到add函数的作用域链([[scope]]属性)中。我们可以从图1中看到作用域链的第一个对象保存的是全局对象,全局对象中保存了诸如this,window,document以及全局对象中的add函数,也就是他自己。这也就是我们可以在全局作用域下的函数中访问window(this),访问全局变量,访问函数自身的原因。当然还有函数作用域不是全局的情况,等会儿我们再讨论。

Execution Context(运行期上下文)、Activation Object(激活对象):

(前天看了老罗的演讲,老罗说过年的时候给全公司的人每人发一台电冰箱,要给校舍的所有的厕所门上都安上新锁,保证童鞋们能有个真正隐私的地方。)

var total = add(5, 10);

当开始执行此函数时,就会创建一个Execution Context的内部对象,该对象定义了函数运行时的作用域环境(注意这里要和函数创建时的作用域链对象[[scope]]区分,这是两个不同的作用域链对象,这样分开我猜测一是为了保护[[scope]],二是为了方便根据不同的运行时环境控制作用域链。函数每执行一次,都会创建单独的Execution Context,也就相当于每次执行函数前,都把函数的作用域链复制了一份到当前的Execution Context中)。Execution Context对象有自己的作用域链,在Execution Context创建时初始化,会将函数创建时的作用域链对象[[scope]]中的全部内容按照在[[scope]]作用域链中的顺序复制到Execution Context的作用域链中。

此时,在Execution Context的作用域链的顶部会插入一个新的对象,叫做Activation Object(激活对象),这个激活对象又是干嘛的呢?这个激活对象保存了函数中的所有形参,实参,局部变量,this指针等函数执行时函数内部的数据情况,这个Activation Object是一个可变对象,里面的数据随着函数执行时的数据的变化而变化,当函数执行结束之后,就会销毁Execution Context,也就会销毁Execution Context的作用域链,当然也就会销毁Activation Object(但如果存在闭包,Activation Object就会以另外一种方式存在,这也是闭包产生的真正原因,具体的我们稍后讨论。)。具体情况如图所示:

举例详细说明javascript作用域、闭包原理以及性能问题(转)

图 2

我们从左往右看,第一部分是函数执行时创建的Execution Context,它有自己的作用域链,第二部分是作用域链中的对象,索引为1的对象是从[[scope]]作用域链中复制过来的,索引为0的对象是在函数执行时创建的,第三部分是作用域链中的对象的内容Activation Object和Global Object。

函数在运行过程中,没遇到一个变量,都会去Execution Context的作用域链中从上到下依次搜索,如果在第一个作用域链(假如是Activation Object)中找到了,那么就返回这个变量,如果没有找到,那么继续向下查找,直到找到为止,这也就是为什么函数可以访问全局变量,当局部变量和全局变量同名时,会使用局部变量而不使用全局变量,以及javascript中各种看似怪异的、有趣的作用域问题的答案(你可以用这种方法来解释你以前碰到的所有作用域问题,当然,如果还是有疑问的话,非常希望你能贴出代码,我们一起讨论。)

一般情况下,一个函数的作用域链是不会在函数运行时被改变的,但有些运算符会临时改变作用域链,with和try catch的catch子句。看下面的例子:

function initUI(){
    with (document){     //avoid!
        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";
    }//eOf with
}

上例中,在onclick事件的事件处理器中引用了外部函数assignEvents的局部变量id,形成了闭包,下面我们看一下它们的作用域图示:

举例详细说明javascript作用域、闭包原理以及性能问题(转)图 4

我们一起来从作用域的角度分析一下闭包的形成过程:

  1. assignEvents函数创建,词法解析后,函数对象assignEvents的[[scope]]属性被初始化,作用域链形成,作用域链中包含了全局对象的所有属性和方法(注意,此时因为assignEvents函数并未执行,所以闭包函数并没有被解析)。
  2. assignEvents函数执行,在开始执行时,创建Execution Context(我们将图4按照从左到右,从上到下的顺序划分为6部分,第一部分就是运行期上下文),在运行期上下文的作用域链中创建Activation Object(第二、三部分),并将Activation Object放置与作用域链顶点,在其中保存了函数执行时所有可访问函数内部的数据。
  3. 当执行到闭包时,javascript引擎发现了闭包函数的存在,按照通常的手法,将闭包函数解析,为闭包函数对象创建[[scope]]属性,初始化作用域链(此时闭包函数对象的作用域链中有两个对象,一个是assignEvents函数执行时的Activation Object,还有一个是全局对象,图4的4、5、6部分。)。我们看到图中闭包函数对象的作用域链和assignEvents函数的执行上下文作用域链相同?为什么相同呢?我们来分析一下,闭包函数是在assignEvents函数执行的过程中被发现并且解析的,而函数执行时的作用域是Activation Object,那么结果就很明显了,闭包函数被解析的时候它的作用域正是assignEvents作用域链中的第一个作用域对象Activation Object,当然,由于作用域链的关系,全局对象作用域也被引入到闭包函数的作用域链中。 那么我们现在考虑另一个问题,闭包作用域链中的Activation Object,是引用了assignEvents函数的Activation Object,还是拷贝了一个副本到闭包的作用域链中了?我们可以做一个小的测试,在有多个闭包同时引用外层函数局部变量(i)的情况下,如果其中一个闭包改变了i的内容,而其他闭包中的i的内容没有发生改变,则说明产生了拷贝,反之,则引用了同一个Activation Object。
  4. function fn(){
      var i = 0;
      (function(){++i;console.log(i)})();
      (function(){++i;console.log(i)})();
    }
    
    
    fn();
    //1
    //2
  1. 我们发现变量i从1变为了2,说明两个闭包引用的是同一个变量i,也就说明他们引用的fn的Activation Object是同一个,其实完全可以换一种非常简单的方式来解释:全局对象肯定是同一个吧?
  2. 下面讨论当闭包函数执行时的情况,因为在词法分析的时候闭包函数就已经在作用域链中保存了对assignEvents函数的Activation Object的引用,所以当assignEvents函数执行完毕之后,闭包函数虽然还没有开始执行,但依然可以访问assignEvents的局部数据(并不是因为闭包函数要访问assignEvents的局部变量id,所以当assignEvents函数执行完毕之后依然保持了对局部变量id的引用。而是不管是否存在变量引用,都会保存对assignEvents的Activation Object作用域对象的引用。因为在词法分析时,闭包函数没有执行,函数内部根本就不知道是否要对assignEvents的局部变量进行访问和操作,所以只能先把assignEvents的Activation Object作用域对象保存起来,当闭包函数执行时,如果需要访问assignEvents的局部变量,那么再去作用域链中搜索)。
  3. 闭包函数执行时创建了自己的Execution Context和Activation Object,在运行期上下文的作用域链中保存了自己的Activation Object,外层函数assignEvents的Execution Context的Activation Object,以及Global Object,如图:

举例详细说明javascript作用域、闭包原理以及性能问题(转)图 5

这也就是闭包为何能“记得”在它周围到底发生了什么,为何闭包能访问外层函数的局部数据,为何闭包能保持这些局部数据而不在外层函数执行完毕销毁时一起销毁等等的原因。

前些天一个前辈(Darrel文叔)告诉我一句话,一针见血:没有内存,就没有闭包。

性能问题:

在作用域链和闭包中的性能问题主要表现在数据读写的速度上。

由于作用域链的原因,我们访问全局作用域的数据(这里为什么不说变量呢?因为不仅包括变量,还有函数,对象等其他内容)时,效率是最低的,而访问局部数据时的效率是最高的。

所以一个非常经典的解决数据访问性能问题的方案出现了:将需要访问的数据尽量的以局部数据的方式缓存起来。这样当标识符解析程序在作用域链中寻找数据时,直接就可以在作用域链的最上层找到想要的数据,效率自然就提升了。

这句话可以解决很多性能问题:设置缓存,将数据保存在局部变量中。

转载请注明出处:

参考: