JS的作用域、闭包原理以及性能问题

来源:互联网 发布:conoha windows 编辑:程序博客网 时间:2024/05/23 13:44

作用域:

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

函数对象的[[scope]]属性、ScopeChain(作用域链)

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


[[scope]]属性:

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

该作用域链中保存的作用域对象,就是该函数可以访问的所有数据。

例如:

function add(num1, num2){

    var sum = num1 + num2;

    return sum;

}

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


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

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";


       }


}


当代码执行到with时,Execution Context的作用域链被临时改变了,一个新的可变对象被插入到作用域链的顶部,这个可变对象包含了with指定的对象的所有属性。如果此时在with 中访问函数的局部变量,就会先把新插入的可变对象遍历一遍,然后才会去Activation Object中查找,直到找到为止,此时查找效率就会降低(这也是很多人说不要使用with的原因,我认为只要设法不影响性能就行了,毕竟访问with语句指定的对象的属性还是很快的,关于性能的问题大家如果想了解的话,可以关注我的下一篇博文《javascript数据访问性能》),当try catch语句中try语句块中的代码发生错误时,会自动跳入catch语句块,并且会把catch语句指定的异常对象插入到作用域链的顶端,但catch有个特点,就是catch子句执行完毕之后,作用域链都会返回到原来的状态。


闭包:

A "closure" is an expression (typically a function) that can have free varuables together with an environment that binds those variables (that "closes" the expression). —— ECMA262

“闭包”是一个表达式(一般是函数),它具有自由变量以及绑定这些变量的环境(该环境“封闭了”这个表达式)。

闭包就是能够读取其他函数内部变量的函数。

对于闭包这个经典的话题,网上的前辈高手已经做过很多详尽的解释,如果我再过多的说明,显得有些班门弄斧,不过,对于闭包,理解的角度不同,看到的面可能就不一样。

这里我们从作用域的角度来分析一下闭包产生的方式和特点。

我们都知道,闭包允许我们访问闭包函数作用域之外的作用域内的数据(说简单点就是可以闭包允许我们访问闭包函数之外的函数的数据。),这是闭包的一个非常强大的功能,很多复杂的网页应用都和这个特性有关,例如:创建封闭的命名空间、保留外部函数执行环境。


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

function assignEvents(){


    var id = "xdi9592";


    document.getElementById("save-btn").onclick = function(event){


        saveDocument(id);


    };


}

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


性能问题:

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

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


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

0 0
原创粉丝点击